├── .gitignore ├── .gitmodules ├── CMakeLists.txt ├── LICENSE ├── README.md ├── benchmark ├── README.md ├── process_results.py └── run_benchmark.py ├── deps └── predicates │ └── predicates.c ├── media ├── blender_file.png ├── nodes.png ├── parameterize.png ├── spherical_uniformize.png └── teaser.png ├── render ├── ProjectiveInterpolation.blend ├── SphericalProjectiveInterpolation.blend └── equirect.png └── src ├── .clang-format ├── .clang-tidy ├── CMakeLists.txt ├── Clausen.cpp ├── Clausen.h ├── CommonRefinement.cpp ├── CommonRefinement.h ├── CommonRefinement.ipp ├── ConeFlattening.cpp ├── ConeFlattening.h ├── ConePlacement.cpp ├── ConePlacement.h ├── Cutter.cpp ├── Cutter.h ├── DirectSum.h ├── DirectSum.ipp ├── FlipFormulas.cpp ├── FlipFormulas.h ├── GeometryUtils.cpp ├── GeometryUtils.h ├── IO.cpp ├── IO.h ├── IO.ipp ├── Layout.cpp ├── Layout.h ├── Logger.cpp ├── Logger.h ├── Optimizer.cpp ├── Optimizer.h ├── Parameterize.cpp ├── SphericalUniformization ├── CMakeLists.txt ├── CorrectWindowsPaths.cmake ├── FindPackageMultipass.cmake ├── PartiallyDecoratedTriangulation.cpp ├── PartiallyDecoratedTriangulation.h ├── PetscWrapper.cpp ├── PetscWrapper.h ├── ResolveCompilerPaths.cmake ├── SphericalDemo.cpp ├── SphericalUniformization.cpp ├── SphericalUniformization.h ├── findPETSc.cmake └── polyscope │ ├── SurfaceProjectiveSphericalParameterizationQuantity.cpp │ ├── SurfaceProjectiveSphericalParameterizationQuantity.h │ ├── SurfaceProjectiveSphericalParameterizationQuantity.ipp │ ├── bindata.h │ └── bindata_map_equirectangular.cpp ├── Tracing.cpp ├── Tracing.h ├── Triangulation.cpp ├── Triangulation.h ├── Utils.cpp ├── Utils.h ├── Utils.ipp ├── polyscope ├── SurfaceProjectiveParameterizationQuantity.cpp ├── SurfaceProjectiveParameterizationQuantity.h └── SurfaceProjectiveParameterizationQuantity.ipp ├── testConeFlattening.cpp └── tests ├── CMakeLists.txt ├── CommonRefinementTests.h ├── ExampleTests.h ├── FlippingTests.h ├── NumericalNormalCoordinatesComputations.h ├── TracingTests.h ├── bunny_tiny.obj ├── test.cpp └── test_utils.h /.gitignore: -------------------------------------------------------------------------------- 1 | # Build directories 2 | build/ 3 | build_debug/ 4 | 5 | # Editor and OS things 6 | .DS_Store 7 | .vscode 8 | *.swp 9 | tags 10 | 11 | # Prerequisites 12 | *.d 13 | 14 | # Compiled Object files 15 | *.slo 16 | *.lo 17 | *.o 18 | *.obj 19 | 20 | # Precompiled Headers 21 | *.gch 22 | *.pch 23 | 24 | # Compiled Dynamic libraries 25 | *.so 26 | *.dylib 27 | *.dll 28 | 29 | # Fortran module files 30 | *.mod 31 | *.smod 32 | 33 | # Compiled Static libraries 34 | *.lai 35 | *.la 36 | *.a 37 | *.lib 38 | 39 | # Executables 40 | *.exe 41 | *.out 42 | *.app 43 | 44 | # Blender stuff 45 | *.blend1 -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "deps/polyscope"] 2 | path = deps/polyscope 3 | url = https://github.com/MarkGillespie/polyscope.git 4 | branch = CEPS_features 5 | [submodule "deps/geometry-central"] 6 | path = deps/geometry-central 7 | url = https://github.com/MarkGillespie/geometry-central.git 8 | [submodule "deps/googletest"] 9 | path = deps/googletest 10 | url = https://github.com/google/googletest.git 11 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.10.0) 2 | 3 | project(conformal_equivalence_of_polyhedral_surfaces) 4 | 5 | # == Export compile commands 6 | set (CMAKE_EXPORT_COMPILE_COMMANDS ON) 7 | 8 | ### Configure output locations 9 | set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) 10 | set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) 11 | set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) 12 | 13 | # Print the build type 14 | if(NOT CMAKE_BUILD_TYPE) 15 | set(CMAKE_BUILD_TYPE "Release" CACHE STRING "Choose the type of build, options are: Debug Release" FORCE) 16 | endif() 17 | message(STATUS "cmake build type: ${CMAKE_BUILD_TYPE}") 18 | 19 | ### Configure the compiler 20 | # This is a basic, decent setup that should do something sane on most compilers 21 | 22 | if ("${CMAKE_CXX_COMPILER_ID}" MATCHES "Clang" OR "${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU") 23 | 24 | # using Clang (linux or apple) or GCC 25 | message("Using clang/gcc compiler flags") 26 | SET(BASE_CXX_FLAGS "-std=c++14 -g3") 27 | SET(DISABLED_WARNINGS " -Wno-unused-parameter -Wno-unused-variable -Wno-unused-function -Wno-deprecated-declarations -Wno-missing-braces -Wno-unused-private-field") 28 | SET(TRACE_INCLUDES " -H -Wno-error=unused-command-line-argument") 29 | 30 | if ("${CMAKE_CXX_COMPILER_ID}" MATCHES "Clang") 31 | message("Setting clang-specific options") 32 | SET(BASE_CXX_FLAGS "${BASE_CXX_FLAGS} -ferror-limit=3 -fcolor-diagnostics") 33 | SET(CMAKE_CXX_FLAGS_DEBUG "-fsanitize=address -fno-limit-debug-info") 34 | elseif ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU") 35 | message("Setting gcc-specific options") 36 | SET(BASE_CXX_FLAGS "${BASE_CXX_FLAGS} -fmax-errors=5") 37 | SET(DISABLED_WARNINGS "${DISABLED_WARNINGS} -Wno-maybe-uninitialized -Wno-format-zero-length -Wno-unused-but-set-parameter -Wno-unused-but-set-variable -Wno-deprecated-copy") 38 | endif() 39 | 40 | SET(CMAKE_CXX_FLAGS "${BASE_CXX_FLAGS} ${DISABLED_WARNINGS}") 41 | SET(CMAKE_CXX_FLAGS_DEBUG "-g3") 42 | SET(CMAKE_CXX_FLAGS_RELEASE "-O3 -march=native -DNDEBUG") 43 | 44 | elseif ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "MSVC") 45 | # using Visual Studio C++ 46 | message("Using Visual Studio compiler flags") 47 | set(BASE_CXX_FLAGS "${BASE_CXX_FLAGS} /W4") 48 | set(BASE_CXX_FLAGS "${BASE_CXX_FLAGS} /MP") # parallel build 49 | SET(DISABLED_WARNINGS "${DISABLED_WARNINGS} /wd\"4267\"") # ignore conversion to smaller type (fires more aggressively than the gcc version, which is annoying) 50 | SET(DISABLED_WARNINGS "${DISABLED_WARNINGS} /wd\"4244\"") # ignore conversion to smaller type (fires more aggressively than the gcc version, which is annoying) 51 | SET(DISABLED_WARNINGS "${DISABLED_WARNINGS} /wd\"4305\"") # ignore truncation on initialization 52 | SET(CMAKE_CXX_FLAGS "${BASE_CXX_FLAGS} ${DISABLED_WARNINGS}") 53 | set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} /MD") 54 | set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} /MDd") 55 | 56 | add_definitions(/D "_CRT_SECURE_NO_WARNINGS") 57 | add_definitions (-DNOMINMAX) 58 | else() 59 | # unrecognized 60 | message( FATAL_ERROR "Unrecognized compiler [${CMAKE_CXX_COMPILER_ID}]" ) 61 | endif() 62 | 63 | 64 | # == Deps 65 | add_subdirectory(deps/geometry-central) 66 | add_subdirectory(deps/polyscope) 67 | add_subdirectory(src) 68 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Mark Gillespie 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Discrete Conformal Equivalence of Polyhedral Surfaces 2 | 3 | C++ demo for "[Discrete Conformal Equivalence of Polyhedral Surfaces](http://www.cs.cmu.edu/~kmcrane/Projects/CEPS/index.html)" by [Mark Gillespie](https://www.markjgillespie.com), [Boris Springborn](http://page.math.tu-berlin.de/~springb/), and [Keenan Crane](http://www.cs.cmu.edu/~kmcrane/) at SIGGRAPH 2021. 4 | 5 | PDF: [link](http://www.cs.cmu.edu/~kmcrane/Projects/CEPS/CEPS.pdf) 6 | Talk: [20 minute version](https://www.youtube.com/watch?v=wwuHFUQLlWA), [5 minute version](https://www.youtube.com/watch?v=W0iBHF5-r-M) 7 | 8 | ![Examples of parameterized meshes](media/teaser.png) 9 | 10 | This algorithm takes as input a manifold surface mesh and produces parameterizations that are locally injective and discretely conformal in an exact sense. Unlike previous methods for discrete conformal parameterization, the method is guaranteed to work for any manifold triangle mesh, with no restrictions on triangulation quality or cone singularities. Stress tests involving difficult cone configurations and near-degenerate triangulations indicate that the method is extremely robust in practice, and provides high-quality interpolation even on meshes with poor elements. 11 | 12 | If this code contributes to academic work, please cite as: 13 | ```bibtex 14 | @article{Gillespie:2021:DCE, 15 | author = {Gillespie, Mark and Springborn, Boris and Crane, Keenan}, 16 | title = {Discrete Conformal Equivalence of Polyhedral Surfaces}, 17 | journal = {ACM Trans. Graph.}, 18 | volume = {40}, 19 | number = {4}, 20 | year = {2021}, 21 | publisher = {ACM}, 22 | address = {New York, NY, USA} 23 | } 24 | ``` 25 | 26 | ## Getting started 27 | On mac/linux, you can set up this project with the following commands. 28 | ```bash 29 | git clone --recursive https://github.com/MarkGillespie/CEPS.git 30 | cd CEPS 31 | mkdir build 32 | cd build 33 | cmake -DCMAKE_BUILD_TYPE=Release .. 34 | make -j7 35 | bin/parameterize /mesh/path.obj --viz 36 | ``` 37 | On Windows, Visual Studio can use the provided CMake files to build and run the project. 38 | 39 | ## Usage 40 | You can parameterize meshes by running `bin/parameterize /path/to/mesh` executable. The input mesh must be manifold and connected. 41 | 42 | ![Screenshot of parameterized filigree100k](media/parameterize.png) 43 | 44 | The script takes a variety of arguments. 45 | 46 | |flag | purpose| 47 | | ------------- |-------------| 48 | |`--curvatures=curvatures.txt`| Specify curvatures (angle defects) at given vertices| 49 | |`--scaleFactors=scaleFactors.txt`| Specify log scale factors at given vertices| 50 | |`--ffield=a_mesh.ffield`| Use cones from an [MPZ](http://vcg.isti.cnr.it/Publications/2014/MPZ14/)-style cross field| 51 | |`--greedyConeMaxU=5`| Maximum allowed log scale factor when placing cones (lower value = lower distortion in final parameterization, default value=5)| 52 | |`--outputMeshFilename=a_mesh.obj`| File to save output mesh to, along with homogeneous texture coordinates| 53 | |`--outputLinearTextureFilename=a_mesh.obj`| File to save output mesh to, along with linear texture coordinates (aka ordinary uv coordinates)| 54 | |`--outputMatrixFilename=a_matrix.spmat`| File to save output interpolation matrix to | 55 | |`--outputVertexMapFilename=vertex_map.txt`| File to save vertex map to. For each vertex of the input mesh, the vertex map gives its index in the output mesh | 56 | |`--outputLogFilename=a_log.tsv`| File to save logs (timing + injectivty) to | 57 | |`--useExactCones`| Do not lump together nearby cones in the ffield input, if any. Cones prescribed via `--curvatures` or `--scaleFactors` are never lumped| 58 | |`--noFreeBoundary`| Do not impose minimal-area-distortion boundary conditions (useful, e.g. if prescribing polygonal boundary conditions with the `--curvatures` option)| 59 | |`--viz`| Show the GUI | 60 | |`--version`, `-v`| Version info | 61 | |`--help`, `-h`| Display help | 62 | 63 | ### File formats 64 | The input mesh may be an `obj`, `ply`, `off`, or `stl`. 65 | 66 | Curvatures and scale factors should be given in text files where each line has a 0-indexed vertex index and a prescribed curvature/scale factor. Lines starting with `#` are ignored: 67 | ``` 68 | # A curvature file 69 | 0 1.57 70 | 36 3.14 71 | 12 -1.57 72 | ... 73 | ``` 74 | In the curvatures file, all vertices must be accompanied by a desired curvature. In the scale factors file, vertices may appear without any prescribed scale factor---such vertices are assigned a scale factor of 0. 75 | 76 | The interpolation matrix is exported as a list of triplets. Explicitly, each line of the output file contains the row index, column index, and value of some entry of the matrix. These indices are 1-indexed to make it easy to load in [Matlab](https://www.mathworks.com/help/matlab/ref/spconvert.html). 77 | 78 | ### Homogeneous vs Linear texture coordinates 79 | The parameterizations that we compute look smoother when visualized projectively (see Fig 3 from the paper). Thus by default, we output texture coordinates in _homogeneous coordinates_---rather than the standard uv coordinates, we output 3-dimensional uvw texture coordinates. To visualize these textures you can interpolate the 3d coordinates linearly across each triangle. Then, for each pixel you perform a homogeneous divide, dividing the first two coordinates by the last coordinate to obtain the final uv texture coordinates. This can be done e.g. in a shader or via shader nodes in blender (see `ProjectiveInterpolation.blend` for an example). 80 | 81 | If you want ordinary uv coordinates rather than homogeneous texture coordinates, you can set the `--outputLinearTextureFilename` flag or switch the 'Saved Texture Type' to `Linear` 82 | 83 | ## Visualization 84 | The `render/` directory contains a blender file (`ProjectiveInterpolation.blend`) that can load and visualize meshes that you parameterize. The blender file should open to a Python script in the `Scripting` workspace. You can load your own uniformized mesh by changing the mesh name in the script and clicking on `Run Script`. This will load your model and apply a correctly-interpolated checkerboard texture. 85 | 86 | ![Screenshot of the provided blender file](media/blender_file.png) 87 | 88 | ## Spherical uniformization 89 | 90 | ![Screenshot of spherically uniformizated duck](media/spherical_uniformize.png) 91 | 92 | By default, this project only performs planar uniformization. 93 | 94 | The spherical uniformization procedure requires [PETSc](https://petsc.org/), which can be installed from [here](https://petsc.org/release/install/install_tutorial/). During the installation process, PETSc should give you a `PETSC_ARCH` and `PETSC_DIR`. Once you have completed the installation, you can build the spherical uniformization code by running 95 | ```bash 96 | cmake -DCMAKE_BUILD_TYPE=Release -DSPHERICAL_UNIFORMIZATION=ON -DPETSC_DIR=YOUR_PETSC_DIR -DPETSC_ARCH=YOUR_PETSC_ARCH .. 97 | make -j7 98 | ``` 99 | (where `YOUR_PETSC_DIR` and `YOUR_PETSC_ARC` are whatever PETSc told you during installation). 100 | 101 | Then you can spherically-uniformize meshes by running `bin/spherical_uniformize /path/to/mesh`. 102 | 103 | |flag | purpose| 104 | | ------------- |-------------| 105 | |`--outputMeshFilename=a_mesh.obj`| File to save output mesh to, along with its homogeneous spherical parameterization| 106 | |`--outputLinearTextureFilename=a_mesh.obj`| File to save output mesh to, along with linear texture coordinates (aka ordinary uv coordinates)| 107 | |`--outputMatrixFilename=a_matrix.spmat`| File to save output interpolation matrix to | 108 | |`--outputLogFilename=a_log.tsv`| File to save logs (timing + injectivty) to | 109 | |`--viz`| Show the GUI | 110 | |`--version`, `-v`| Version info | 111 | |`--help`, `-h`| Display help | 112 | 113 | (Note that the output mesh file may not technically be a valid `obj` file, as we store texture coordinates with 4 components). 114 | 115 | The world map here is from [Natural Earth](https://www.naturalearthdata.com/). 116 | 117 | ### Visualizing spherical uniformization 118 | The output of `spherical_uniformize` can be loaded into blender in the `render/SphericalProjectiveInterpolation.blend` file. The texture can be rotated by going to the Shading tab, selecting the uniformized mesh, and adjusting the Euler angles on the `Vector Rotate` node. You can load another texture in the equirectangular projection in the environment texture node (currently labeled `equirect.jpg`). 119 | 120 | ![Screenshot of blender nodes](media/nodes.png) 121 | 122 | ## Test scripts 123 | The `benchmark/` directory has test scripts that you can use to evaluate the performance of this code. For more details, see the [README in that directory](benchmark). 124 | -------------------------------------------------------------------------------- /benchmark/README.md: -------------------------------------------------------------------------------- 1 | ## Running a benchmark 2 | `run_benchmark.py` runs the cone flattening code on a dataset that you provide and records the algorithm's performance. 3 | The script produces a `tsv` file recording some performance statistics for each mesh in the input. These files can then be analyed by `summarize_results.py`. 4 | 5 | |flag | purpose| 6 | | ------------- |-------------| 7 | |`--dataset_dir=/path/to/meshes`| Directory of mesh files to run on (required). | 8 | |`--output_dir=/path/to/output/dir`| Directory to store results (required). | 9 | |`--good_list=meshes_to_use.txt`| File of meshes which should be used. By default, all meshes are used. | 10 | |`--bad_list=meshes_to_skip.txt`| File of meshes which should be excluded. | 11 | |`--n_threads=1` | Number of threads to run on (default=1). | 12 | |`--timeout=600` | Timeout in seconds (default=600). | 13 | |`--max_meshes=1000` | Maximum number of meshes to process. By default, all meshes are used. | 14 | |`--use_ffield_cones` | Use cones from an MPZ-style `.ffield` file (must have the same base name as the mesh file, and be in the same directory. | 15 | |`--save_parameterized_meshes` | Save parameterizations as well as performance statistics. | 16 | 17 | ## Summarizing benchmark results 18 | Run `process_results.py your_output_dir` to summarize the results and plot the algorithm runtime (saved to `your_output_dir/analysis`) along with a `csv` containing the statistics from all meshes. 19 | 20 | |flag | purpose| 21 | | ------------- |-------------| 22 | |`--name=DatasetName` | Dataset name to use in figure captions. | 23 | |`--plot_runtime` | Plot the algorithm runtime. | 24 | |`--merged_files` | Name of a `csv` to read, instead of reading in individual mesh records. | 25 | -------------------------------------------------------------------------------- /benchmark/process_results.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Consume results from a dataset test, building a table for analysis and making plots 4 | 5 | import os 6 | import subprocess 7 | import shutil 8 | import argparse 9 | 10 | import numpy as np 11 | import pandas as pd 12 | import seaborn as sns 13 | import matplotlib.pyplot as plt 14 | import matplotlib.ticker as ticker 15 | import matplotlib.gridspec as gridspec 16 | from itertools import takewhile 17 | from matplotlib import rc 18 | import os 19 | import sys 20 | import signal 21 | import random 22 | from math import log 23 | 24 | def signal_handler(signal, frame): 25 | print("\nprogram exiting gracefully") 26 | sys.exit(0) 27 | 28 | signal.signal(signal.SIGINT, signal_handler) 29 | 30 | # Style options 31 | sns.set() 32 | 33 | sns.set(font="Linux Biolinum O", font_scale=1.5) 34 | # rc('text', usetex=True) 35 | 36 | keenan_purple = "#1b1f8a" 37 | alt_color = "#1f8a1b" 38 | sad_color = "#ff0000" 39 | 40 | def ensure_dir_exists(d): 41 | if not os.path.exists(d): 42 | os.makedirs(d) 43 | 44 | def main(): 45 | 46 | parser = argparse.ArgumentParser() 47 | parser.add_argument('dir', help="directory with result") 48 | parser.add_argument('--record_files', action='store_true') 49 | parser.add_argument('--max_files', type=int, default = -1, help="read in at most max_files many of the input files") 50 | parser.add_argument('--name', default = "", help="data set name to use in captions") 51 | parser.add_argument('--merged_files', default = "", help="csv of dataset") 52 | parser.add_argument('--plot_runtime', action='store_true', help="plot parameterization runtime") 53 | args = parser.parse_args() 54 | 55 | plot_time_vs_mesh_size = args.plot_runtime 56 | 57 | # Place results here 58 | out_dir = os.path.join(args.dir, "analysis") 59 | ensure_dir_exists(out_dir) 60 | 61 | # Parse in the individual CSVs 62 | frames = [] 63 | bad_frames = [] 64 | bad_files = [] 65 | 66 | synthetic_timeout = 2000 67 | 68 | if args.merged_files: 69 | df = pd.read_csv(args.merged_files, header=0, sep=',',engine='python') 70 | # drop rows whose times are too long 71 | bad_rows = df[df['duration'] > synthetic_timeout ].index 72 | df.drop(bad_rows, inplace=True) 73 | bad_df = None 74 | print(df) 75 | print("Done loading df") 76 | else: 77 | data_files = os.listdir(args.dir) 78 | if (args.max_files > 0): 79 | data_files = os.listdir(args.dir)[:args.max_files] 80 | for i,f in enumerate(data_files): 81 | if not f.endswith(".tsv"): 82 | continue 83 | 84 | if i % 10 == 0: 85 | # print("loading " + str(f)) 86 | wdth = 50 87 | progress = int(i / len(data_files) * wdth) 88 | bar = "[" + (progress * "=") + ((wdth-progress)*" ") + "]" 89 | print(f"\rLoading Data {bar} ({int(progress / wdth * 100)}%)", end="", flush=True) 90 | # print("loading " + str(f)) 91 | try : 92 | frame = pd.read_csv(os.path.join(args.dir,f), header=0, sep='\t',engine='python') 93 | 94 | # remove leading/trailing whitespace from column names 95 | frame = frame.rename(columns=lambda x:x.strip()) 96 | 97 | # replace inf w/ np.inf, allowing for spaces around "inf" 98 | frame = frame.replace(to_replace=r"^\s*inf\s*$", value=np.inf, regex=True) 99 | 100 | # replace nan w/ np.nan, allowing for spaces around "nan" 101 | frame = frame.replace(to_replace=r"^\s*nan\s*$", value=np.nan, regex=True) 102 | 103 | # replace " false" w/ False, allowing for spaces around "false" 104 | frame = frame.replace(to_replace=r"^\s*false\s*$", value=False, regex=True) 105 | except: 106 | frame = pd.DataFrame() 107 | if frame.empty or "name" not in frame.columns: 108 | bad_files.append(f) 109 | elif not isinstance(frame.at[0, "nVertices"], (int,float, np.integer)) or not isinstance(frame.at[0, "duration"], (int, float, np.integer)): 110 | print(f"Vertex type {frame.at[0, 'nVertices']} : {type(frame.at[0, 'nVertices'])}") 111 | print(f"duration type {frame.at[0, 'duration']} : {type(frame.at[0, 'duration'])}") 112 | bad_files.append(f) 113 | # elif frame.shape[1] != 20 or pd.isnull(frame.at[0, 'nvertices']): 114 | elif pd.isnull(frame.at[0, 'nVertices']): 115 | bad_frames.append(frame.head(1)) 116 | else: 117 | frames.append(frame) 118 | print("\n") 119 | 120 | # Make a mega-frame 121 | if frames: 122 | df = pd.concat(frames, ignore_index=True) 123 | else: 124 | df = pd.DataFrame() 125 | 126 | # Save it 127 | df.to_csv(os.path.join(out_dir, "merged_results.csv"), index=False) 128 | 129 | bad_df = None 130 | if(len(bad_frames) > 0): 131 | bad_df = pd.concat(bad_frames, ignore_index=True) 132 | 133 | # drop rows whose times are too long 134 | bad_rows = df[df['duration'] > synthetic_timeout].index 135 | df.drop(bad_rows, inplace=True) 136 | 137 | if bad_files: 138 | print(f"\n\n === bad files({len(bad_files)}) === ") 139 | print(bad_files) 140 | else: 141 | print("\n\n === all files parsed :) ") 142 | 143 | if bad_df is None: 144 | print("\n\n === no incomplete records :) ") 145 | filtered_bad_names = [] 146 | else: 147 | print("\n\n === incomplete records === ") 148 | print(bad_df) 149 | 150 | bad_names = bad_df["name"].to_list() 151 | 152 | filtered_bad_names = [int(c) if len(c) else None for c in (''.join(takewhile(str.isdigit, str(x) or "")) for x in bad_names)] 153 | 154 | # print(bad_names) 155 | # print(filtered_bad_names) 156 | with open(f"bad_meshes.txt", "w") as f: 157 | f.write("\n".join(map(str, [x[:-4] for x in bad_files]))) 158 | f.write("\n") 159 | f.write("\n".join(map(str, filtered_bad_names))) 160 | 161 | 162 | if args.record_files: 163 | with open("good_meshes.txt", "w") as f: 164 | f.write("\n".join(map(str,df["name"]))) 165 | 166 | df['relevant time'] = df['duration'] - df['cone placement time'] 167 | slow_meshes=df[(df["relevant time"] >= 1000)] 168 | with open("slow_meshes.txt", "w") as f: 169 | f.write("\n".join(map(str,slow_meshes["name"]))) 170 | 171 | noninjective_meshes=df[df["nFlippedTriangles"] + df["nZeroAreaTriangles"] > 0] 172 | with open("noninjective_meshes.txt", "w") as f: 173 | f.write("\n".join(map(str,noninjective_meshes["name"]))) 174 | 175 | 176 | """ 177 | # Filtering, etc 178 | df = df[df["minCornerAngle"] > 1e-6] 179 | 180 | # Compute some extra values 181 | df["flip_timeFrac"] = df["flip_flipTime"] / df["flip_totalTime"] 182 | 183 | # Print the table to the terminal 184 | """ 185 | print(df) 186 | print(df.columns) 187 | 188 | n_bad_df = 0 if bad_df is None else bad_df.shape[0] 189 | 190 | n_total = len(df) + n_bad_df + len(bad_files) 191 | if args.name =="Thingi10k": 192 | n_total = 32744 # number of total thingi10k meshes 193 | else: 194 | print(f"args.name: {args.name}") 195 | 196 | print(f"\tfailed to terminate successfully within the time limit on {n_bad_df+len(bad_files)} meshes") 197 | 198 | n = df.shape[0] 199 | df['injective'] = df.apply(lambda x : (x['nFlippedTriangles'] == 0) and (x['nZeroAreaTriangles']==0), axis=1) 200 | print(f"Achieved local injectivity on {df['injective'].sum()} of {n} meshes ({df['injective'].sum()/n*100.}% of successes, {df['injective'].sum()/n_total*100.}% of all meshes)") 201 | 202 | print(df[df['injective']==False]) 203 | 204 | mean_runtime = df['duration'].mean() 205 | print(f"mean runtime: {mean_runtime}") 206 | 207 | df_not_tiny = df[df['nVertices'] > 1000] 208 | mean_not_tiny_runtime = df_not_tiny['duration'].mean() 209 | print(f"mean not tiny runtime: {mean_not_tiny_runtime}") 210 | 211 | 212 | ############################################################ 213 | # Plots 214 | ############################################################ 215 | 216 | name = args.name 217 | 218 | ### Plot Time vs Mesh Size 219 | if plot_time_vs_mesh_size: 220 | scale = 2 221 | fig, ax = plt.subplots(figsize=(3.377 * scale,1.75 * scale)) 222 | 223 | ax.scatter(df["nVertices"], df["duration"], color=keenan_purple, edgecolors='none',alpha=0.5) 224 | 225 | ax.set_xlabel("number of mesh vertices") 226 | ax.set_ylabel("duration (s)") 227 | ax.set_title(f"{name} Time vs Mesh Size") 228 | 229 | fig.savefig(os.path.join(out_dir,'time.pdf')) 230 | 231 | plt.show() 232 | plt.close() 233 | 234 | if __name__ == "__main__": 235 | main() 236 | -------------------------------------------------------------------------------- /benchmark/run_benchmark.py: -------------------------------------------------------------------------------- 1 | # Helper class to run tasks in multiple processes 2 | 3 | import os 4 | import subprocess 5 | import shutil 6 | import random 7 | import argparse 8 | import sys 9 | 10 | from threading import Thread 11 | from queue import Queue 12 | 13 | 14 | # Simple task queue 15 | class ShellTaskQueue(Queue): 16 | 17 | def __init__(self, nWorkers=1, timeout=99999999999): 18 | Queue.__init__(self) 19 | self.nWorkers = nWorkers 20 | self.timeout = timeout 21 | self.task_count = 0 22 | 23 | for i in range(self.nWorkers): 24 | t = Thread(target=self.worker) 25 | t.daemon = True 26 | t.start() 27 | 28 | def add_task(self, cmd_str): 29 | self.put((self.task_count, cmd_str)) 30 | self.task_count += 1 31 | 32 | def worker(self): 33 | while True: 34 | id, cmd_str = self.get() 35 | print("running task {} / {} {}\n".format(id+1, self.task_count, cmd_str)) 36 | 37 | with subprocess.Popen(cmd_str, shell=True, stdout=open(os.devnull, 'w'), stderr=open(os.devnull, 'w'), preexec_fn=os.setsid) as process: 38 | 39 | try: 40 | output = process.communicate(timeout=self.timeout)[0] 41 | except subprocess.TimeoutExpired: 42 | print(" Timeout on {} :(".format(cmd_str)) 43 | os.killpg(process.pid, subprocess.signal.SIGINT) # send signal to the process group 44 | output = process.communicate()[0] 45 | except Exception as e: 46 | print(" EXCEPTION ON {} !!!!".format(cmd_str)) 47 | print(str(e)) 48 | 49 | 50 | self.task_done() 51 | 52 | # location of flip binaries, assumed relative to this file 53 | BIN_DIR = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "build", "bin")) 54 | 55 | def ensure_dir_exists(d): 56 | if not os.path.exists(d): 57 | os.makedirs(d) 58 | 59 | def parse_file_list(f): 60 | names = set() 61 | for line in open(f, 'r').readlines(): 62 | if line[0] == '#': continue 63 | 64 | raw_name = line.strip() 65 | name , _ = os.path.splitext(os.path.basename(raw_name)) 66 | names.add(name) 67 | 68 | return names 69 | 70 | def main(): 71 | parser = argparse.ArgumentParser() 72 | 73 | # Build arguments 74 | parser.add_argument('--dataset_dir', type=str, required=True, help='directory of mesh files to run on') 75 | parser.add_argument('--output_dir', type=str, required=True, help='where to put results') 76 | 77 | parser.add_argument('--good_list', type=str, help='file with meshes which should be used (optional, otherwise all used. ignores paths and extensions.)') 78 | parser.add_argument('--bad_list', type=str, help='file with meshes which should not be used, even if on good list (optional, otherwise none skipped)') 79 | 80 | parser.add_argument('--n_threads', type=int, default=1, help='number of threads to run on') 81 | parser.add_argument('--timeout', type=int, default=600, help='task timeout in seconds') 82 | 83 | parser.add_argument('--max_meshes', type=int, default=-1) 84 | parser.add_argument('--use_ffield_cones', action='store_true', help='use cones from a *.ffield file (must have the same base name as the mesh file and be in dataset_dir)') 85 | parser.add_argument('--save_parameterized_meshes', action='store_true', help='save parameterized meshes as well as performance statistics. Meshes are placed in output_dir/meshes/') 86 | 87 | # Parse arguments 88 | args = parser.parse_args() 89 | 90 | mesh_dir = os.path.join(args.output_dir, 'meshes') 91 | ensure_dir_exists(args.output_dir) 92 | if args.save_parameterized_meshes: 93 | ensure_dir_exists(mesh_dir) 94 | 95 | # save the arguments 96 | with open(os.path.join(args.output_dir,"run_args.txt"), 'w') as f: 97 | argstr = " ".join(sys.argv) 98 | f.write(argstr) 99 | 100 | # Deal with lists 101 | if args.good_list: 102 | good_set = parse_file_list(args.good_list) 103 | if args.bad_list: 104 | bad_set = parse_file_list(args.bad_list) 105 | 106 | # Load the list of meshes 107 | meshes = [] 108 | for f in os.listdir(args.dataset_dir): 109 | 110 | # respect lists 111 | f_name , f_ext = os.path.splitext(os.path.basename(f)) 112 | if f_ext not in [".obj", ".stl", ".ply", ".off"]: 113 | continue 114 | 115 | if args.good_list and f_name not in good_set: 116 | continue 117 | if args.bad_list and f_name in bad_set: 118 | continue 119 | 120 | meshes.append(os.path.join(args.dataset_dir, f)) 121 | if args.max_meshes > 0 and len(meshes) >= args.max_meshes: 122 | break 123 | 124 | print("Found {} input mesh files".format(len(meshes))) 125 | # random.shuffle(meshes) 126 | task_queue = ShellTaskQueue(nWorkers=args.n_threads, timeout=args.timeout) 127 | 128 | for m_path in meshes: 129 | m_basename = os.path.basename(m_path) 130 | m_base, m_ext = os.path.splitext(m_basename) 131 | 132 | if m_ext not in [".obj", ".stl", ".ply", ".off"]: 133 | continue 134 | 135 | output_path = os.path.join(args.output_dir, m_base + ".tsv") 136 | 137 | ffield_path = os.path.join(os.path.splitext(m_path)[0] + ".ffield") 138 | ffield_option = f"--ffield={ffield_path}" if args.use_ffield_cones else "" 139 | 140 | output_mesh_path = os.path.join(mesh_dir, m_base + ".obj") 141 | mesh_save_option = f"--outputMeshFilename={output_mesh_path}" if args.save_parameterized_meshes else "" 142 | 143 | cmd_list = [ 144 | os.path.join(BIN_DIR,"parameterize"), 145 | m_path, 146 | ffield_option, 147 | mesh_save_option, 148 | f"--outputLogFilename={output_path}" 149 | ] 150 | 151 | # build the command 152 | cmd_str = " ".join(cmd_list) 153 | 154 | task_queue.add_task(cmd_str) 155 | 156 | task_queue.join() 157 | 158 | 159 | if __name__ == "__main__": 160 | main() 161 | -------------------------------------------------------------------------------- /media/blender_file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarkGillespie/CEPS/b734d2cdc2afad7dc6afe9d609a108bc9a68c24f/media/blender_file.png -------------------------------------------------------------------------------- /media/nodes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarkGillespie/CEPS/b734d2cdc2afad7dc6afe9d609a108bc9a68c24f/media/nodes.png -------------------------------------------------------------------------------- /media/parameterize.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarkGillespie/CEPS/b734d2cdc2afad7dc6afe9d609a108bc9a68c24f/media/parameterize.png -------------------------------------------------------------------------------- /media/spherical_uniformize.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarkGillespie/CEPS/b734d2cdc2afad7dc6afe9d609a108bc9a68c24f/media/spherical_uniformize.png -------------------------------------------------------------------------------- /media/teaser.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarkGillespie/CEPS/b734d2cdc2afad7dc6afe9d609a108bc9a68c24f/media/teaser.png -------------------------------------------------------------------------------- /render/ProjectiveInterpolation.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarkGillespie/CEPS/b734d2cdc2afad7dc6afe9d609a108bc9a68c24f/render/ProjectiveInterpolation.blend -------------------------------------------------------------------------------- /render/SphericalProjectiveInterpolation.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarkGillespie/CEPS/b734d2cdc2afad7dc6afe9d609a108bc9a68c24f/render/SphericalProjectiveInterpolation.blend -------------------------------------------------------------------------------- /render/equirect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarkGillespie/CEPS/b734d2cdc2afad7dc6afe9d609a108bc9a68c24f/render/equirect.png -------------------------------------------------------------------------------- /src/.clang-format: -------------------------------------------------------------------------------- 1 | Language: Cpp 2 | AlignConsecutiveAssignments: true 3 | PointerAlignment: Left 4 | AlignAfterOpenBracket: Align 5 | AlignOperands: 'true' 6 | AllowShortBlocksOnASingleLine: 'false' 7 | AllowShortIfStatementsOnASingleLine: 'true' 8 | AllowShortLoopsOnASingleLine: 'true' 9 | AlwaysBreakTemplateDeclarations: 'true' 10 | BinPackParameters: 'true' 11 | BreakBeforeBraces: Attach 12 | ColumnLimit: '80' 13 | Cpp11BracedListStyle: 'true' 14 | IndentWidth: '4' 15 | KeepEmptyLinesAtTheStartOfBlocks: 'true' 16 | MaxEmptyLinesToKeep: '2' 17 | PointerAlignment: Left 18 | ReflowComments: 'true' 19 | SpacesInAngles: 'false' 20 | SpacesInParentheses: 'false' 21 | SpacesInSquareBrackets: 'false' 22 | Standard: Cpp11 23 | UseTab: Never 24 | 25 | ... 26 | -------------------------------------------------------------------------------- /src/.clang-tidy: -------------------------------------------------------------------------------- 1 | Checks: '-*,clang-diagnostic-*,llvm-*,misc-*,-misc-unused-parameters,-misc-non-private-member-variables-in-classes,readability-identifier-naming' 2 | CheckOptions: 3 | - key: readability-identifier-naming.ClassCase 4 | value: CamelCase 5 | - key: readability-identifier-naming.EnumCase 6 | value: CamelCase 7 | - key: readability-identifier-naming.FunctionCase 8 | value: camelBack 9 | - key: readability-identifier-naming.MemberCase 10 | value: camelBack 11 | - key: readability-identifier-naming.ParameterCase 12 | value: camelBack 13 | - key: readability-identifier-naming.UnionCase 14 | value: CamelCase 15 | - key: readability-identifier-naming.VariableCase 16 | value: camelBack -------------------------------------------------------------------------------- /src/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 2.8.9) 2 | 3 | # Maybe stop from CMAKEing in the wrong place 4 | if (CMAKE_BINARY_DIR STREQUAL CMAKE_SOURCE_DIR) 5 | message(FATAL_ERROR "Source and build directories cannot be the same. Go use the /build directory.") 6 | endif() 7 | 8 | 9 | add_library( 10 | CEPS 11 | Triangulation.cpp 12 | Optimizer.cpp 13 | Tracing.cpp 14 | FlipFormulas.cpp 15 | Layout.cpp 16 | CommonRefinement.cpp 17 | Utils.cpp 18 | GeometryUtils.cpp 19 | Cutter.cpp 20 | ConePlacement.cpp 21 | ConeFlattening.cpp 22 | IO.cpp 23 | Clausen.cpp 24 | Logger.cpp 25 | polyscope/SurfaceProjectiveParameterizationQuantity.cpp 26 | ../deps/predicates/predicates.c 27 | ) 28 | target_link_libraries(CEPS polyscope geometry-central) 29 | 30 | add_executable( 31 | parameterize 32 | Parameterize.cpp 33 | ) 34 | target_include_directories(parameterize PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}") 35 | # borrow args.hxx directly from polyscope 36 | target_include_directories(parameterize PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/../deps/polyscope/deps/args/") 37 | target_link_libraries(parameterize polyscope geometry-central CEPS) 38 | 39 | option(SPHERICAL_UNIFORMIZATION "Build spherical uniformizer" OFF) 40 | option(PACKAGE_TESTS "Build the cone flattening tests" OFF) 41 | 42 | message("Spherical uniformization: ${SPHERICAL_UNIFORMIZATION}") 43 | message("Tests: ${PACKAGE_TESTS}") 44 | 45 | if(SPHERICAL_UNIFORMIZATION) 46 | # PETSC_DIR=/home/mark/Documents/Code/petsc PETSC_ARCH=arch-linux2-c-opt all 47 | add_subdirectory(SphericalUniformization) 48 | endif() 49 | 50 | if(PACKAGE_TESTS) 51 | enable_testing() 52 | add_subdirectory( 53 | tests 54 | ) 55 | endif() 56 | -------------------------------------------------------------------------------- /src/Clausen.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * $Id: Clausen.cpp,v 1.2 2008/07/16 07:05:09 ps Exp $ 3 | * $Source: /cs/research/multires/cvsroot/src/ConfEquiv/Clausen.cpp,v $ 4 | */ 5 | #include "Clausen.h" 6 | #include 7 | 8 | /* 9 | * C version of r1mach and d1mach from core (netlib) 10 | * specialized for IEEE arithmetic 11 | * 12 | * MACHINE CONSTANTS (s for single, d for double) 13 | * {S|D}1MACH(1) = B**(EMIN-1), THE SMALLEST POSITIVE MAGNITUDE. 14 | * {S|D}1MACH(2) = B**EMAX*(1 - B**(-T)), THE LARGEST MAGNITUDE. 15 | * {S|D}1MACH(3) = B**(-T), THE SMALLEST RELATIVE SPACING. 16 | * {S|D}1MACH(4) = B**(1-T), THE LARGEST RELATIVE SPACING. 17 | * {S|D}1MACH(5) = LOG10(B) 18 | */ 19 | static double mach[5] = {2.2250738585072014e-308, 1.7976931348623157e+308, 20 | 1.1102230246251565e-16, 2.2204460492503131e-16, 21 | 3.0102999566398120e-01}; 22 | 23 | /* 24 | * chebyshev expansion of `cl(t)/t + log(t)' around Pi/6 25 | * accurate to 20 decimal places 26 | */ 27 | static double clpi6[14] = { 28 | 2 * 1.0057346496467363858, .0076523796971586786263, 29 | .0019223823523180480014, .53333368801173950429e-5, 30 | .68684944849366102659e-6, .63769755654413855855e-8, 31 | .57069363812137970721e-9, .87936343137236194448e-11, 32 | .62365831120408524691e-12, .12996625954032513221e-13, 33 | .78762044080566097484e-15, .20080243561666612900e-16, 34 | .10916495826127475499e-17, .32027217200949691956e-19}; 35 | 36 | /* 37 | * chebyshev expansion of cl(t/2)/t + log(t)' around Pi/2 38 | * accurate to 20 decimal places 39 | */ 40 | static double clpi2[19] = { 41 | 2 * .017492908851746863924 + 2 * 1.0057346496467363858, 42 | .023421240075284860656 + .0076523796971586786263, 43 | .0060025281630108248332 + .0019223823523180480014, 44 | .000085934211448718844330 + .53333368801173950429e-5, 45 | .000012155033501044820317 + .68684944849366102659e-6, 46 | .46587486310623464413e-6 + .63769755654413855855e-8, 47 | .50732554559130493329e-7 + .57069363812137970721e-9, 48 | .28794458754760053792e-8 + .87936343137236194448e-11, 49 | .27792370776596244150e-9 + .62365831120408524691e-12, 50 | .19340423475636663004e-10 + .12996625954032513221e-13, 51 | .17726134256574610202e-11 + .78762044080566097484e-15, 52 | .13811355237660945692e-12 + .20080243561666612900e-16, 53 | .12433074161771699487e-13 + .10916495826127475499e-17, 54 | .10342683357723940535e-14 + .32027217200949691956e-19, 55 | .92910354101990447850e-16, 56 | .80428334724548559541e-17, 57 | .72598441354406482972e-18, 58 | .64475701884829384587e-19, 59 | .58630185185185185187e-20}; 60 | 61 | /* 62 | * chebyshev expansion of `-cl(Pi-t)/(Pi-t) + log(2)' around 5Pi/6 63 | * accurate to 20 decimal places 64 | */ 65 | static double cl5pi6[19] = { 66 | 2 * .017492908851746863924, .023421240075284860656, 67 | .0060025281630108248332, .000085934211448718844330, 68 | .000012155033501044820317, .46587486310623464413e-6, 69 | .50732554559130493329e-7, .28794458754760053792e-8, 70 | .27792370776596244150e-9, .19340423475636663004e-10, 71 | .17726134256574610202e-11, .13811355237660945692e-12, 72 | .12433074161771699487e-13, .10342683357723940535e-14, 73 | .92910354101990447850e-16, .80428334724548559541e-17, 74 | .72598441354406482972e-18, .64475701884829384587e-19, 75 | .58630185185185185187e-20}; 76 | 77 | /* 78 | * evaluate a chebyshev series 79 | * adapted from fortran csevl 80 | */ 81 | double csevl(double x, double* cs, int n) { 82 | double b2, b1 = 0, b0 = 0, twox = 2 * x; 83 | 84 | while (n--) { 85 | b2 = b1; 86 | b1 = b0; 87 | b0 = twox * b1 - b2 + cs[n]; 88 | } 89 | 90 | return (b0 - b2) / 2; 91 | } 92 | 93 | /* 94 | * from the original fortran inits 95 | * april 1977 version. w. fullerton, c3, los alamos scientific lab. 96 | * 97 | * initialize the orthogonal series so that inits is the number of terms 98 | * needed to insure the error is no larger than eta. ordinarily, eta 99 | * will be chosen to be one-tenth machine precision. 100 | */ 101 | static int inits(double* series, int n, double eta) { 102 | double err = 0; 103 | 104 | while (err <= eta && n--) { 105 | err += fabs(series[n]); 106 | } 107 | 108 | return n++; 109 | } 110 | 111 | double clausen(double x) { 112 | static int nclpi6 = 0, nclpi2 = 0, ncl5pi6 = 0; 113 | /* 114 | * right half (Pi <= x < 2 Pi) 115 | */ 116 | int rh = 0; 117 | double f; 118 | 119 | if (!nclpi6) { 120 | nclpi6 = inits(clpi6, sizeof clpi6 / sizeof *clpi6, mach[2] / 10); 121 | nclpi2 = inits(clpi2, sizeof clpi2 / sizeof *clpi2, mach[2] / 10); 122 | ncl5pi6 = inits(cl5pi6, sizeof cl5pi6 / sizeof *cl5pi6, mach[2] / 10); 123 | } 124 | 125 | /* 126 | * get to canonical interval 127 | */ 128 | if ((x = fmod(x, 2 * M_PI)) < 0) { 129 | x += (double)(2 * M_PI); 130 | } 131 | if (x > (double)M_PI) { 132 | rh = 1; 133 | x = (double)(2 * M_PI) - x; 134 | } 135 | 136 | if (x == 0) { 137 | f = x; 138 | } else if (x <= (double)(M_PI / 3)) { 139 | f = csevl(x * (double)(6 / M_PI) - 1, clpi6, nclpi6) * x - x * log(x); 140 | } else if (x <= (double)(2 * M_PI / 3)) { 141 | f = csevl(x * (double)(3 / M_PI) - 1, clpi2, nclpi2) * x - x * log(x); 142 | } else { /* x <= Pi */ 143 | f = ((double)M_LN2 - 144 | csevl(5 - x * (double)(6 / M_PI), cl5pi6, ncl5pi6)) * 145 | ((double)M_PI - x); 146 | } 147 | 148 | return rh ? -f : f; 149 | } 150 | -------------------------------------------------------------------------------- /src/Clausen.h: -------------------------------------------------------------------------------- 1 | /* 2 | * $Id: Clausen.h,v 1.2 2008/07/16 07:05:09 ps Exp $ 3 | * $Source: /cs/research/multires/cvsroot/src/ConfEquiv/Clausen.h,v $ 4 | */ 5 | #ifndef CLAUSEN_H 6 | #define CLAUSEN_H 7 | 8 | extern double clausen(double x); 9 | 10 | #endif /* CLAUSEN_H */ 11 | -------------------------------------------------------------------------------- /src/CommonRefinement.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "geometrycentral/surface/simple_polygon_mesh.h" 4 | #include "geometrycentral/utilities/utilities.h" // clamp 5 | 6 | #include 7 | 8 | #include "DirectSum.h" 9 | #include "Tracing.h" 10 | #include "Triangulation.h" 11 | #include "Utils.h" 12 | 13 | namespace CEPS { 14 | 15 | // Returns the common refinement, interpolated projective texture coordinates, 16 | // and a map between vertices of Tc and vertex indices in the common refinement. 17 | // Vertices of Ta which don't appear in the common refinement are assigned index 18 | // -1 (This happens, e.g. when Ta has been doubled, in which case we only 19 | // include the front vertices in the common refinement) 20 | std::tuple, VertexData> 21 | computeCommonRefinement(Triangulation& Ta, Triangulation& Tb, Triangulation& Tc, 22 | const VertexData& vertexPositions, 23 | const CornerData& uv, 24 | const std::set& frontFaces, bool verbose = false); 25 | 26 | // Computes the common refinement, projective texture coordinates, vertex map, 27 | // and an interpolation matrix mapping values on Ta to the common refinement 28 | // (the interpolation matrix has dimensions |V_{common refinement}| x |V_A|) 29 | std::tuple, VertexData, 30 | SparseMatrix> 31 | computeCommonRefinementAndMatrix(Triangulation& Ta, Triangulation& Tb, 32 | Triangulation& Tc, 33 | const VertexData& vertexPositions, 34 | const CornerData& uv, 35 | const std::set& frontFaces, 36 | bool verbose = false); 37 | 38 | namespace ImplementationDetails { 39 | 40 | // A doublet represents a sparse vector entry, in the same way that a triplet 41 | // represents a sparse matrix entry 42 | // Note: this class avoids any constructors so that it is a POD type 43 | struct Doublet { 44 | size_t col; 45 | double val; 46 | bool operator==(const Doublet& t) const; 47 | bool operator!=(const Doublet& t) const; 48 | }; 49 | 50 | // Scalar multiplication 51 | Doublet operator*(const double s, const Doublet& t); 52 | 53 | // Printing 54 | std::ostream& operator<<(std::ostream& str, const Doublet& a); 55 | 56 | class SparseVector { 57 | public: 58 | SparseVector(); 59 | std::vector doublets; 60 | 61 | SparseVector operator+(const SparseVector& tl) const; 62 | bool operator==(const SparseVector& tl) const; 63 | }; 64 | 65 | // Scalar multiplication 66 | SparseVector operator*(const double s, const SparseVector& tl); 67 | SparseVector operator*(const SparseVector& tl, const double s); 68 | 69 | // Printing 70 | std::ostream& operator<<(std::ostream& str, const SparseVector& a); 71 | 72 | template 73 | std::tuple, std::vector, VertexData> 74 | computeCommonRefinement(Triangulation& Ta, Triangulation& Tb, Triangulation& Tc, 75 | const VertexData& initialData, 76 | const CornerData& finalData, 77 | const std::vector& frontFaces, 78 | bool verbose = false); 79 | 80 | // ======================================================================== 81 | // Helpers for SliceTri 82 | // ======================================================================== 83 | 84 | using Line = std::array; 85 | using Segment = std::array; 86 | 87 | enum class LineType { A, B, Boundary }; 88 | inline std::ostream& operator<<(std::ostream& str, const LineType& lt) { 89 | switch (lt) { 90 | case LineType::A: 91 | str << "Type A"; 92 | break; 93 | case LineType::B: 94 | str << "Type B"; 95 | break; 96 | case LineType::Boundary: 97 | str << "Boundary"; 98 | break; 99 | default: 100 | str << "Unrecognized LineType"; 101 | break; 102 | } 103 | 104 | return str; 105 | } 106 | 107 | size_t next(size_t i); 108 | size_t prev(size_t i); 109 | 110 | // If there is a fan edge, return its index. Otherwise, return 3 111 | // Only returns 'strict' fan edges (i.e. there is an edge to the opposite 112 | // vertex) 113 | size_t getFanEdge(const std::array& edgeLengths); 114 | 115 | // If there is a fan edge, return its index. Otherwise, return 3 116 | // Only returns 'strict' fan edges (i.e. there is an edge to the opposite 117 | // vertex) 118 | size_t getFanEdge(const std::array, 3>& edgePts); 119 | 120 | // Important guarantee: if there is a fan edge, lines from the opposite vertex 121 | // to that fan edge will always be oriented away from the vertex and towards the 122 | // fan edge. Also, these lines will be in order in the output list, i.e. lines 123 | // that end at points with earlier barycentric coordinates in the input list 124 | // come first 125 | std::vector getLines(const std::array, 3>& edgePts); 126 | 127 | // Computes the intersection of the line a with line b 128 | // Returns false if they don't intersect. If they do, returns true and sets 129 | // intersection to be their intersection; 130 | bool intersect(Line a, Line b, const std::vector& position, double& tA, 131 | double& tB); 132 | 133 | // Computes the intersection of the line a with line b. Assumes they intersect 134 | // and sets intersection to be their intersection; 135 | void unsafeIntersect(Line a, Line b, const std::vector& position, 136 | double& tA, double& tB); 137 | 138 | void splitIntoSegments( 139 | const std::vector& lines, 140 | const std::vector>& lineVertices, 141 | const std::vector& vertices, const LineType& segType, 142 | std::vector>& vertexSegments, 143 | std::vector>& vertexSegmentDirections, 144 | std::vector& segments, std::vector& segmentTypes, 145 | bool ordered = false); 146 | 147 | 148 | // Reads off the crossings along the boundary of face f 149 | // Assumes that the MeshPaths always intersect halfedges positively 150 | template 151 | std::tuple, 3>, 152 | std::array>, 3>, std::vector> 153 | readEdgeCrossings(Face f, const EdgeData& paths, 154 | const CornerData& inputData, bool projectiveInterpolate, 155 | const VertexData& logScaleFactors, 156 | bool verbose = false); 157 | 158 | // Lists of barycentric coordinates must start at 0, end at 1, and be sorted 159 | // 160 | // At fan edges, you need to set the left value of the vertex to be 161 | // the value at the leftmost corner incident on the vertex, and the right 162 | // value to be the value from the rightmost corner. Otherwise boundary 163 | // interpolation fails 164 | 165 | // Returns: the number of vertices in the refined triangle, the face-vertex 166 | // adjacency list of the triangle, and the interpolated values of val1 and val2 167 | // within each face 168 | template 169 | std::tuple>, 170 | std::vector>, std::vector>> 171 | sliceTri(std::array, 3> bary1, 172 | std::array, 3> bary2, 173 | const std::array>, 3>& val1, 174 | const std::array>, 3>& val2, 175 | const std::vector& longSideCornerVal1, 176 | const std::vector& longSideCornerVal2, bool verbose = false); 177 | 178 | 179 | } // namespace ImplementationDetails 180 | } // namespace CEPS 181 | 182 | #include "CommonRefinement.ipp" 183 | -------------------------------------------------------------------------------- /src/ConeFlattening.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "geometrycentral/surface/manifold_surface_mesh.h" 4 | #include "geometrycentral/surface/meshio.h" 5 | #include "geometrycentral/surface/simple_polygon_mesh.h" 6 | #include "geometrycentral/surface/vertex_position_geometry.h" 7 | 8 | #include "polyscope/SurfaceProjectiveParameterizationQuantity.h" 9 | #include "polyscope/surface_mesh.h" 10 | 11 | #include "CommonRefinement.h" 12 | #include "ConePlacement.h" 13 | #include "Layout.h" 14 | #include "Optimizer.h" 15 | #include "Tracing.h" 16 | #include "Triangulation.h" 17 | 18 | namespace CEPS { 19 | 20 | // Map is stored via projective coordinates at mesh corners 21 | struct ParameterizationResult { 22 | SimplePolygonMesh mesh; 23 | std::vector param; 24 | VertexData parentMap; 25 | SparseMatrix interpolationMatrix; 26 | size_t nFlippedTriangles; 27 | size_t nZeroAreaTriangles; 28 | }; 29 | 30 | // Place cones using the greedy algorithm from ConePlacement.cpp 31 | // Then uniformize, setting u=0 at cone vertices and boundary vertices. 32 | // If viz is true, register the resulting textured mesh with polyscope. 33 | // If checkInjectivity is true, check the intrinsically-flattened mesh for 34 | // flipped triangles and report the results. 35 | // uTol is the maximum allowed area distortion in the greedy cone placement 36 | ParameterizationResult parameterizeWithGreedyCones( 37 | ManifoldSurfaceMesh& mesh, VertexPositionGeometry& geo, bool viz = false, 38 | bool checkInjectivity = false, double uTol = 5, bool verbose = false); 39 | 40 | // Uniformize, setting u=0 at given cone vertices and any additional 41 | // boundary vertices. 42 | // If viz is true, register the resulting textured mesh with polyscope. 43 | // If checkInjectivity is true, check the intrinsically-flattened mesh for 44 | // flipped triangles and report the results. 45 | ParameterizationResult parameterizeWithGivenCones( 46 | ManifoldSurfaceMesh& mesh, VertexPositionGeometry& geo, 47 | const std::vector& coneVertices, bool viz = false, 48 | bool checkInjectivity = false, bool verbose = false); 49 | 50 | // Uniformize, imposing the prescribed scale factors and curvatures. 51 | // A vertex may not have both a prescribed scale factor and a prescribed 52 | // curvature. 53 | // Faces indicated in the imaginaryFaceIndices list will be removed from the 54 | // final mesh (useful, e.g. for removing filled boundary loops). 55 | // If viz is true, register the resulting textured mesh with polyscope. 56 | // If checkInjectivity is true, check the intrinsically-flattened mesh for 57 | // flipped triangles and report the results. 58 | ParameterizationResult 59 | parameterize(ManifoldSurfaceMesh& mesh, VertexPositionGeometry& geo, 60 | std::vector> prescribedScaleFactors, 61 | std::vector> prescribedCurvatures, 62 | std::vector imaginaryFaceIndices, bool viz = false, 63 | bool checkInjectivity = false, bool verbose = false); 64 | 65 | // The core uniformization algorithm. 66 | // The input mesh must have no boundary. 67 | // Uniformizes the input mesh while imposing the prescribed scale factors and 68 | // curvatures. 69 | // All faces except for those in frontFaceIndices are deleted from the final 70 | // mesh. 71 | ParameterizationResult parameterizeHelper( 72 | ManifoldSurfaceMesh& mesh, VertexPositionGeometry& geo, 73 | std::vector> prescribedScaleFactors, 74 | std::vector> prescribedCurvatures, 75 | std::set frontFaceIndices, bool viz = false, 76 | bool checkInjectivity = false, bool verbose = false); 77 | 78 | } // namespace CEPS 79 | -------------------------------------------------------------------------------- /src/ConePlacement.cpp: -------------------------------------------------------------------------------- 1 | #include "ConePlacement.h" 2 | 3 | namespace CEPS { 4 | std::vector placeCones(ManifoldSurfaceMesh& mesh, 5 | IntrinsicGeometryInterface& geo, double uTol, 6 | size_t minNumCones, size_t gaussSeidelIterations, 7 | bool verbose) { 8 | 9 | // We only place cones on interior vertices. If there aren't any, don't 10 | // place any cones 11 | if (mesh.nInteriorVertices() == 0) return {}; 12 | 13 | // Using the ordinary cotan Laplacian leads to numerical issues on 14 | // near-degenerate models (e.g. thingi10k 424212_7.ply) 15 | SparseMatrix L = 16 | ImplementationDetails::intrinsicDelaunayLaplacian(mesh, geo); 17 | 18 | 19 | // Also regularize slightly 20 | Eigen::SparseMatrix eye(mesh.nVertices(), mesh.nVertices()); 21 | eye.setIdentity(); 22 | 23 | L = L + 1e-6 * eye; 24 | 25 | auto getWorstVertex = [&](const VertexData& vec) -> Vertex { 26 | Vertex worstSoFar = mesh.vertex(0); 27 | for (Vertex v : mesh.vertices()) { 28 | if (abs(vec[v]) > abs(vec[worstSoFar])) worstSoFar = v; 29 | } 30 | return worstSoFar; 31 | }; 32 | 33 | if (verbose) std::cout << "\t computing initial greedy cones" << std::endl; 34 | 35 | // Place first cone by approximately uniformizing to a constant curvature 36 | // metric and picking the vertex with worst scale factor (We would flatten, 37 | // except you can't necessarily flatten with one cone) 38 | VertexData scaleFactors = 39 | ImplementationDetails::computeConstantCurvatureScaleFactors(mesh, geo, 40 | L); 41 | Vertex worstVertex = getWorstVertex(scaleFactors); 42 | 43 | std::vector cones; 44 | cones.push_back(worstVertex); 45 | 46 | // Place cones by approximately flattening and picking the vertex 47 | // with worst scale factor. Keep doing this until you have at least 48 | // minNumCones cones, and the worst scale factor is at most uTol 49 | while (cones.size() < minNumCones || 50 | abs(scaleFactors[worstVertex]) > uTol) { 51 | scaleFactors = 52 | ImplementationDetails::computeScaleFactors(mesh, geo, L, cones); 53 | worstVertex = getWorstVertex(scaleFactors); 54 | cones.push_back(worstVertex); 55 | } 56 | 57 | if (verbose) std::cout << "\t improving cones" << std::endl; 58 | 59 | // Improve cones by removing each cone and replacing it by the vertex with 60 | // the biggest scale factor 61 | for (size_t iGS = 0; iGS < gaussSeidelIterations; ++iGS) { 62 | for (size_t iCone = 0; iCone < cones.size(); ++iCone) { 63 | VertexData scaleFactors = 64 | ImplementationDetails::computeScaleFactors(mesh, geo, L, cones, 65 | iCone); 66 | cones[iCone] = getWorstVertex(scaleFactors); 67 | } 68 | } 69 | 70 | return cones; 71 | } 72 | 73 | namespace ImplementationDetails { 74 | 75 | // Compute scale factors which give the mesh constant curvature 76 | VertexData 77 | computeConstantCurvatureScaleFactors(ManifoldSurfaceMesh& mesh, 78 | IntrinsicGeometryInterface& geo, 79 | const SparseMatrix& L) { 80 | geo.requireCotanLaplacian(); 81 | geo.requireVertexGaussianCurvatures(); 82 | 83 | Vector weightedCurvature = -geo.vertexGaussianCurvatures.toVector(); 84 | 85 | double totalCurvature = weightedCurvature.sum(); 86 | double avgCurvature = totalCurvature / (double)mesh.nVertices(); 87 | 88 | Vector constantCurvature = 89 | Vector::Constant(mesh.nVertices(), avgCurvature); 90 | 91 | Vector curvatureDifference = weightedCurvature - constantCurvature; 92 | 93 | 94 | // Identify interior vertices 95 | VertexData vIdx = mesh.getVertexIndices(); 96 | Vector isInterior = Vector::Constant(mesh.nVertices(), true); 97 | 98 | size_t nB = 0; 99 | for (BoundaryLoop b : mesh.boundaryLoops()) { 100 | for (Vertex v : b.adjacentVertices()) { 101 | isInterior[vIdx[v]] = false; 102 | nB++; 103 | } 104 | } 105 | 106 | 107 | // Solve Lu = curvatureDifference w/ zero Dirichlet boundary conditions 108 | BlockDecompositionResult laplacianBlocks = 109 | blockDecomposeSquare(L, isInterior, true); 110 | 111 | Vector boundaryCurvature, interiorCurvature; 112 | decomposeVector(laplacianBlocks, weightedCurvature, interiorCurvature, 113 | boundaryCurvature); 114 | 115 | Vector interiorResult = 116 | solvePositiveDefinite(laplacianBlocks.AA, interiorCurvature); 117 | Vector zero = Eigen::VectorXd::Zero(nB); 118 | Vector totalResult = 119 | reassembleVector(laplacianBlocks, interiorResult, zero); 120 | 121 | return VertexData(mesh, totalResult); 122 | } 123 | 124 | // Compute scale factors which flatten mesh using the given cones. 125 | // If vSkip1 or vSkip2 are nonzero, ignore the corresponding cones (i.e. flatten 126 | // those vertices too) 127 | VertexData computeScaleFactors(ManifoldSurfaceMesh& mesh, 128 | IntrinsicGeometryInterface& geo, 129 | const SparseMatrix& L, 130 | const std::vector cones, 131 | int vSkip1, int vSkip2) { 132 | geo.requireCotanLaplacian(); 133 | geo.requireVertexGaussianCurvatures(); 134 | 135 | // interior vertices are the ones that aren't cones 136 | VertexData vIdx = mesh.getVertexIndices(); 137 | Vector isInterior = Vector::Constant(mesh.nVertices(), true); 138 | 139 | size_t nCones = 0; 140 | for (size_t iV = 0; iV < cones.size(); ++iV) { 141 | if ((int)iV != vSkip1 && (int)iV != vSkip2) { 142 | if (isInterior[vIdx[cones[iV]]]) nCones++; 143 | isInterior[vIdx[cones[iV]]] = false; 144 | } 145 | } 146 | // If the mesh has boundary, make every boundary vertex a cone 147 | for (BoundaryLoop b : mesh.boundaryLoops()) { 148 | for (Vertex v : b.adjacentVertices()) { 149 | isInterior[vIdx[v]] = false; 150 | nCones++; 151 | } 152 | } 153 | 154 | BlockDecompositionResult laplacianBlocks = 155 | blockDecomposeSquare(L, isInterior, true); 156 | 157 | Vector weightedCurvature = -geo.vertexGaussianCurvatures.toVector(); 158 | 159 | Vector coneCurvature, interiorCurvature; 160 | decomposeVector(laplacianBlocks, weightedCurvature, interiorCurvature, 161 | coneCurvature); 162 | 163 | Vector interiorResult = 164 | solvePositiveDefinite(laplacianBlocks.AA, interiorCurvature); 165 | Vector zero = Eigen::VectorXd::Zero(nCones); 166 | Vector totalResult = 167 | reassembleVector(laplacianBlocks, interiorResult, zero); 168 | 169 | return VertexData(mesh, totalResult); 170 | } 171 | 172 | SparseMatrix intrinsicDelaunayLaplacian(ManifoldSurfaceMesh& mesh, 173 | IntrinsicGeometryInterface& geo, 174 | double mollify) { 175 | std::unique_ptr meshCpy = mesh.copy(); 176 | 177 | geo.requireEdgeLengths(); 178 | EdgeData eLen = geo.edgeLengths.reinterpretTo(*meshCpy); 179 | geo.unrequireEdgeLengths(); 180 | 181 | EdgeLengthGeometry eGeo(*meshCpy, eLen); 182 | 183 | mollifyIntrinsic(*meshCpy, eGeo.inputEdgeLengths, mollify); 184 | flipToDelaunay(*meshCpy, eGeo.inputEdgeLengths); 185 | 186 | eGeo.requireHalfedgeCotanWeights(); 187 | for (Halfedge he : eGeo.mesh.interiorHalfedges()) { 188 | if (!std::isfinite(eGeo.halfedgeCotanWeights[he])) { 189 | std::cout << "Bad cotan weight: " << eGeo.halfedgeCotanWeights[he] 190 | << vendl; 191 | std::array edgeLengths{ 192 | eGeo.inputEdgeLengths[he.edge()], 193 | eGeo.inputEdgeLengths[he.next().edge()], 194 | eGeo.inputEdgeLengths[he.next().next().edge()]}; 195 | std::cout << "\tedge lengths:"; 196 | for (double l : edgeLengths) { 197 | std::cout << " " << l; 198 | } 199 | std::cout << vendl; 200 | } 201 | } 202 | 203 | 204 | eGeo.requireCotanLaplacian(); 205 | 206 | return eGeo.cotanLaplacian; 207 | } 208 | 209 | } // namespace ImplementationDetails 210 | } // namespace CEPS 211 | -------------------------------------------------------------------------------- /src/ConePlacement.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "geometrycentral/numerical/linear_algebra_utilities.h" 4 | #include "geometrycentral/numerical/linear_solvers.h" 5 | #include "geometrycentral/surface/intrinsic_geometry_interface.h" 6 | #include "geometrycentral/surface/intrinsic_mollification.h" 7 | #include "geometrycentral/surface/manifold_surface_mesh.h" 8 | #include "geometrycentral/surface/simple_idt.h" 9 | 10 | #include "Utils.h" 11 | 12 | using namespace geometrycentral; 13 | using namespace geometrycentral::surface; 14 | 15 | namespace CEPS { 16 | 17 | // Place cones iteratively & greedily at the vertices with the most scale 18 | // distortion. Then run a few nonlinear Gauss-Seidel iterations to improve 19 | // placement. Works best on a connected mesh. 20 | std::vector placeCones(ManifoldSurfaceMesh& mesh, 21 | IntrinsicGeometryInterface& geo, double uTol = 5, 22 | size_t minNumCones = 4, 23 | size_t gaussSeidelIterations = 4, 24 | bool verbose = false); 25 | 26 | namespace ImplementationDetails { 27 | 28 | // Compute scale factors which give the mesh constant curvature 29 | VertexData 30 | computeConstantCurvatureScaleFactors(ManifoldSurfaceMesh& mesh, 31 | IntrinsicGeometryInterface& geo, 32 | const SparseMatrix& L); 33 | 34 | // Compute scale factors which flatten the mesh except at the given cone 35 | // vertices If vSkip1 or vSkip2 are set, we flatten the mesh at those cones as 36 | // well. 37 | VertexData computeScaleFactors(ManifoldSurfaceMesh& mesh, 38 | IntrinsicGeometryInterface& geo, 39 | const SparseMatrix& L, 40 | const std::vector cones, 41 | int vSkip1 = -1, int vSkip2 = -1); 42 | 43 | 44 | SparseMatrix intrinsicDelaunayLaplacian(ManifoldSurfaceMesh& mesh, 45 | IntrinsicGeometryInterface& geo, 46 | double mollify = 1e-6); 47 | } // namespace ImplementationDetails 48 | } // namespace CEPS 49 | -------------------------------------------------------------------------------- /src/Cutter.h: -------------------------------------------------------------------------------- 1 | 2 | #pragma once 3 | 4 | #include "geometrycentral/surface/manifold_surface_mesh.h" 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include "Utils.h" 13 | 14 | using namespace geometrycentral; 15 | using namespace geometrycentral::surface; 16 | 17 | namespace CEPS { 18 | namespace ImplementationDetails { 19 | 20 | // ========================================================================== 21 | // Compute Mesh Cuts 22 | // ========================================================================== 23 | 24 | std::set goodCuts(ManifoldSurfaceMesh& mesh, 25 | const std::vector& cones, 26 | const EdgeData& edgeCost); 27 | 28 | std::set goodCuts(ManifoldSurfaceMesh& mesh, 29 | const std::vector& cones, 30 | const EdgeData& edgeCost, 31 | std::set startingCut); 32 | 33 | std::set bestCuts(ManifoldSurfaceMesh& mesh, 34 | const std::vector& cones, 35 | const EdgeData& edgeCost); 36 | 37 | std::set bestCuts(ManifoldSurfaceMesh& mesh, 38 | const std::vector& cones, 39 | const EdgeData& edgeCost, 40 | std::set startingCut); 41 | 42 | std::set cutOutFront(ManifoldSurfaceMesh& mesh, 43 | const std::set& frontVertices, 44 | const std::vector& boundaryVertices); 45 | 46 | std::set shortestPath(ManifoldSurfaceMesh& mesh, 47 | const EdgeData& weight, Vertex src, 48 | Vertex dst); 49 | 50 | VertexData distances(ManifoldSurfaceMesh& mesh, 51 | const EdgeData& weight, Vertex src); 52 | 53 | // Compute vertex indices of corners in cut mesh 54 | // Useful for building Laplacian on cut mesh 55 | // Returns the new indices, and the number of distinct indices (i.e. number of 56 | // vertices in the cut mesh) 57 | std::tuple, size_t> indexCorners(ManifoldSurfaceMesh& mesh, 58 | const std::set& cut); 59 | 60 | 61 | // ========================================================================== 62 | // Homology Generators via Tree-Cotree 63 | // ========================================================================== 64 | 65 | // Encodes the primal spanning tree via a VertexData primalParent 66 | // where primalParent[v].vertex() is v's parent, and primalParent[v].edge() is 67 | // the edge from the parent to v 68 | 69 | // Similarly, encodes the dual spanning tree via a Faedge> dualParent 70 | // where dualParent[f].face() is f's parent and dualParent[f].edge() is the 71 | // (primal) edge between f and its parent 72 | 73 | VertexData buildPrimalSpanningTree(ManifoldSurfaceMesh& mesh); 74 | FaceData 75 | buildDualSpanningTree(ManifoldSurfaceMesh& mesh, 76 | const VertexData& primalParent); 77 | 78 | VertexData buildPrimalSpanningTree(ManifoldSurfaceMesh& mesh, 79 | const EdgeData& edgeCost); 80 | FaceData 81 | buildDualSpanningTree(ManifoldSurfaceMesh& mesh, 82 | const VertexData& primalParent, 83 | const EdgeData& edgeCost); 84 | 85 | std::vector> 86 | computeHomologyGenerators(ManifoldSurfaceMesh& mesh); 87 | 88 | // Compute greedy homology generators according to 89 | // http://ddg.dreamhosters.com/CS1762014/wp-content/uploads/2014/02/Paper-Presentation.pdf 90 | // Slide 26 [Erickson?] 91 | 92 | std::vector> 93 | computeHomologyGenerators(ManifoldSurfaceMesh& mesh, 94 | const EdgeData& edgeCost); 95 | 96 | bool inPrimalSpanningTree(Edge e, const VertexData& primalParent); 97 | bool inDualSpanningTree(Edge e, const FaceData& dualParent); 98 | 99 | std::vector primalTreeLoop(Edge e, 100 | const VertexData& primalParent); 101 | 102 | 103 | // ========================================================================== 104 | // Data Structure for Cuts 105 | // ========================================================================== 106 | // Stolen from boundary-first-flattening 107 | typedef std::tuple VertexEntry; 108 | class DisjointSets { 109 | public: 110 | // constructor 111 | DisjointSets(size_t n_) : parent(n_ + 1), rank(n_ + 1), marked(n_ + 1) { 112 | // initialize all elements to be in different sets and to have rank 0 113 | for (size_t i = 0; i <= n_; i++) { 114 | rank[i] = 0; 115 | parent[i] = i; 116 | marked[i] = false; 117 | } 118 | } 119 | 120 | // find parent of element x 121 | int find(size_t x) { 122 | if (x != parent[x]) parent[x] = find(parent[x]); 123 | return parent[x]; 124 | } 125 | 126 | // union by rank 127 | // if either set is marked, then the result is marked 128 | void merge(size_t x, size_t y) { 129 | x = find(x); 130 | y = find(y); 131 | 132 | // smaller tree becomes a subtree of the larger tree 133 | if (rank[x] > rank[y]) 134 | parent[y] = x; 135 | else 136 | parent[x] = y; 137 | 138 | if (rank[x] == rank[y]) rank[y]++; 139 | 140 | // if either set was marked, both are marked 141 | if (marked[x] || marked[y]) { 142 | marked[x] = true; 143 | marked[y] = true; 144 | } 145 | } 146 | 147 | // mark set 148 | void mark(size_t x) { marked[find(x)] = true; } 149 | 150 | // unmark set 151 | void unmark(size_t x) { marked[find(x)] = false; } 152 | 153 | // check if set is marked 154 | bool isMarked(size_t x) { return marked[find(x)]; } 155 | 156 | private: 157 | // members 158 | std::vector parent; 159 | std::vector rank; 160 | std::vector marked; 161 | }; 162 | 163 | } // namespace ImplementationDetails 164 | } // namespace CEPS 165 | -------------------------------------------------------------------------------- /src/DirectSum.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace CEPS { 6 | namespace ImplementationDetails { 7 | 8 | template 9 | struct DirectSum { 10 | T1 x; 11 | T2 y; 12 | DirectSum(){}; 13 | DirectSum(T1 x_, T2 y_) : x(x_), y(y_){}; 14 | 15 | double norm() const; 16 | double norm2() const; 17 | }; 18 | 19 | template 20 | DirectSum operator+(const DirectSum& a, 21 | const DirectSum& b); 22 | 23 | template 24 | DirectSum operator-(const DirectSum& a, 25 | const DirectSum& b); 26 | 27 | template 28 | DirectSum operator*(const double& s, const DirectSum& a); 29 | 30 | template 31 | DirectSum operator*(const DirectSum& a, const double& s); 32 | 33 | template 34 | DirectSum operator*(const DirectSum& a, const double& s); 35 | 36 | double dot(double a, double b); 37 | 38 | template 39 | double dot(const DirectSum& a, const DirectSum& b); 40 | 41 | template 42 | bool operator==(const DirectSum& a, const DirectSum& b); 43 | 44 | template 45 | bool operator!=(const DirectSum& a, const DirectSum& b); 46 | 47 | template 48 | std::ostream& operator<<(std::ostream& str, const DirectSum& a); 49 | 50 | } // namespace ImplementationDetails 51 | } // namespace CEPS 52 | 53 | #include "DirectSum.ipp" 54 | -------------------------------------------------------------------------------- /src/DirectSum.ipp: -------------------------------------------------------------------------------- 1 | namespace CEPS { 2 | namespace ImplementationDetails { 3 | template 4 | double DirectSum::norm() const { 5 | return sqrt(norm2()); 6 | } 7 | 8 | template 9 | double DirectSum::norm2() const { 10 | return dot(*this, *this); 11 | } 12 | 13 | template 14 | DirectSum operator+(const DirectSum& a, 15 | const DirectSum& b) { 16 | return DirectSum(a.x + b.x, a.y + b.y); 17 | } 18 | 19 | template 20 | DirectSum operator-(const DirectSum& a, 21 | const DirectSum& b) { 22 | return DirectSum(a.x - b.x, a.y - b.y); 23 | } 24 | 25 | template 26 | DirectSum operator*(const double& s, const DirectSum& a) { 27 | return DirectSum(s * a.x, s * a.y); 28 | } 29 | 30 | template 31 | DirectSum operator*(const DirectSum& a, const double& s) { 32 | return DirectSum(a.x * s, a.y * s); 33 | } 34 | 35 | template 36 | DirectSum operator/(const DirectSum& a, const double& s) { 37 | return DirectSum(a.x / s, a.y / s); 38 | } 39 | 40 | template 41 | double dot(const DirectSum& a, const DirectSum& b) { 42 | return dot(a.x, b.x) + dot(a.y, b.y); 43 | } 44 | 45 | template 46 | bool operator==(const DirectSum& a, const DirectSum& b) { 47 | return a.x == b.x && a.y == b.y; 48 | } 49 | 50 | template 51 | bool operator!=(const DirectSum& a, const DirectSum& b) { 52 | return !(a == b); 53 | } 54 | 55 | template 56 | std::ostream& operator<<(std::ostream& str, const DirectSum& a) { 57 | str << "{" << a.x << ", " << a.y << "}"; 58 | return str; 59 | } 60 | } // namespace ImplementationDetails 61 | } // namespace CEPS 62 | -------------------------------------------------------------------------------- /src/FlipFormulas.cpp: -------------------------------------------------------------------------------- 1 | #include "FlipFormulas.h" 2 | /* 3 | * k 4 | * / |\ 5 | * / \ 6 | * eki ejk 7 | * / \ 8 | * |/ \ 9 | * i --- eij ----> j 10 | * \ /| 11 | * \ / 12 | * eil elj 13 | * \ / 14 | * \| / 15 | * l 16 | */ 17 | 18 | namespace CEPS { 19 | // Delaunay flip quantity (eq 9) for edge e, and flip(e) 20 | std::pair 21 | delaunayCondition(const std::array& lengths) { 22 | // unpack lengths into conveniently named variables 23 | double lij, ljk, lki, lil, llj; 24 | std::tie(lij, ljk, lki, lil, llj) = std::tuple_cat(lengths); 25 | 26 | double llk = flipHyperbolicLength(lengths); 27 | 28 | double flipQ = (lil * lki + ljk * llj) * (lil * ljk + lki * llj) - 29 | lij * lij * (ljk * lki + lil * llj); 30 | 31 | double flippedFlipQ = (ljk * lki + lil * llj) * (lil * ljk + lki * llj) - 32 | llk * llk * (lil * lki + ljk * llj); 33 | return std::make_pair(flipQ, flippedFlipQ); 34 | } 35 | 36 | size_t flipNormalCoordinate(const std::array& normalCoordinates) { 37 | // unpack normal coordinates into conveniently named variables 38 | size_t nij, njk, nki, nil, nlj; 39 | std::tie(nij, njk, nki, nil, nlj) = std::tuple_cat(normalCoordinates); 40 | 41 | int Eilj = positivePart(nlj - nij - nil); 42 | int Ejil = positivePart(nil - nlj - nij); 43 | int Elji = positivePart(nij - nil - nlj); 44 | 45 | int Eijk = positivePart(njk - nki - nij); 46 | int Ejki = positivePart(nki - nij - njk); 47 | int Ekij = positivePart(nij - njk - nki); 48 | 49 | double Cilj = -(negativePart(nlj - nij - nil) + Ejil + Elji) / 2.; 50 | double Cjil = -(negativePart(nil - nlj - nij) + Eilj + Elji) / 2.; 51 | double Clji = -(negativePart(nij - nil - nlj) + Eilj + Ejil) / 2.; 52 | 53 | double Cijk = -(negativePart(njk - nki - nij) + Ejki + Ekij) / 2.; 54 | double Cjki = -(negativePart(nki - nij - njk) + Eijk + Ekij) / 2.; 55 | double Ckij = -(negativePart(nij - njk - nki) + Eijk + Ejki) / 2.; 56 | 57 | int doubledAnswer = 2 * Clji + 2 * Ckij + abs(Cjil - Cjki) + 58 | abs(Cilj - Cijk) - Elji - Ekij + 2 * Eilj + 2 * Eijk + 59 | 2 * Ejil + 2 * Ejki - 2 * negativePart(nij - 1); 60 | 61 | return positivePart(std::round(doubledAnswer / 2)); 62 | } 63 | 64 | double flipEuclideanLength(const std::array& lengths) { 65 | // unpack lengths into conveniently named variables 66 | double lij, ljk, lki, lil, llj; 67 | std::tie(lij, ljk, lki, lil, llj) = std::tuple_cat(lengths); 68 | 69 | // Lay out triangle pair in the plane, and compute the length from k to l 70 | Vector2 pl{0., 0.}; 71 | Vector2 pj{llj, 0.}; 72 | Vector2 pi = layoutTriangleVertex( 73 | pl, pj, lij, lil); // involves more arithmetic than strictly necessary 74 | Vector2 pk = layoutTriangleVertex(pi, pj, ljk, lki); 75 | double llk = (pl - pk).norm(); 76 | 77 | return llk; 78 | } 79 | 80 | double flipHyperbolicLength(const std::array& lengths) { 81 | // unpack lengths into conveniently named variables 82 | double lij, ljk, lki, lil, llj; 83 | std::tie(lij, ljk, lki, lil, llj) = std::tuple_cat(lengths); 84 | 85 | return (ljk * lil + lki * llj) / lij; 86 | } 87 | 88 | std::array 89 | flipRoundabout(const std::array& normalCoordinates, size_t nlk, 90 | size_t dk, size_t dl, size_t rki, size_t rlj) { 91 | // unpack normal coordinates into conveniently named variables 92 | size_t nij, njk, nki, nil, nlj; 93 | std::tie(nij, njk, nki, nil, nlj) = std::tuple_cat(normalCoordinates); 94 | 95 | /* Rotates edge clockwise 96 | * k k 97 | * / |\ / ||\ 98 | * / \ / | \ 99 | * eki ejk / | \ 100 | * / \ e2 | e1 101 | * |/ \ |/ | \ 102 | * i --- eij ---> j i \ v /| j 103 | * \ /| \ | / 104 | * \ / \ | / 105 | * eil elj e3 | e4 106 | * \ / \ | / 107 | * \| / _\|/ 108 | * l l 109 | */ 110 | 111 | size_t deltaki = (nki == 0) ? 1 : 0; 112 | size_t deltalj = (nlj == 0) ? 1 : 0; 113 | 114 | size_t ekil = positivePart(nil - nki - nlk); 115 | size_t eljk = positivePart(njk - nlj - nlk); 116 | 117 | size_t rkl = (rki + deltaki + ekil) % dk; 118 | size_t rlk = (rlj + deltalj + eljk) % dl; 119 | 120 | return {rkl, rlk}; 121 | } 122 | } // namespace CEPS 123 | -------------------------------------------------------------------------------- /src/FlipFormulas.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Utils.h" 4 | 5 | #include "geometrycentral/utilities/elementary_geometry.h" 6 | 7 | /* 8 | * k 9 | * / |\ 10 | * / \ 11 | * eki ejk 12 | * / \ 13 | * |/ \ 14 | * i --- eij ----> j 15 | * \ /| 16 | * \ / 17 | * eil elj 18 | * \ / 19 | * \| / 20 | * l 21 | */ 22 | 23 | namespace CEPS { 24 | // Delaunay flip quantity (eq ???) for edge e, and flip(e) 25 | std::pair 26 | delaunayCondition(const std::array& lengths); 27 | 28 | size_t flipNormalCoordinate(const std::array& normalCoordinates); 29 | 30 | double flipEuclideanLength(const std::array& lengths); 31 | 32 | double flipHyperbolicLength(const std::array& lengths); 33 | 34 | std::array 35 | flipRoundabout(const std::array& normalCoordinates, size_t nlk, 36 | size_t dk, size_t dl, size_t rki, size_t rlj); 37 | } // namespace CEPS 38 | -------------------------------------------------------------------------------- /src/GeometryUtils.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "geometrycentral/numerical/linear_algebra_utilities.h" 4 | #include "geometrycentral/numerical/linear_solvers.h" 5 | #include "geometrycentral/surface/intrinsic_geometry_interface.h" 6 | #include "geometrycentral/surface/manifold_surface_mesh.h" 7 | 8 | #include "Utils.h" 9 | 10 | using namespace geometrycentral; 11 | using namespace geometrycentral::surface; 12 | 13 | namespace CEPS { 14 | 15 | // Compute the singularities of an n-vector field 16 | // from geometrycentral/surface/direction_fields.h 17 | 18 | // TODO: contribute back to geometry central 19 | VertexData computeVertexIndex(ManifoldSurfaceMesh& mesh, 20 | IntrinsicGeometryInterface& geo, 21 | const FaceData& directionField, 22 | int nSym); 23 | 24 | std::vector> 25 | lumpCones(ManifoldSurfaceMesh& mesh, 26 | const std::vector>& cones); 27 | 28 | // Doubles a mesh and geometry, gluing two copies of the mesh along their 29 | // boundary to produce a single mesh without boundary. 30 | // Returns the new mesh, the new geometry, the doubling map (taking each vertex 31 | // to its twin on the other copy of the original mesh), and a list of boundary 32 | // vertices. The doubling map sends boundary vertices to themselves 33 | std::tuple, 34 | std::unique_ptr, VertexData, 35 | std::vector> 36 | doubleMesh(ManifoldSurfaceMesh& mesh, VertexPositionGeometry& geo); 37 | 38 | // Copy a VertexPositionGeometry to a new ManifoldSurfaceMesh and 39 | // EdgeLengthGeometry 40 | std::pair, 41 | std::unique_ptr> 42 | copyGeometry(ManifoldSurfaceMesh& mesh, VertexPositionGeometry& geo); 43 | 44 | // Copy an EdgeLengthGeometry to a new ManifoldSurfaceMesh and 45 | // EdgeLengthGeometry 46 | std::pair, 47 | std::unique_ptr> 48 | copyGeometry(ManifoldSurfaceMesh& mesh, EdgeLengthGeometry& geo); 49 | 50 | // Count the number of flipped and zero-area triangles in a parameterization 51 | std::pair checkTriangleOrientations(ManifoldSurfaceMesh& mesh, 52 | CornerData& uv); 53 | 54 | namespace ImplementationDetails { 55 | 56 | // Doubles a mesh, gluing two copies of the mesh along their 57 | // boundary to produce a single mesh without boundary. 58 | // Returns the new mesh, the parent vertex in the original mesh for each 59 | // doubled vertex, and a list of boundary vertices Beware - the gluing 60 | // involves tricky halfedge manipulations 61 | std::tuple, VertexData, 62 | std::vector> 63 | doubleMesh(ManifoldSurfaceMesh& mesh); 64 | 65 | // Take in a list of faces, and an upper bound on the number vertices. 66 | // Reindex the faces to remove any unused vertices and return the map 67 | // sending an old index to its new compressed value Stolen from 68 | // geometrycentral/surface/simple_polygon_mesh.cpp 69 | std::vector stripUnusedVertices(std::vector>& faces, 70 | size_t nV); 71 | 72 | // Exact predicates from Jonathan Shewchuk's predicates.c 73 | extern "C" { 74 | void exactinit(); 75 | double orient2d(double* pa, double* pb, double* pc); 76 | } 77 | 78 | // Determine the orientation of triangle pqr. Returns a positive number if the 79 | // triangle is positively oriented, a negative number if it is negatively 80 | // oriented, and 0 if the triangle has 0 area. (Just a wrapper around Shewchuk's 81 | // orient2d). 82 | double orientation(const Vector2& p, const Vector2& q, const Vector2& r); 83 | 84 | 85 | // Computes the smallest eigenvector of M^-1*A orthogonal to the all 1s vector 86 | Vector> eig(SparseMatrix>& A, 87 | const SparseMatrix>& M, 88 | double tol); 89 | 90 | } // namespace ImplementationDetails 91 | } // namespace CEPS 92 | -------------------------------------------------------------------------------- /src/IO.cpp: -------------------------------------------------------------------------------- 1 | #include "IO.h" 2 | 3 | namespace CEPS { 4 | 5 | std::vector> 6 | readPrescribedConeAngles(std::string filename) { 7 | return readIndexValuePairs(filename); 8 | } 9 | 10 | std::vector> 11 | readPrescribedScaleFactors(std::string filename) { 12 | return readIndexValuePairs(filename, 0); 13 | } 14 | 15 | std::vector> 16 | readIndexValuePairs(std::string filename, double defaultValue) { 17 | std::ifstream inStream(filename, std::ios::binary); 18 | 19 | return readIndexValuePairs(inStream, defaultValue); 20 | } 21 | 22 | std::vector> 23 | readIndexValuePairs(std::istream& in, double defaultValue) { 24 | 25 | std::string line; 26 | bool done = false; 27 | std::vector> parsedList; 28 | while (!done && std::getline(in, line)) { 29 | std::stringstream ss(line); 30 | std::string token; 31 | 32 | ss >> token; 33 | 34 | if (token == "#") { 35 | continue; 36 | } else { 37 | size_t index; 38 | double value; 39 | 40 | // parse token to size_t 41 | sscanf(token.c_str(), "%zu", &index); 42 | 43 | // try to read a value to go with the index 44 | if (ss >> value) { 45 | // read succeeded, nothing to do here 46 | } else { 47 | value = defaultValue; 48 | } 49 | verbose_assert(!std::isnan(value), 50 | "Error, failed to parse line '" + line + "'"); 51 | 52 | parsedList.push_back(std::make_pair(index, value)); 53 | } 54 | } 55 | 56 | return parsedList; 57 | } 58 | 59 | void writeMeshWithProjectiveTextureCoords(const SimplePolygonMesh& mesh, 60 | const std::vector& pUV, 61 | std::string filename) { 62 | 63 | std::cout << "Writing mesh to: " << filename << std::endl; 64 | 65 | std::ofstream outFile(filename); 66 | if (!outFile) { 67 | throw std::runtime_error("failed to open output file " + filename); 68 | } 69 | writeMeshWithProjectiveTextureCoords(mesh, pUV, outFile); 70 | outFile.close(); 71 | } 72 | 73 | void writeMeshWithProjectiveTextureCoords(const SimplePolygonMesh& mesh, 74 | const std::vector& pUV, 75 | std::ostream& out) { 76 | 77 | // Write header 78 | out << "# vertices: " << mesh.vertexCoordinates.size() << std::endl; 79 | out << "# faces: " << mesh.polygons.size() << std::endl; 80 | out << std::endl; 81 | 82 | // Write vertices 83 | for (Vector3 p : mesh.vertexCoordinates) { 84 | out << "v " << p.x << " " << p.y << " " << p.z << std::endl; 85 | } 86 | 87 | 88 | // Write texture coords 89 | for (size_t iC = 0; iC < pUV.size(); iC++) { 90 | out << "vt " << pUV[iC].x << " " << pUV[iC].y << " " << pUV[iC].z 91 | << std::endl; 92 | } 93 | 94 | // Write faces 95 | size_t iC = 0; 96 | for (const std::vector& face : mesh.polygons) { 97 | out << "f"; 98 | for (size_t ind : face) { 99 | out << " " << (ind + 1) << "/" << (iC + 1); 100 | iC++; 101 | } 102 | out << std::endl; 103 | } 104 | } 105 | 106 | void writeMeshWithOrdinaryTextureCoords(const SimplePolygonMesh& mesh, 107 | const std::vector& pUV, 108 | std::string filename) { 109 | 110 | std::cout << "Writing mesh to: " << filename << std::endl; 111 | 112 | std::ofstream outFile(filename); 113 | if (!outFile) { 114 | throw std::runtime_error("failed to open output file " + filename); 115 | } 116 | writeMeshWithOrdinaryTextureCoords(mesh, pUV, outFile); 117 | outFile.close(); 118 | } 119 | 120 | void writeMeshWithOrdinaryTextureCoords(const SimplePolygonMesh& mesh, 121 | const std::vector& pUV, 122 | std::ostream& out) { 123 | 124 | // Write header 125 | out << "# vertices: " << mesh.vertexCoordinates.size() << std::endl; 126 | out << "# faces: " << mesh.polygons.size() << std::endl; 127 | out << std::endl; 128 | 129 | // Write vertices 130 | for (Vector3 p : mesh.vertexCoordinates) { 131 | out << "v " << p.x << " " << p.y << " " << p.z << std::endl; 132 | } 133 | 134 | 135 | // Write texture coords 136 | for (size_t iC = 0; iC < pUV.size(); iC++) { 137 | Vector2 uv{pUV[iC].x / pUV[iC].z, pUV[iC].y / pUV[iC].z}; 138 | out << "vt " << uv.x << " " << uv.y << std::endl; 139 | } 140 | 141 | // Write faces 142 | size_t iC = 0; 143 | for (const std::vector& face : mesh.polygons) { 144 | out << "f"; 145 | for (size_t ind : face) { 146 | out << " " << (ind + 1) << "/" << (iC + 1); 147 | iC++; 148 | } 149 | out << std::endl; 150 | } 151 | } 152 | 153 | void writeMeshWithProjectiveTextureCoords( 154 | const SimplePolygonMesh& mesh, 155 | const std::vector>& pUV, std::string filename) { 156 | 157 | std::cout << "Writing mesh to: " << filename << std::endl; 158 | 159 | std::ofstream outFile(filename); 160 | if (!outFile) { 161 | throw std::runtime_error("failed to open output file " + filename); 162 | } 163 | 164 | writeMeshWithProjectiveTextureCoords(mesh, pUV, outFile); 165 | outFile.close(); 166 | } 167 | 168 | void writeMeshWithProjectiveTextureCoords( 169 | const SimplePolygonMesh& mesh, 170 | const std::vector>& pUV, std::ostream& out) { 171 | 172 | // Write headers 173 | out << "# vertices: " << mesh.vertexCoordinates.size() << std::endl; 174 | out << "# faces: " << mesh.polygons.size() << std::endl; 175 | out << std::endl; 176 | 177 | // Write vertices 178 | for (Vector3 p : mesh.vertexCoordinates) { 179 | out << "v " << p.x << " " << p.y << " " << p.z << std::endl; 180 | } 181 | 182 | 183 | // Write texture coords 184 | for (size_t iC = 0; iC < pUV.size(); iC++) { 185 | out << "vt " << pUV[iC][0] << " " << pUV[iC][1] << " " << pUV[iC][2] 186 | << " " << pUV[iC][3] << std::endl; 187 | } 188 | 189 | // Write faces 190 | for (const std::vector& face : mesh.polygons) { 191 | out << "f"; 192 | for (size_t ind : face) { 193 | out << " " << (ind + 1) << "/" << (ind + 1); 194 | } 195 | out << std::endl; 196 | } 197 | } 198 | 199 | void writeMeshWithOrdinaryTextureCoords( 200 | const SimplePolygonMesh& mesh, 201 | const std::vector>& pUV, std::string filename) { 202 | 203 | std::cout << "Writing mesh to: " << filename << std::endl; 204 | 205 | std::ofstream outFile(filename); 206 | if (!outFile) { 207 | throw std::runtime_error("failed to open output file " + filename); 208 | } 209 | 210 | writeMeshWithOrdinaryTextureCoords(mesh, pUV, outFile); 211 | outFile.close(); 212 | } 213 | 214 | void writeMeshWithOrdinaryTextureCoords( 215 | const SimplePolygonMesh& mesh, 216 | const std::vector>& pUV, std::ostream& out) { 217 | 218 | // Write headers 219 | out << "# vertices: " << mesh.vertexCoordinates.size() << std::endl; 220 | out << "# faces: " << mesh.polygons.size() << std::endl; 221 | out << std::endl; 222 | 223 | // Write vertices 224 | for (Vector3 p : mesh.vertexCoordinates) { 225 | out << "v " << p.x << " " << p.y << " " << p.z << std::endl; 226 | } 227 | 228 | 229 | // Write texture coords 230 | for (size_t iC = 0; iC < pUV.size(); iC++) { 231 | Vector3 uv{pUV[iC][0] / pUV[iC][3], pUV[iC][1] / pUV[iC][3], 232 | pUV[iC][2] / pUV[iC][3]}; 233 | out << "vt " << uv[0] << " " << uv[1] << " " << uv[2] << std::endl; 234 | } 235 | 236 | // Write faces 237 | for (const std::vector& face : mesh.polygons) { 238 | out << "f"; 239 | for (size_t ind : face) { 240 | out << " " << (ind + 1) << "/" << (ind + 1); 241 | } 242 | out << std::endl; 243 | } 244 | } 245 | 246 | void writeVertexMap(ManifoldSurfaceMesh& mesh, const VertexData& vertexMap, 247 | std::string filename) { 248 | std::cout << "Writing vertex index map to: " << filename << std::endl; 249 | 250 | std::ofstream outFile(filename); 251 | if (!outFile) { 252 | throw std::runtime_error("failed to open output file " + filename); 253 | } 254 | 255 | writeVertexMap(mesh, vertexMap, outFile); 256 | 257 | outFile.close(); 258 | } 259 | 260 | void writeVertexMap(ManifoldSurfaceMesh& mesh, const VertexData& vertexMap, 261 | std::ostream& out) { 262 | for (Vertex v : mesh.vertices()) { 263 | out << vertexMap[v] << std::endl; 264 | } 265 | } 266 | 267 | FaceData readFFieldIntrinsic(std::string filename, 268 | ManifoldSurfaceMesh& mesh, 269 | VertexPositionGeometry& geo) { 270 | 271 | std::vector angles; 272 | std::ifstream inStream(filename, std::ios::binary); 273 | 274 | std::string line; 275 | bool done = false; 276 | while (!done && std::getline(inStream, line)) { 277 | std::stringstream ss(line); 278 | std::string token; 279 | 280 | ss >> token; 281 | 282 | if (token == "crossfield_angles") { 283 | std::getline(inStream, line); 284 | ss = std::stringstream(line); 285 | 286 | double angle; 287 | size_t nF = mesh.nFaces(); 288 | for (size_t iF = 0; iF < nF; ++iF) { 289 | ss >> angle; 290 | angles.push_back(angle); 291 | } 292 | done = true; 293 | } 294 | } 295 | 296 | if (!done) { 297 | std::cerr << "Didn't find crossfield angles?" << std::endl; 298 | } 299 | 300 | FaceData ffieldData(mesh); 301 | geo.requireCornerAngles(); 302 | FaceData fIdx = mesh.getFaceIndices(); 303 | for (Face f : mesh.faces()) { 304 | double theta = geo.cornerAngles[f.halfedge().corner()]; 305 | ffieldData[f] = Vector2::fromAngle(theta + angles[fIdx[f]]); 306 | } 307 | 308 | return ffieldData; 309 | } 310 | 311 | std::vector> 312 | readFFieldCones(std::string filename, ManifoldSurfaceMesh& mesh, 313 | VertexPositionGeometry& geo) { 314 | FaceData crossField = readFFieldIntrinsic(filename, mesh, geo); 315 | VertexData vertexIndices = 316 | computeVertexIndex(mesh, geo, crossField, 4); 317 | 318 | Vertex vBad; 319 | std::vector> cones; 320 | for (Vertex v : mesh.vertices()) { 321 | if (vertexIndices[v] != 0 && !v.isBoundary()) { 322 | cones.push_back(std::make_pair(v, M_PI / 2. * vertexIndices[v])); 323 | } 324 | if (vertexIndices[v] > 4) vBad = v; 325 | } 326 | 327 | return cones; 328 | } 329 | } // namespace CEPS 330 | -------------------------------------------------------------------------------- /src/IO.h: -------------------------------------------------------------------------------- 1 | #include "geometrycentral/surface/simple_polygon_mesh.h" 2 | 3 | #include "GeometryUtils.h" 4 | #include "Utils.h" 5 | 6 | namespace CEPS { 7 | 8 | std::vector> 9 | readPrescribedConeAngles(std::string filename); 10 | 11 | std::vector> 12 | readPrescribedScaleFactors(std::string filename); 13 | 14 | //== Read in a list of index-value pairs from a file. 15 | // Each pair must appear on its own line. 16 | // Lines beginning with '#' are ignored. 17 | // If defaultValue is set to something other than NaN, then indices which do not 18 | // have values are assigned the default value. Otherwise, throws an error if an 19 | // index appears without a corresponding value. 20 | std::vector> readIndexValuePairs( 21 | std::string filename, 22 | double defaultValue = std::numeric_limits::quiet_NaN()); 23 | 24 | std::vector> readIndexValuePairs( 25 | std::istream& in, 26 | double defaultValue = std::numeric_limits::quiet_NaN()); 27 | 28 | //== Save mesh to obj with projective texture coordinates 29 | // pUV gives UV coordinates at *corners* 30 | void writeMeshWithProjectiveTextureCoords(const SimplePolygonMesh& mesh, 31 | const std::vector& pUV, 32 | std::string filename); 33 | 34 | void writeMeshWithProjectiveTextureCoords(const SimplePolygonMesh& mesh, 35 | const std::vector& pUV, 36 | std::ostream& out); 37 | 38 | void writeMeshWithOrdinaryTextureCoords(const SimplePolygonMesh& mesh, 39 | const std::vector& pUV, 40 | std::string filename); 41 | 42 | void writeMeshWithOrdinaryTextureCoords(const SimplePolygonMesh& mesh, 43 | const std::vector& pUV, 44 | std::ostream& out); 45 | 46 | // pUV gives UV coordinates at *vertices*. 47 | void writeMeshWithProjectiveTextureCoords( 48 | const SimplePolygonMesh& mesh, 49 | const std::vector>& pUV, std::string filename); 50 | 51 | void writeMeshWithProjectiveTextureCoords( 52 | const SimplePolygonMesh& mesh, 53 | const std::vector>& pUV, std::ostream& out); 54 | 55 | void writeMeshWithOrdinaryTextureCoords( 56 | const SimplePolygonMesh& mesh, 57 | const std::vector>& pUV, std::string filename); 58 | 59 | void writeMeshWithOrdinaryTextureCoords( 60 | const SimplePolygonMesh& mesh, 61 | const std::vector>& pUV, std::ostream& out); 62 | 63 | //== Save a sparse matrix as a list of triplets. Uses 1-indexing 64 | template 65 | void saveMatrix(Eigen::SparseMatrix& matrix, std::string filename); 66 | 67 | template 68 | void saveMatrix(Eigen::SparseMatrix& matrix, std::ostream& out); 69 | 70 | void writeVertexMap(ManifoldSurfaceMesh& mesh, const VertexData& vertexMap, 71 | std::string filename); 72 | 73 | void writeVertexMap(ManifoldSurfaceMesh& mesh, const VertexData& vertexMap, 74 | std::ostream& out); 75 | 76 | //== Read cross fields from MPZ 77 | // Read the intrinsic cross field description. Output cross field is encoded by 78 | // an arbitrary choice of representative vector per face (i.e. not raised to the 79 | // 4th power) 80 | FaceData readFFieldIntrinsic(std::string filename, 81 | ManifoldSurfaceMesh& mesh, 82 | VertexPositionGeometry& geo); 83 | 84 | // Extract the cone angles (i.e. singularities) from an MPZ cross field 85 | std::vector> 86 | readFFieldCones(std::string filename, ManifoldSurfaceMesh& mesh, 87 | VertexPositionGeometry& geo); 88 | } // namespace CEPS 89 | 90 | #include "IO.ipp" 91 | -------------------------------------------------------------------------------- /src/IO.ipp: -------------------------------------------------------------------------------- 1 | namespace CEPS { 2 | 3 | // Stolen from https://github.com/nmwsharp/nonmanifold-Laplacian 4 | // src/main.cpp 5 | template 6 | void saveMatrix(Eigen::SparseMatrix& matrix, std::string filename) { 7 | 8 | // WARNING: this follows matlab convention and thus is 1-indexed 9 | 10 | std::cout << "Writing sparse matrix to: " << filename << std::endl; 11 | 12 | std::ofstream outFile(filename); 13 | if (!outFile) { 14 | throw std::runtime_error("failed to open output file " + filename); 15 | } 16 | saveMatrix(matrix, outFile); 17 | outFile.close(); 18 | } 19 | 20 | template 21 | void saveMatrix(Eigen::SparseMatrix& matrix, std::ostream& out) { 22 | out << std::setprecision(16); 23 | 24 | for (int k = 0; k < matrix.outerSize(); ++k) { 25 | for (typename Eigen::SparseMatrix::InnerIterator it(matrix, k); it; 26 | ++it) { 27 | T val = it.value(); 28 | size_t iRow = it.row(); 29 | size_t iCol = it.col(); 30 | 31 | out << (iRow + 1) << " " << (iCol + 1) << " " << val << std::endl; 32 | } 33 | } 34 | } 35 | } // namespace CEPS 36 | -------------------------------------------------------------------------------- /src/Layout.cpp: -------------------------------------------------------------------------------- 1 | #include "Layout.h" 2 | 3 | namespace CEPS { 4 | 5 | CornerData layOutTriangulation(Triangulation& tri, 6 | const std::vector& cones, 7 | double targetSurfaceArea) { 8 | 9 | 10 | EdgeData edgeCost(*tri.mesh, 1); 11 | for (Edge e : tri.mesh->edges()) verbose_assert(edgeCost[e] == 1, "??"); 12 | return layOutTriangulation(tri, cones, edgeCost, targetSurfaceArea); 13 | } 14 | 15 | CornerData layOutTriangulation(Triangulation& tri, 16 | const std::vector& cones, 17 | const EdgeData& edgeCutCosts, 18 | double targetSurfaceArea) { 19 | 20 | std::set cut = 21 | ImplementationDetails::goodCuts(*tri.mesh, cones, edgeCutCosts); 22 | 23 | // Hack to visualize cuts for debugging 24 | // auto drawCut = [&](const std::set& cut, std::string name) { 25 | // polyscope::SurfaceMesh* psMesh = 26 | // polyscope::getSurfaceMesh("input_mesh"); 27 | 28 | // std::vector cutVertexPositions; 29 | // std::vector> cutEdges; 30 | // for (Edge e : cut) { 31 | // cutEdges.push_back( 32 | // {cutVertexPositions.size(), cutVertexPositions.size() + 1}); 33 | // cutVertexPositions.push_back( 34 | // psMesh->vertices[e.firstVertex().getIndex()]); 35 | // cutVertexPositions.push_back( 36 | // psMesh->vertices[e.secondVertex().getIndex()]); 37 | // } 38 | // polyscope::registerCurveNetwork(name, cutVertexPositions, cutEdges); 39 | // }; 40 | // drawCut(cut, "cut"); 41 | 42 | CornerData cutCornerIndices; 43 | size_t nV; 44 | std::tie(cutCornerIndices, nV) = 45 | ImplementationDetails::indexCorners(*tri.mesh, cut); 46 | 47 | // Check that cut mesh is a topological disk 48 | // int cutEulerCharacteristic = 0; 49 | // cutEulerCharacteristic = 50 | // nV + tri.mesh->nFaces() - tri.mesh->nEdges() - cut.size(); 51 | // WATCH(cutEulerCharacteristic); 52 | 53 | 54 | // Build Laplacian for the cut mesh 55 | // (We can't use geometry-central's Laplacian due to the cuts) 56 | tri.geo->requireHalfedgeCotanWeights(); 57 | std::vector>> TL; 58 | for (Halfedge he : tri.mesh->interiorHalfedges()) { 59 | Corner cTail = he.corner(); 60 | Corner cHead = he.next().corner(); 61 | 62 | size_t iCHead = cutCornerIndices[cHead]; 63 | size_t iCTail = cutCornerIndices[cTail]; 64 | 65 | std::complex weight = tri.geo->halfedgeCotanWeights[he]; 66 | 67 | TL.emplace_back(iCTail, iCTail, weight); 68 | TL.emplace_back(iCHead, iCHead, weight); 69 | TL.emplace_back(iCTail, iCHead, -weight); 70 | TL.emplace_back(iCHead, iCTail, -weight); 71 | } 72 | for (size_t iC = 0; iC < nV; ++iC) TL.emplace_back(iC, iC, 1e-12); 73 | SparseMatrix> L(nV, nV); 74 | L.setFromTriplets(std::begin(TL), std::end(TL)); 75 | 76 | // Build the mass matrix 77 | std::vector>> TM; 78 | tri.geo->requireFaceAreas(); 79 | for (Face f : tri.mesh->faces()) { 80 | std::complex weight = tri.geo->faceAreas[f] / 3.; 81 | for (Corner c : f.adjacentCorners()) { 82 | size_t iC = cutCornerIndices[c]; 83 | TM.emplace_back(iC, iC, weight / 3.); 84 | } 85 | } 86 | SparseMatrix> M(nV, nV); 87 | M.setFromTriplets(std::begin(TM), std::end(TM)); 88 | 89 | // Build the area term 90 | std::complex i(0, 1); 91 | std::vector>> TA; 92 | 93 | for (Edge e : cut) { 94 | for (Halfedge he : e.adjacentInteriorHalfedges()) { 95 | size_t j = cutCornerIndices[he.next().corner()]; 96 | size_t k = cutCornerIndices[he.corner()]; 97 | 98 | TA.emplace_back( 99 | Eigen::Triplet>(j, k, i * 0.25)); 100 | TA.emplace_back( 101 | Eigen::Triplet>(k, j, i * -0.25)); 102 | } 103 | } 104 | SparseMatrix> A(nV, nV); 105 | A.setFromTriplets(std::begin(TA), std::end(TA)); 106 | 107 | SparseMatrix> EC = 0.5 * L - A; 108 | 109 | Vector> pos = ImplementationDetails::eig(EC, M, 1e-8); 110 | 111 | CornerData positions(*tri.mesh); 112 | for (Vertex v : tri.mesh->vertices()) { 113 | for (Corner c : v.adjacentCorners()) { 114 | Vector2 cPos = Vector2::fromComplex(pos(cutCornerIndices[c])); 115 | positions[c] = cPos; 116 | } 117 | } 118 | 119 | // Normalize parameterization to have the desired surface area 120 | double layoutSurfaceArea = 0; 121 | for (Face f : tri.mesh->faces()) { 122 | Vector2 p = positions[f.halfedge().corner()]; 123 | Vector2 q = positions[f.halfedge().next().corner()]; 124 | Vector2 r = positions[f.halfedge().next().next().corner()]; 125 | 126 | double fArea = abs(cross(q - p, r - p)) / 2.; 127 | layoutSurfaceArea += fArea; 128 | } 129 | 130 | positions *= sqrt(targetSurfaceArea / layoutSurfaceArea); 131 | 132 | return positions; 133 | } 134 | } // namespace CEPS 135 | -------------------------------------------------------------------------------- /src/Layout.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "geometrycentral/numerical/linear_algebra_utilities.h" 4 | #include "geometrycentral/numerical/linear_solvers.h" 5 | #include "geometrycentral/surface/intrinsic_geometry_interface.h" 6 | #include "geometrycentral/surface/manifold_surface_mesh.h" 7 | 8 | #include "Cutter.h" 9 | #include "Triangulation.h" 10 | #include "Utils.h" 11 | 12 | #include "polyscope/curve_network.h" 13 | #include "polyscope/surface_mesh.h" 14 | 15 | 16 | namespace CEPS { 17 | 18 | // Lay out a triangulation in the plane. The non-flat vertices must be provided 19 | // in the cones list. The parameterization is scaled to have an area of 20 | // targetSurfaceArea 21 | CornerData layOutTriangulation(Triangulation& tri, 22 | const std::vector& cones, 23 | double targetSurfaceArea = 1); 24 | 25 | // Lay out a triangulation. While cutting the triangulation open, this tries to 26 | // use cuts which have low cost according to edgeCutCosts, although the cuts are 27 | // not guaranteed to be optimal. 28 | CornerData layOutTriangulation(Triangulation& tri, 29 | const std::vector& cones, 30 | const EdgeData& edgeCutCosts, 31 | double targetSurfaceArea = 1); 32 | } // namespace CEPS 33 | -------------------------------------------------------------------------------- /src/Logger.cpp: -------------------------------------------------------------------------------- 1 | #include "Logger.h" 2 | 3 | std::string to_string(LogType lot) { 4 | switch (lot) { 5 | case LogType::STRING: 6 | return "string"; 7 | case LogType::DOUBLE: 8 | return "double"; 9 | } 10 | return "Unrecognized log type"; 11 | } 12 | 13 | Logger::Logger() {} 14 | 15 | template <> 16 | void Logger::log(std::string name, const char* val) { 17 | log(name, std::string(val)); 18 | } 19 | 20 | template <> 21 | void Logger::log(std::string name, std::string val) { 22 | logs.push_back(std::make_tuple(name, LogType::STRING, stringLogs.size())); 23 | stringLogs.push_back(val); 24 | } 25 | 26 | template <> 27 | void Logger::log(std::string name, bool val) { 28 | logs.push_back(std::make_tuple(name, LogType::STRING, stringLogs.size())); 29 | stringLogs.push_back(val ? "True" : "False"); 30 | } 31 | 32 | template <> 33 | void Logger::log(std::string name, double val) { 34 | logs.push_back(std::make_tuple(name, LogType::DOUBLE, doubleLogs.size())); 35 | doubleLogs.push_back(val); 36 | } 37 | 38 | template <> 39 | void Logger::log(std::string name, size_t val) { 40 | logs.push_back(std::make_tuple(name, LogType::DOUBLE, doubleLogs.size())); 41 | doubleLogs.push_back(val); 42 | } 43 | 44 | template <> 45 | void Logger::log(std::string name, int val) { 46 | logs.push_back(std::make_tuple(name, LogType::DOUBLE, doubleLogs.size())); 47 | doubleLogs.push_back(val); 48 | } 49 | 50 | bool Logger::writeLog(std::string filename) { 51 | std::ofstream out; 52 | 53 | // std::ios::trunc ensures that we overwrite anything previously in the file 54 | out.open(filename, std::ios::trunc); 55 | if (out.is_open()) { 56 | writeLog(out); 57 | out.close(); 58 | return true; 59 | } else { 60 | return false; 61 | } 62 | } 63 | 64 | void Logger::writeLog(std::ostream& out) { 65 | out << std::setw(12); 66 | size_t N = logs.size(); 67 | for (size_t iL = 0; iL + 1 < N; ++iL) { 68 | out << std::get<0>(logs[iL]) << "\t"; 69 | } 70 | out << std::get<0>(logs[logs.size() - 1]) << std::endl; 71 | 72 | auto printLog = [&](size_t iL) { 73 | switch (std::get<1>(logs[iL])) { 74 | case LogType::STRING: 75 | out << stringLogs[std::get<2>(logs[iL])]; 76 | break; 77 | case LogType::DOUBLE: 78 | out << doubleLogs[std::get<2>(logs[iL])]; 79 | break; 80 | } 81 | }; 82 | 83 | for (size_t iL = 0; iL + 1 < N; ++iL) { 84 | printLog(iL); 85 | out << "\t"; 86 | } 87 | printLog(logs.size() - 1); 88 | } 89 | -------------------------------------------------------------------------------- /src/Logger.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include // setw 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | enum class LogType { STRING, DOUBLE }; 11 | std::string to_string(LogType lot); 12 | 13 | std::ostream& operator<<(std::ostream& output, const LogType& lo); 14 | 15 | class Logger { 16 | public: 17 | Logger(); 18 | 19 | // use templates to prevent casting of input types 20 | template 21 | void log(std::string name, T val); 22 | 23 | bool writeLog(std::string filename); 24 | void writeLog(std::ostream& out); 25 | 26 | protected: 27 | std::vector> logs; 28 | std::vector stringLogs; 29 | std::vector doubleLogs; 30 | }; 31 | 32 | // explicit specializations 33 | // clang-format off 34 | template <> void Logger::log(std::string name, const char* val); 35 | template <> void Logger::log(std::string name, std::string val); 36 | template <> void Logger::log(std::string name, bool val); 37 | template <> void Logger::log(std::string name, double val); 38 | template <> void Logger::log(std::string name, int val); 39 | template <> void Logger::log(std::string name, size_t val); 40 | //clang-format on 41 | -------------------------------------------------------------------------------- /src/Optimizer.cpp: -------------------------------------------------------------------------------- 1 | #include "Optimizer.h" 2 | 3 | namespace CEPS { 4 | Optimizer::Optimizer(Triangulation& tri_, 5 | const VertexData& targetAngleDefects_) 6 | : tri(tri_) { 7 | targetAngleDefects = targetAngleDefects_.reinterpretTo(*tri.mesh); 8 | vIdx = tri.mesh->getVertexIndices(); 9 | } 10 | 11 | SparseMatrix Optimizer::hessian() { 12 | tri.geo->requireCotanLaplacian(); 13 | SparseMatrix H = tri.geo->cotanLaplacian; 14 | tri.geo->unrequireCotanLaplacian(); 15 | return H; 16 | } 17 | 18 | Vector Optimizer::gradient(std::vector fixed) { 19 | tri.geo->requireVertexGaussianCurvatures(); 20 | const VertexData& curvature = tri.geo->vertexGaussianCurvatures; 21 | Vector grad(tri.mesh->nVertices()); 22 | for (Vertex v : tri.mesh->vertices()) { 23 | grad[vIdx[v]] = curvature[v] - targetAngleDefects[v]; 24 | } 25 | for (Vertex v : fixed) { 26 | grad[vIdx[v]] = 0; 27 | } 28 | tri.geo->unrequireVertexGaussianCurvatures(); 29 | return grad; 30 | } 31 | 32 | 33 | double Optimizer::objective() { 34 | double obj = 0; 35 | // Vertex term 36 | for (Vertex v : tri.mesh->vertices()) { 37 | obj += (2 * M_PI - targetAngleDefects[v]) * tri.logScaleFactors[v]; 38 | } 39 | // Edge term 40 | const EdgeData& length = tri.geo->inputEdgeLengths; 41 | for (Edge e : tri.mesh->edges()) { 42 | obj -= 2 * M_PI * log(length[e]); 43 | } 44 | // Face term 45 | for (Face face : tri.mesh->faces()) { 46 | obj += 2 * f(face); 47 | } 48 | return obj; 49 | } 50 | 51 | bool Optimizer::takeStep(Vector stepDirVec, Vector gradVec) { 52 | VertexData stepDir(*tri.mesh, stepDirVec); 53 | VertexData oldU = tri.logScaleFactors; 54 | double oldF = objective(); 55 | double targetDecrease = stepDirVec.dot(gradVec); 56 | 57 | if (verbose) { 58 | std::cout << "\t gradient l2 norm: " << gradVec.lpNorm<2>(); 59 | if (extraVerbose) { 60 | std::cout << "\t" << targetDecrease << "\t " 61 | << gradVec.lpNorm(); 62 | } 63 | } 64 | 65 | double stepInfinityNorm = 66 | fmax(stepDirVec.maxCoeff(), -stepDirVec.minCoeff()); 67 | 68 | double stepSize = fmin(8, 30. / stepInfinityNorm); 69 | double newF = oldF; 70 | 71 | size_t iter = 0; 72 | while (newF > oldF + 1e-2 * stepSize * targetDecrease && iter < 50) { 73 | stepSize /= 2; 74 | tri.setScaleFactors(oldU + stepSize * stepDir); 75 | tri.flipToDelaunay(GeometryType::HYPERBOLIC); 76 | newF = objective(); 77 | iter += 1; 78 | } 79 | 80 | if (verbose) { 81 | if (extraVerbose) { 82 | std::cout << "\t" << stepSize << "\tstepNorm (l_infty): " 83 | << (stepDirVec * stepSize).norm(); 84 | } 85 | std::cout << std::endl; 86 | } 87 | 88 | return newF <= oldF + 1e-4 * stepSize * targetDecrease; 89 | } 90 | 91 | bool Optimizer::uniformize(std::vector fixed, double tol) { 92 | 93 | // Remove any duplicates 94 | std::sort(std::begin(fixed), std::end(fixed)); 95 | fixed.erase(std::unique(std::begin(fixed), std::end(fixed)), 96 | std::end(fixed)); 97 | 98 | tri.flipToDelaunay(GeometryType::HYPERBOLIC); 99 | Vector grad = gradient(fixed); 100 | 101 | size_t flips = 0; 102 | 103 | // Selector for free vertices. Only used if some scale factors are 104 | // fixed 105 | Vector isFree; 106 | Vector zeros; 107 | 108 | bool allFree = fixed.empty(); 109 | 110 | Vector ones; 111 | if (allFree) { 112 | ones = Vector::Constant(tri.mesh->nVertices(), 113 | 1. / tri.mesh->nVertices()); 114 | } else { 115 | isFree = Vector::Ones(tri.mesh->nVertices(), true); 116 | zeros = Vector::Zero(fixed.size()); 117 | for (Vertex v : fixed) isFree[vIdx[v]] = false; 118 | } 119 | 120 | 121 | if (verbose) { 122 | std::cout << "\tInitial objective: " << objective() << std::endl; 123 | } 124 | 125 | bool done = grad.dot(grad) < tol; 126 | size_t totalSteps = 0; 127 | while (!done && totalSteps < 1000) { 128 | SparseMatrix H = hessian(); 129 | 130 | if (verbose) { 131 | std::cout << "\t" << totalSteps << ": objective=" << objective(); 132 | } 133 | 134 | Vector step; 135 | 136 | if (allFree) { 137 | step = solvePositiveDefinite(H, grad); 138 | 139 | // Project out constant part 140 | step -= step.sum() * ones; 141 | 142 | verbose_assert((H * step - grad).norm() < 1e-5, "solve failed"); 143 | verbose_assert(abs(step.sum()) < 1e-5, 144 | "step direction has constant part ?!"); 145 | step = -step; 146 | } else { 147 | // If some scale factors are fixed, we need to project them 148 | // out of the solution 149 | BlockDecompositionResult decomp = 150 | blockDecomposeSquare(H, isFree, false); 151 | Vector rhsValsA, rhsValsB; 152 | decomposeVector(decomp, grad, rhsValsA, rhsValsB); 153 | Vector freeResult = 154 | solvePositiveDefinite(decomp.AA, rhsValsA); 155 | step = reassembleVector(decomp, freeResult, zeros); 156 | step = -step; 157 | } 158 | 159 | done = (abs(step.dot(grad)) < tol); 160 | 161 | if (!takeStep(step, grad) && verbose) { 162 | std::cout << totalSteps << ". Line search failed\t" << objective() 163 | << std::endl; 164 | } 165 | totalSteps++; 166 | 167 | // Update gradient 168 | grad = gradient(fixed); 169 | } 170 | 171 | if (verbose) { 172 | std::cout << "\tFinished in " << totalSteps << " steps." << std::endl; 173 | if (done) { 174 | std::cout << "\tConverged!" << std::endl; 175 | } else { 176 | std::cout << "\tDid not converge :'(" << std::endl; 177 | } 178 | } 179 | 180 | return done; 181 | } 182 | 183 | double Optimizer::f(Face face) { 184 | double out = 0; 185 | tri.geo->requireCornerAngles(); 186 | EdgeData& lengths = tri.geo->inputEdgeLengths; 187 | CornerData& angles = tri.geo->cornerAngles; 188 | for (Halfedge he : face.adjacentHalfedges()) { 189 | double angle = angles[he.next().next().corner()]; 190 | double contrib = 0; 191 | out += angle * log(lengths[he.edge()]); 192 | out += lobachevsky(angle); 193 | } 194 | if (out != out) out = std::numeric_limits::infinity(); 195 | tri.geo->unrequireCornerAngles(); 196 | return out; 197 | } 198 | } // namespace CEPS 199 | -------------------------------------------------------------------------------- /src/Optimizer.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "Triangulation.h" 3 | #include "Utils.h" 4 | 5 | #include "geometrycentral/numerical/linear_algebra_utilities.h" 6 | #include "geometrycentral/numerical/linear_solvers.h" 7 | 8 | using namespace geometrycentral; 9 | using namespace geometrycentral::surface; 10 | 11 | namespace CEPS { 12 | 13 | //== Optimizer for performing Euclidean uniformization/cone flattening 14 | class Optimizer { 15 | public: 16 | Optimizer(Triangulation& tri, const VertexData& targetAngleDefects); 17 | 18 | VertexData targetAngleDefects; 19 | 20 | SparseMatrix hessian(); 21 | Vector gradient(std::vector fixed); 22 | double objective(); 23 | 24 | bool takeStep(Vector stepDir, Vector grad); 25 | 26 | bool uniformize(std::vector fixed, double tol = 1e-5); 27 | 28 | bool verbose = false; 29 | bool extraVerbose = false; 30 | 31 | protected: 32 | Triangulation& tri; 33 | double f(Face f); 34 | VertexData vIdx; 35 | }; 36 | } // namespace CEPS 37 | -------------------------------------------------------------------------------- /src/SphericalUniformization/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | include("findPETSc.cmake") 2 | 3 | add_library( 4 | CEPS_SPHERICAL 5 | SphericalUniformization.cpp 6 | PartiallyDecoratedTriangulation.cpp 7 | PetscWrapper.cpp 8 | polyscope/SurfaceProjectiveSphericalParameterizationQuantity.cpp 9 | ../../deps/polyscope/deps/stb/stb_impl.cpp 10 | polyscope/bindata_map_equirectangular.cpp 11 | ) 12 | target_include_directories(CEPS_SPHERICAL PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}" ${PETSC_INCLUDES} "${CMAKE_CURRENT_SOURCE_DIR}/..") 13 | target_include_directories(CEPS_SPHERICAL PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/../../deps/polyscope/deps/stb/") 14 | target_link_libraries(CEPS_SPHERICAL polyscope geometry-central CEPS ${PETSC_LIBRARIES}) 15 | 16 | add_executable( 17 | spherical_uniformize 18 | SphericalDemo.cpp 19 | ) 20 | target_include_directories(spherical_uniformize PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}" ${PETSC_INCLUDES} "${CMAKE_CURRENT_SOURCE_DIR}/..") 21 | # borrow args.hxx directly from polyscope 22 | target_include_directories(spherical_uniformize PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/../../deps/polyscope/deps/args/") 23 | target_link_libraries(spherical_uniformize polyscope geometry-central CEPS CEPS_SPHERICAL ${PETSC_LIBRARIES}) 24 | -------------------------------------------------------------------------------- /src/SphericalUniformization/CorrectWindowsPaths.cmake: -------------------------------------------------------------------------------- 1 | # CorrectWindowsPaths - this module defines one macro 2 | # 3 | # CONVERT_CYGWIN_PATH( PATH ) 4 | # This uses the command cygpath (provided by cygwin) to convert 5 | # unix-style paths into paths useable by cmake on windows 6 | 7 | macro (CONVERT_CYGWIN_PATH _path) 8 | if (WIN32) 9 | EXECUTE_PROCESS(COMMAND cygpath.exe -m ${${_path}} 10 | OUTPUT_VARIABLE ${_path}) 11 | string (STRIP ${${_path}} ${_path}) 12 | endif (WIN32) 13 | endmacro (CONVERT_CYGWIN_PATH) 14 | -------------------------------------------------------------------------------- /src/SphericalUniformization/FindPackageMultipass.cmake: -------------------------------------------------------------------------------- 1 | # PackageMultipass - this module defines two macros 2 | # 3 | # FIND_PACKAGE_MULTIPASS (Name CURRENT 4 | # STATES VAR0 VAR1 ... 5 | # DEPENDENTS DEP0 DEP1 ...) 6 | # 7 | # This function creates a cache entry _CURRENT which 8 | # the user can set to "NO" to trigger a reconfiguration of the package. 9 | # The first time this function is called, the values of 10 | # _VAR0, ... are saved. If _CURRENT 11 | # is false or if any STATE has changed since the last time 12 | # FIND_PACKAGE_MULTIPASS() was called, then CURRENT will be set to "NO", 13 | # otherwise CURRENT will be "YES". IF not CURRENT, then 14 | # _DEP0, ... will be FORCED to NOTFOUND. 15 | # Example: 16 | # find_path (FOO_DIR include/foo.h) 17 | # FIND_PACKAGE_MULTIPASS (Foo foo_current 18 | # STATES DIR 19 | # DEPENDENTS INCLUDES LIBRARIES) 20 | # if (NOT foo_current) 21 | # # Make temporary files, run programs, etc, to determine FOO_INCLUDES and FOO_LIBRARIES 22 | # endif (NOT foo_current) 23 | # 24 | # MULTIPASS_SOURCE_RUNS (Name INCLUDES LIBRARIES SOURCE RUNS LANGUAGE) 25 | # Always runs the given test, use this when you need to re-run tests 26 | # because parent variables have made old cache entries stale. The LANGUAGE 27 | # variable is either C or CXX indicating which compiler the test should 28 | # use. 29 | # MULTIPASS_C_SOURCE_RUNS (Name INCLUDES LIBRARIES SOURCE RUNS) 30 | # DEPRECATED! This is only included for backwards compatability. Use 31 | # the more general MULTIPASS_SOURCE_RUNS instead. 32 | # Always runs the given test, use this when you need to re-run tests 33 | # because parent variables have made old cache entries stale. 34 | 35 | macro (FIND_PACKAGE_MULTIPASS _name _current) 36 | string (TOUPPER ${_name} _NAME) 37 | set (_args ${ARGV}) 38 | list (REMOVE_AT _args 0 1) 39 | 40 | set (_states_current "YES") 41 | list (GET _args 0 _cmd) 42 | if (_cmd STREQUAL "STATES") 43 | list (REMOVE_AT _args 0) 44 | list (GET _args 0 _state) 45 | while (_state AND NOT _state STREQUAL "DEPENDENTS") 46 | # The name of the stored value for the given state 47 | set (_stored_var PACKAGE_MULTIPASS_${_NAME}_${_state}) 48 | if (NOT "${${_stored_var}}" STREQUAL "${${_NAME}_${_state}}") 49 | set (_states_current "NO") 50 | endif (NOT "${${_stored_var}}" STREQUAL "${${_NAME}_${_state}}") 51 | set (${_stored_var} "${${_NAME}_${_state}}" CACHE INTERNAL "Stored state for ${_name}." FORCE) 52 | list (REMOVE_AT _args 0) 53 | list (GET _args 0 _state) 54 | endwhile (_state AND NOT _state STREQUAL "DEPENDENTS") 55 | endif (_cmd STREQUAL "STATES") 56 | 57 | set (_stored ${_NAME}_CURRENT) 58 | if (NOT ${_stored}) 59 | set (${_stored} "YES" CACHE BOOL "Is the configuration for ${_name} current? Set to \"NO\" to reconfigure." FORCE) 60 | set (_states_current "NO") 61 | endif (NOT ${_stored}) 62 | 63 | set (${_current} ${_states_current}) 64 | if (NOT ${_current} AND PACKAGE_MULTIPASS_${_name}_CALLED) 65 | message (STATUS "Clearing ${_name} dependent variables") 66 | # Clear all the dependent variables so that the module can reset them 67 | list (GET _args 0 _cmd) 68 | if (_cmd STREQUAL "DEPENDENTS") 69 | list (REMOVE_AT _args 0) 70 | foreach (dep ${_args}) 71 | set (${_NAME}_${dep} "NOTFOUND" CACHE INTERNAL "Cleared" FORCE) 72 | endforeach (dep) 73 | endif (_cmd STREQUAL "DEPENDENTS") 74 | set (${_NAME}_FOUND "NOTFOUND" CACHE INTERNAL "Cleared" FORCE) 75 | endif () 76 | set (PACKAGE_MULTIPASS_${name}_CALLED YES CACHE INTERNAL "Private" FORCE) 77 | endmacro (FIND_PACKAGE_MULTIPASS) 78 | 79 | 80 | macro (MULTIPASS_SOURCE_RUNS includes libraries source runs language) 81 | include (Check${language}SourceRuns) 82 | # This is a ridiculous hack. CHECK_${language}_SOURCE_* thinks that if the 83 | # *name* of the return variable doesn't change, then the test does 84 | # not need to be re-run. We keep an internal count which we 85 | # increment to guarantee that every test name is unique. If we've 86 | # gotten here, then the configuration has changed enough that the 87 | # test *needs* to be rerun. 88 | if (NOT MULTIPASS_TEST_COUNT) 89 | set (MULTIPASS_TEST_COUNT 00) 90 | endif (NOT MULTIPASS_TEST_COUNT) 91 | math (EXPR _tmp "${MULTIPASS_TEST_COUNT} + 1") # Why can't I add to a cache variable? 92 | set (MULTIPASS_TEST_COUNT ${_tmp} CACHE INTERNAL "Unique test ID") 93 | set (testname MULTIPASS_TEST_${MULTIPASS_TEST_COUNT}_${runs}) 94 | set (CMAKE_REQUIRED_INCLUDES ${includes}) 95 | set (CMAKE_REQUIRED_LIBRARIES ${libraries}) 96 | if(${language} STREQUAL "C") 97 | check_c_source_runs ("${source}" ${testname}) 98 | elseif(${language} STREQUAL "CXX") 99 | check_cxx_source_runs ("${source}" ${testname}) 100 | endif() 101 | set (${runs} "${${testname}}") 102 | endmacro (MULTIPASS_SOURCE_RUNS) 103 | 104 | macro (MULTIPASS_C_SOURCE_RUNS includes libraries source runs) 105 | multipass_source_runs("${includes}" "${libraries}" "${source}" ${runs} "C") 106 | endmacro (MULTIPASS_C_SOURCE_RUNS) 107 | -------------------------------------------------------------------------------- /src/SphericalUniformization/PartiallyDecoratedTriangulation.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | #include "geometrycentral/surface/edge_length_geometry.h" 5 | #include "geometrycentral/surface/manifold_surface_mesh.h" 6 | #include "geometrycentral/surface/vertex_position_geometry.h" 7 | 8 | #include "geometrycentral/utilities/elementary_geometry.h" // placeTriangleVertex 9 | 10 | #include "FlipFormulas.h" 11 | #include "GeometryUtils.h" 12 | #include "Triangulation.h" 13 | #include "Utils.h" 14 | 15 | using namespace geometrycentral; 16 | using namespace geometrycentral::surface; 17 | 18 | namespace CEPS { 19 | 20 | class PartiallyDecoratedTriangulation : public Triangulation { 21 | public: 22 | PartiallyDecoratedTriangulation(ManifoldSurfaceMesh& mesh, 23 | VertexPositionGeometry& geo); 24 | 25 | PartiallyDecoratedTriangulation(const Triangulation& otherTri, 26 | bool clearFlips); 27 | 28 | //== Core data 29 | VertexData invScaleFactors; 30 | CornerData originalHorocyclicArcLengths; 31 | 32 | //== Mutators 33 | 34 | void initializeHorocycleData(); 35 | 36 | // Flip to intrinsic Delaunay if gType is EUCLIDEAN and ideal Delaunay if 37 | // gType is HYPERBOLIC 38 | size_t flipToDelaunay(GeometryType gType); 39 | 40 | // Flip to ideal Delaunay, storing geometry via log lengths 41 | // (Generally you can just use flipToDelaunay. This should only be important 42 | // when computing single-source geodesic distance) 43 | void flipToLogHyperbolicDelaunay(bool verbose); 44 | 45 | void setScaleFactors(const VertexData& u); 46 | void setVertexScaleFactor(Vertex v, double u); 47 | 48 | void updateEdgeLengths(); // TODO: also update invscalefactors 49 | 50 | // Updates mesh, normal coordinates, roundabouts, edge lengths, and 51 | // horocyclic arc lengths. 52 | // Uses Euclidean formula if gType is EUCLIDEAN and 53 | // Ptolemy formula if gType is HYPERBOLIC Returns true if edge was flipped, 54 | // false if edge was unflippable 55 | bool flipEdge(Edge e, GeometryType gType); 56 | 57 | //== Data Accesses 58 | /* double currentEdgeLength(Edge e) const; */ 59 | 60 | // Return the (non-log) inverse scale factor associated with vertex v 61 | double computeInvScaleFactor(Vertex v) const; 62 | 63 | // Returns the length of the horocyclic arc relative to the original 64 | // horocycles 65 | double computeOriginalHorocyclicArcLength(Corner c) const; 66 | 67 | VertexData angleSums() const; 68 | 69 | // Returns true if the vertex has finite scale factor and false otherwise 70 | bool finite(Vertex v) const; 71 | 72 | // Returns true if none of the vertices of the edge have infinite scale 73 | // factors and false otherwise. 74 | bool finite(Edge f) const; 75 | 76 | // Returns true if none of the vertices of the face have infinite scale 77 | // factors and false otherwise. Assumes f is a triangle 78 | bool finite(Face f) const; 79 | 80 | // Numer of finite edges incident on vertex v 81 | size_t degE(Vertex v) const; 82 | 83 | // Numer of finite faces incident on vertex v 84 | size_t degF(Vertex v) const; 85 | 86 | bool isDelaunay(Edge e, double tol = 1e-8) const; 87 | 88 | // Essential edges of a Delaunay triangulation may not be flipped. 89 | // Nonessential edges may 90 | bool isEssential(Edge e, double tol = 1e-8) const; 91 | 92 | void checkEdgeLengthsNaN() const; 93 | 94 | bool satisfiesTriangleInequality() const; 95 | 96 | // Cotan laplacian of finite part of mesh 97 | SparseMatrix partialCotanLaplacian(); 98 | 99 | // Distance from v to v is given as 0 even though that isn't the true answer 100 | // because we never need it and I don't know how to compute it 101 | VertexData distanceOfHorocyclesTo(Vertex v, bool verbose = false); 102 | 103 | // Delaunay flip quantity for edge e, and flip(e) 104 | std::pair delaunayFlipQuantity(Edge e) const; 105 | 106 | void initializeRoundabouts(); 107 | }; 108 | } // namespace CEPS 109 | -------------------------------------------------------------------------------- /src/SphericalUniformization/PetscWrapper.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "PartiallyDecoratedTriangulation.h" 6 | 7 | using namespace geometrycentral; 8 | using namespace geometrycentral::surface; 9 | using namespace CEPS; 10 | 11 | using std::cerr; 12 | using std::cout; 13 | using std::endl; 14 | 15 | namespace CEPS { 16 | 17 | // Forward declarations of spherical energy, defined in SphericalUniformization 18 | double sphericalObjective(PartiallyDecoratedTriangulation& tri); 19 | Vector sphericalGradient(PartiallyDecoratedTriangulation& tri); 20 | SparseMatrix sphericalHessian(PartiallyDecoratedTriangulation& tri); 21 | 22 | class PetscWrapper { 23 | 24 | public: 25 | PetscWrapper(PartiallyDecoratedTriangulation& tri, size_t vInfinityIndex_); 26 | 27 | PetscWrapper(PartiallyDecoratedTriangulation& tri, size_t vInfinityIndex_, 28 | double vInfinityScaling); 29 | 30 | PetscWrapper(PartiallyDecoratedTriangulation& tri, 31 | VertexData& distancesToHorocycle_, 32 | size_t vInfinityIndex_, double vInfinityScaling); 33 | void setup(double vInfinityScaling); 34 | 35 | PartiallyDecoratedTriangulation& tri; 36 | Vertex vInfinity; 37 | size_t vInfinityIndex; 38 | VertexData distancesToHorocycle; 39 | VertexData vIdx; 40 | 41 | bool uniformize(int argc, char** argv); 42 | PetscErrorCode testOptimize(int argc, char** argv); 43 | 44 | void kktError(double& dualInfeasibility, 45 | double& complementarySlacknessError, double& angleError); 46 | 47 | bool activeConstraint(Vertex v); 48 | void adjustTriangulation(); 49 | void cleanUpTriangulation(); 50 | 51 | bool useFlips = true; 52 | double flipTime = 0; 53 | }; 54 | 55 | /* 56 | FormFunctionGradient - Evaluates the function, f(X), and gradient, G(X). 57 | 58 | Input Parameters: 59 | . tao - the Tao context 60 | . X - input vector 61 | . ptr - optional user-defined context, as set by 62 | TaoSetFunctionGradient() 63 | 64 | Output Parameters: 65 | . G - vector containing the newly evaluated gradient 66 | . f - function value 67 | 68 | Note: 69 | Some optimization methods ask for the function and the gradient evaluation 70 | at the same time. Evaluating both at once may be more efficient that 71 | evaluating each separately. 72 | */ 73 | PetscErrorCode FormFunctionGradient(Tao tao, Vec X, PetscReal* f, Vec G, 74 | void* ptr); 75 | 76 | PetscErrorCode FormFunctionGradientTest(Tao tao, Vec X, PetscReal* f, Vec G, 77 | void* ptr); 78 | 79 | /* 80 | FormHessian - Evaluates Hessian matrix. 81 | 82 | Input Parameters: 83 | . tao - the Tao context 84 | . x - input vector 85 | . ptr - optional user-defined context, as set by TaoSetHessian() 86 | 87 | Output Parameters: 88 | . H - Hessian matrix 89 | 90 | Note: Providing the Hessian may not be necessary. Only some solvers 91 | require this matrix. 92 | */ 93 | PetscErrorCode FormHessian(Tao tao, Vec X, Mat H, Mat Hpre, void* ptr); 94 | 95 | PetscErrorCode ConvergenceTest(Tao tao, void* ptr); 96 | } // namespace CEPS 97 | -------------------------------------------------------------------------------- /src/SphericalUniformization/ResolveCompilerPaths.cmake: -------------------------------------------------------------------------------- 1 | # ResolveCompilerPaths - this module defines two macros 2 | # 3 | # RESOLVE_LIBRARIES (XXX_LIBRARIES LINK_LINE) 4 | # This macro is intended to be used by FindXXX.cmake modules. 5 | # It parses a compiler link line and resolves all libraries 6 | # (-lfoo) using the library path contexts (-L/path) in scope. 7 | # The result in XXX_LIBRARIES is the list of fully resolved libs. 8 | # Example: 9 | # 10 | # RESOLVE_LIBRARIES (FOO_LIBRARIES "-L/A -la -L/B -lb -lc -ld") 11 | # 12 | # will be resolved to 13 | # 14 | # FOO_LIBRARIES:STRING="/A/liba.so;/B/libb.so;/A/libc.so;/usr/lib/libd.so" 15 | # 16 | # if the filesystem looks like 17 | # 18 | # /A: liba.so libc.so 19 | # /B: liba.so libb.so 20 | # /usr/lib: liba.so libb.so libc.so libd.so 21 | # 22 | # and /usr/lib is a system directory. 23 | # 24 | # Note: If RESOLVE_LIBRARIES() resolves a link line differently from 25 | # the native linker, there is a bug in this macro (please report it). 26 | # 27 | # RESOLVE_INCLUDES (XXX_INCLUDES INCLUDE_LINE) 28 | # This macro is intended to be used by FindXXX.cmake modules. 29 | # It parses a compile line and resolves all includes 30 | # (-I/path/to/include) to a list of directories. Other flags are ignored. 31 | # Example: 32 | # 33 | # RESOLVE_INCLUDES (FOO_INCLUDES "-I/A -DBAR='\"irrelevant -I/string here\"' -I/B") 34 | # 35 | # will be resolved to 36 | # 37 | # FOO_INCLUDES:STRING="/A;/B" 38 | # 39 | # assuming both directories exist. 40 | # Note: as currently implemented, the -I/string will be picked up mistakenly (cry, cry) 41 | include (CorrectWindowsPaths.cmake) 42 | 43 | macro (RESOLVE_LIBRARIES LIBS LINK_LINE) 44 | string (REGEX MATCHALL "((-L|-l|-Wl)([^\" ]+|\"[^\"]+\")|[^\" ]+\\.(a|so|dll|lib))" _all_tokens "${LINK_LINE}") 45 | set (_libs_found "") 46 | set (_directory_list "") 47 | foreach (token ${_all_tokens}) 48 | if (token MATCHES "-L([^\" ]+|\"[^\"]+\")") 49 | # If it's a library path, add it to the list 50 | string (REGEX REPLACE "^-L" "" token ${token}) 51 | string (REGEX REPLACE "//" "/" token ${token}) 52 | convert_cygwin_path(token) 53 | list (APPEND _directory_list ${token}) 54 | elseif (token MATCHES "^(-l([^\" ]+|\"[^\"]+\")|[^\" ]+\\.(a|so|dll|lib))") 55 | # It's a library, resolve the path by looking in the list and then (by default) in system directories 56 | if (WIN32) #windows expects "libfoo", linux expects "foo" 57 | string (REGEX REPLACE "^-l" "lib" token ${token}) 58 | else (WIN32) 59 | string (REGEX REPLACE "^-l" "" token ${token}) 60 | endif (WIN32) 61 | set (_root "") 62 | if (token MATCHES "^/") # We have an absolute path 63 | #separate into a path and a library name: 64 | string (REGEX MATCH "[^/]*\\.(a|so|dll|lib)$" libname ${token}) 65 | string (REGEX MATCH ".*[^${libname}$]" libpath ${token}) 66 | convert_cygwin_path(libpath) 67 | set (_directory_list ${_directory_list} ${libpath}) 68 | set (token ${libname}) 69 | endif (token MATCHES "^/") 70 | set (_lib "NOTFOUND" CACHE FILEPATH "Cleared" FORCE) 71 | find_library (_lib ${token} HINTS ${_directory_list} ${_root}) 72 | if (_lib) 73 | string (REPLACE "//" "/" _lib ${_lib}) 74 | list (APPEND _libs_found ${_lib}) 75 | else (_lib) 76 | message (STATUS "Unable to find library ${token}") 77 | endif (_lib) 78 | endif (token MATCHES "-L([^\" ]+|\"[^\"]+\")") 79 | endforeach (token) 80 | set (_lib "NOTFOUND" CACHE INTERNAL "Scratch variable" FORCE) 81 | # only the LAST occurence of each library is required since there should be no circular dependencies 82 | if (_libs_found) 83 | list (REVERSE _libs_found) 84 | list (REMOVE_DUPLICATES _libs_found) 85 | list (REVERSE _libs_found) 86 | endif (_libs_found) 87 | set (${LIBS} "${_libs_found}") 88 | endmacro (RESOLVE_LIBRARIES) 89 | 90 | macro (RESOLVE_INCLUDES INCS COMPILE_LINE) 91 | string (REGEX MATCHALL "-I([^\" ]+|\"[^\"]+\")" _all_tokens "${COMPILE_LINE}") 92 | set (_incs_found "") 93 | foreach (token ${_all_tokens}) 94 | string (REGEX REPLACE "^-I" "" token ${token}) 95 | string (REGEX REPLACE "//" "/" token ${token}) 96 | convert_cygwin_path(token) 97 | if (EXISTS ${token}) 98 | list (APPEND _incs_found ${token}) 99 | else (EXISTS ${token}) 100 | message (STATUS "Include directory ${token} does not exist") 101 | endif (EXISTS ${token}) 102 | endforeach (token) 103 | list (REMOVE_DUPLICATES _incs_found) 104 | set (${INCS} "${_incs_found}") 105 | endmacro (RESOLVE_INCLUDES) 106 | -------------------------------------------------------------------------------- /src/SphericalUniformization/SphericalDemo.cpp: -------------------------------------------------------------------------------- 1 | #include "geometrycentral/surface/manifold_surface_mesh.h" 2 | #include "geometrycentral/surface/meshio.h" 3 | #include "geometrycentral/surface/simple_polygon_mesh.h" 4 | #include "geometrycentral/surface/vertex_position_geometry.h" 5 | 6 | #include "polyscope/polyscope.h" 7 | #include "polyscope/surface_mesh.h" 8 | 9 | #include "args/args.hxx" 10 | #include "imgui.h" 11 | 12 | #include "IO.h" 13 | #include "Logger.h" 14 | #include "SphericalUniformization.h" 15 | 16 | using namespace geometrycentral; 17 | using namespace geometrycentral::surface; 18 | using namespace CEPS; 19 | 20 | //== Input data 21 | std::unique_ptr mesh; 22 | std::unique_ptr geometry; 23 | bool verbose = false; 24 | 25 | //== Result data 26 | SphericalUniformizationResult result; 27 | 28 | //== Visualization data 29 | polyscope::SurfaceMesh* psMesh; 30 | 31 | const int IM_STR_LEN = 128; 32 | static char meshSaveName[IM_STR_LEN]; 33 | 34 | enum class InterpolationType { PROJECTIVE, LINEAR }; 35 | int currentInterpolationType = 0; 36 | const char* prettyInterpolationTypeOptions[] = {"Homogeneous", "Linear"}; 37 | const InterpolationType interpolationTypeOptions[] = { 38 | InterpolationType::PROJECTIVE, InterpolationType::LINEAR}; 39 | 40 | // A user-defined callback, for creating control panels (etc) 41 | // Use ImGUI commands to build whatever you want here, see 42 | // https://github.com/ocornut/imgui/blob/master/imgui.h 43 | void myCallback() { 44 | if (ImGui::Button("Uniformize")) { 45 | bool viz = true; 46 | result = sphericalUniformize(*mesh, *geometry, Vertex(), viz, verbose); 47 | psMesh->setEnabled(false); 48 | } 49 | 50 | ImGui::Separator(); 51 | ImGui::InputText("###PtexturedMeshSaveName", meshSaveName, 52 | IM_ARRAYSIZE(meshSaveName)); 53 | ImGui::SameLine(); 54 | if (ImGui::Button("Save textured mesh")) { 55 | switch (interpolationTypeOptions[currentInterpolationType]) { 56 | case InterpolationType::PROJECTIVE: 57 | writeMeshWithProjectiveTextureCoords( 58 | result.mesh, result.param, std::string(meshSaveName) + ".obj"); 59 | break; 60 | case InterpolationType::LINEAR: 61 | writeMeshWithOrdinaryTextureCoords( 62 | result.mesh, result.param, std::string(meshSaveName) + ".obj"); 63 | break; 64 | } 65 | saveMatrix(result.interpolationMatrix, 66 | std::string(meshSaveName) + ".spmat"); 67 | } 68 | ImGui::Combo("Saved Texture Type", ¤tInterpolationType, 69 | prettyInterpolationTypeOptions, 70 | IM_ARRAYSIZE(interpolationTypeOptions)); 71 | } 72 | 73 | int main(int argc, char** argv) { 74 | 75 | // Configure the argument parser 76 | args::ArgumentParser parser("spherical uniformization demo"); 77 | args::Positional meshFilename( 78 | parser, "mesh", "Mesh to be processed (required)."); 79 | args::ValueFlag outputMeshFilename( 80 | parser, "string", 81 | "file to save output mesh to, along with homogeneous texture " 82 | "coordinates", 83 | {"outputMeshFilename"}); 84 | args::ValueFlag outputLinearTextureFilename( 85 | parser, "string", 86 | "file to save output mesh to, along with linear texture coordinates " 87 | "(aka ordinary uv coordinates)", 88 | {"outputLinearTextureFilename"}); 89 | args::ValueFlag outputMatrixFilename( 90 | parser, "string", "file to save the output interpolation matrix to", 91 | {"outputMatrixFilename"}); 92 | args::ValueFlag outputLogFilename( 93 | parser, "string", "file to save logs to", {"outputLogFilename"}); 94 | args::Flag viz(parser, "viz", "Use polyscope GUI", {"viz"}); 95 | args::ValueFlag beVerbose( 96 | parser, "verbose", 97 | "[y/n] Print out progress information (default " 98 | "true in GUI mode, false otherwise)", 99 | {"verbose"}); 100 | args::Flag version(parser, "version", "Display version number", 101 | {'v', "version"}); 102 | args::HelpFlag help(parser, "help", "Display this help menu", 103 | {'h', "help"}); 104 | 105 | // Parse args 106 | try { 107 | parser.ParseCLI(argc, argv); 108 | } catch (args::Help& h) { 109 | std::cout << parser; 110 | return 0; 111 | } catch (args::ParseError& e) { 112 | std::cerr << e.what() << std::endl; 113 | std::cerr << parser; 114 | return 1; 115 | } 116 | 117 | if (version) { 118 | std::cout << "spherical_uniformize version 1.2" << std::endl; 119 | return 0; 120 | } 121 | 122 | if (!meshFilename) { 123 | std::cout << "Please provide a mesh file as input" << std::endl; 124 | std::cout << parser; 125 | return 1; 126 | } 127 | 128 | verbose = args::get(viz); 129 | if (beVerbose) { 130 | std::string verbosity = args::get(beVerbose); 131 | std::transform(verbosity.begin(), verbosity.end(), verbosity.begin(), 132 | ::toupper); 133 | if (verbosity == "Y" || verbosity == "TRUE") { 134 | verbose = true; 135 | } else if (verbosity == "N" || verbosity == "FALSE") { 136 | verbose = false; 137 | } else { 138 | std::cout << "Unrecognized verbosity option '" << verbosity << "'" 139 | << std::endl; 140 | std::cout << "\t please use true/false or y/n" << std::endl; 141 | std::cout << parser; 142 | return 1; 143 | } 144 | } 145 | 146 | std::string filename = args::get(meshFilename); 147 | 148 | // Load mesh 149 | std::tie(mesh, geometry) = loadMesh(filename); 150 | verbose_assert(mesh->nConnectedComponents() == 1, "mesh must be connected"); 151 | verbose_assert(mesh->genus() == 0, "mesh must have genus 0 (but it is " + 152 | std::to_string(mesh->genus()) + ")"); 153 | verbose_assert(mesh->nBoundaryLoops() == 0, "mesh must not have boundary"); 154 | 155 | // center mesh 156 | Vector3 mean = Vector3::zero(); 157 | for (Vertex v : mesh->vertices()) mean += geometry->inputVertexPositions[v]; 158 | mean /= mesh->nVertices(); 159 | for (Vertex v : mesh->vertices()) geometry->inputVertexPositions[v] -= mean; 160 | 161 | std::string nicename = polyscope::guessNiceNameFromPath(filename); 162 | 163 | std::string saveNameGuess = nicename + "_spherical"; 164 | // Initialize ImGui's C-string to our niceName 165 | // https://stackoverflow.com/a/347959 166 | // truncate saveNameGuess to fit in ImGui's string 167 | if (saveNameGuess.length() + 1 > IM_STR_LEN) 168 | saveNameGuess.resize(IM_STR_LEN - 1); 169 | // copy over string contents 170 | std::copy(saveNameGuess.begin(), saveNameGuess.end(), meshSaveName); 171 | // null-terminate string 172 | meshSaveName[saveNameGuess.size()] = '\0'; 173 | 174 | if (args::get(viz)) { 175 | // Initialize polyscope 176 | polyscope::init(); 177 | 178 | // Set the callback function 179 | polyscope::state::userCallback = myCallback; 180 | 181 | // Register the mesh with polyscope 182 | psMesh = polyscope::registerSurfaceMesh( 183 | "input_mesh", geometry->inputVertexPositions, 184 | mesh->getFaceVertexList(), polyscopePermutations(*mesh)); 185 | 186 | std::cout << "Loaded mesh " << filename << std::endl; 187 | 188 | // Give control to the polyscope gui 189 | polyscope::show(); 190 | } else { 191 | if (outputLogFilename) { 192 | std::ofstream out; 193 | 194 | // std::ios::trunc ensures that we overwrite old versions 195 | out.open(args::get(outputLogFilename), std::ios::trunc); 196 | if (out.is_open()) { 197 | // output a temporary symbol so we can tell if the program 198 | // crashes before writing the real log 199 | out << ":'(" << std::endl; 200 | out.close(); 201 | } else { 202 | std::cout << "Error: failed to write to " 203 | << args::get(outputLogFilename) << vendl; 204 | } 205 | } 206 | 207 | Logger logger; 208 | bool logStats = false; 209 | if (outputLogFilename) { 210 | logStats = true; 211 | logger.log("name", nicename); 212 | logger.log("nVertices", mesh->nVertices()); 213 | } 214 | 215 | double duration; 216 | std::clock_t start = std::clock(); 217 | 218 | bool viz = false; 219 | result = sphericalUniformize(*mesh, *geometry, Vertex(), viz, verbose); 220 | 221 | duration = (std::clock() - start) / (double)CLOCKS_PER_SEC; 222 | 223 | 224 | if (outputMeshFilename) { 225 | writeMeshWithProjectiveTextureCoords(result.mesh, result.param, 226 | args::get(outputMeshFilename)); 227 | } 228 | 229 | if (outputLinearTextureFilename) { 230 | writeMeshWithOrdinaryTextureCoords( 231 | result.mesh, result.param, 232 | args::get(outputLinearTextureFilename)); 233 | } 234 | 235 | if (outputMatrixFilename) { 236 | saveMatrix(result.interpolationMatrix, 237 | args::get(outputMatrixFilename)); 238 | } 239 | 240 | if (logStats) { 241 | logger.log("duration", duration); 242 | logger.writeLog(args::get(outputLogFilename)); 243 | } 244 | } 245 | 246 | return EXIT_SUCCESS; 247 | } 248 | -------------------------------------------------------------------------------- /src/SphericalUniformization/SphericalUniformization.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "geometrycentral/surface/manifold_surface_mesh.h" 4 | #include "geometrycentral/surface/meshio.h" 5 | #include "geometrycentral/surface/simple_polygon_mesh.h" 6 | #include "geometrycentral/surface/surface_centers.h" 7 | #include "geometrycentral/surface/vertex_position_geometry.h" 8 | 9 | #include "polyscope/SurfaceProjectiveSphericalParameterizationQuantity.h" 10 | #include "polyscope/surface_mesh.h" 11 | 12 | #include "CommonRefinement.h" 13 | #include "Layout.h" 14 | #include "PartiallyDecoratedTriangulation.h" 15 | #include "PetscWrapper.h" 16 | 17 | namespace CEPS { 18 | 19 | // Map is stored via projective coordinates at mesh vertices 20 | struct SphericalUniformizationResult { 21 | SimplePolygonMesh mesh; 22 | std::vector> param; 23 | VertexData parentMap; 24 | SparseMatrix interpolationMatrix; 25 | }; 26 | 27 | SphericalUniformizationResult 28 | hemisphericalUniformize(ManifoldSurfaceMesh& mesh, VertexPositionGeometry& geo, 29 | bool viz = false); 30 | 31 | SphericalUniformizationResult sphericalUniformize(ManifoldSurfaceMesh& mesh, 32 | VertexPositionGeometry& geo, 33 | Vertex vInfinity = Vertex(), 34 | bool viz = false, 35 | bool verbose = false); 36 | 37 | //== The spherical uniformization energy 38 | double sphericalObjective(PartiallyDecoratedTriangulation& tri); 39 | Vector sphericalGradient(PartiallyDecoratedTriangulation& tri); 40 | SparseMatrix sphericalHessian(PartiallyDecoratedTriangulation& tri); 41 | 42 | VertexData 43 | sphericalGradientVertexData(PartiallyDecoratedTriangulation& tri); 44 | 45 | namespace ImplementationDetails { 46 | VertexData layOutTriangulation(PartiallyDecoratedTriangulation& tri, 47 | Vertex vInfinity); 48 | 49 | VertexData mobiusCenter(ManifoldSurfaceMesh& mesh, 50 | const VertexData& positions, 51 | VertexData area, bool verbose = false); 52 | 53 | std::tuple>, 54 | VertexData, SparseMatrix> 55 | computeCommonRefinementAndMatrix(Triangulation& Ta, Triangulation& Tb, 56 | Triangulation& Tc, 57 | const VertexData& initialPositions, 58 | const VertexData& sphericalPositions, 59 | const std::set& frontFaces, 60 | bool verbose = false); 61 | } // namespace ImplementationDetails 62 | } // namespace CEPS 63 | -------------------------------------------------------------------------------- /src/SphericalUniformization/polyscope/SurfaceProjectiveSphericalParameterizationQuantity.cpp: -------------------------------------------------------------------------------- 1 | #include "SurfaceProjectiveSphericalParameterizationQuantity.h" 2 | 3 | #include "polyscope/file_helpers.h" 4 | #include "polyscope/polyscope.h" 5 | #include "polyscope/render/engine.h" 6 | 7 | #include 8 | 9 | #include "imgui.h" 10 | 11 | using std::cout; 12 | using std::endl; 13 | 14 | namespace polyscope { 15 | 16 | // == Custom shader rule for projective texture interpolation 17 | const render::ShaderReplacementRule MESH_PROPAGATE_PROJECTIVE_VALUE4( 18 | /* rule name */ "MESH_PROPAGATE_PROJECTIVE_VALUE2", 19 | { 20 | /* replacement sources */ 21 | {"VERT_DECLARATIONS", R"( 22 | in vec4 a_value4; 23 | out vec4 a_value4ToFrag; 24 | )"}, 25 | {"VERT_ASSIGNMENTS", R"( 26 | a_value4ToFrag = a_value4; 27 | )"}, 28 | {"FRAG_DECLARATIONS", R"( 29 | in vec4 a_value4ToFrag; 30 | uniform mat4 u_rot; 31 | const float PI = 3.1415926535897932384626433832795; 32 | )"}, 33 | {"GENERATE_SHADE_VALUE", R"( 34 | vec3 p = vec3(u_rot * vec4(a_value4ToFrag.xyz, 0)); 35 | vec3 projectivePosition = p / a_value4ToFrag.w; 36 | float theta = atan(projectivePosition.y, projectivePosition.x); 37 | float phi = acos(projectivePosition.z); 38 | vec2 tCoord = vec2((theta+PI) / (2*PI), phi / PI); 39 | )"}, 40 | }, 41 | /* uniforms */ {{"u_rot", render::DataType::Matrix44Float}}, 42 | /* attributes */ 43 | { 44 | {"a_value4", render::DataType::Vector4Float}, 45 | }, 46 | /* textures */ {}); 47 | 48 | const render::ShaderReplacementRule SHADE_TEXTURE( 49 | /* rule name */ "SHADE_TEXTURE", 50 | {/* replacement sources */ 51 | {"FRAG_DECLARATIONS", R"( 52 | uniform sampler2D t_ceps_image; 53 | vec3 undoGammaCorrect(vec3 colorLinear); 54 | )"}, 55 | {"GENERATE_SHADE_COLOR", R"( 56 | vec3 albedoColor = undoGammaCorrect(texture(t_ceps_image, tCoord).rgb); 57 | )"}}, 58 | /* uniforms */ 59 | {}, 60 | /* attributes */ {}, 61 | /* textures */ {{"t_ceps_image", 2}}); 62 | 63 | // ============================================================== 64 | // ======= Base Projective Spherical Parameterization ========== 65 | // ============================================================== 66 | 67 | SurfaceProjectiveSphericalParameterizationQuantity:: 68 | SurfaceProjectiveSphericalParameterizationQuantity(std::string name, 69 | SurfaceMesh& mesh_) 70 | : SurfaceMeshQuantity(name, mesh_, true), 71 | eulerAngles(uniquePrefix() + "#eulerAngles", glm::vec3(0, 0, 0)) 72 | 73 | { 74 | 75 | // Register a custom shader rule for projective interpolation 76 | render::engine->registerShaderRule("MESH_PROPAGATE_PROJECTIVE_VALUE4", 77 | MESH_PROPAGATE_PROJECTIVE_VALUE4); 78 | render::engine->registerShaderRule("SHADE_TEXTURE", SHADE_TEXTURE); 79 | 80 | 81 | // Load texture 82 | int width, height, numComponents; 83 | 84 | unsigned char* imgData = 85 | stbi_load_from_memory(reinterpret_cast( 86 | &gl::bindata_map_equirectangular[0]), 87 | gl::bindata_map_equirectangular.size(), &width, 88 | &height, &numComponents, STBI_rgb); 89 | // unsigned char* imgData = 90 | // stbi_load("/Users/mgillesp/Downloads/earth.png", &width, &height, 91 | // &numComponents, STBI_rgb); 92 | 93 | if (!imgData) throw std::logic_error("failed to load earth texture image"); 94 | textureBuffer = render::engine->generateTextureBuffer( 95 | TextureFormat::RGB8, width, height, imgData); 96 | stbi_image_free(imgData); 97 | } 98 | 99 | void SurfaceProjectiveSphericalParameterizationQuantity::draw() { 100 | if (!isEnabled()) return; 101 | 102 | if (program == nullptr) { 103 | createProgram(); 104 | } 105 | 106 | // Set uniforms 107 | parent.setTransformUniforms(*program); 108 | setProgramUniforms(*program); 109 | parent.setStructureUniforms(*program); 110 | 111 | program->draw(); 112 | } 113 | 114 | void SurfaceProjectiveSphericalParameterizationQuantity::createProgram() { 115 | // Create the program to draw this quantity 116 | program = render::engine->requestShader( 117 | "MESH", parent.addStructureRules( 118 | {"MESH_PROPAGATE_PROJECTIVE_VALUE4", "SHADE_TEXTURE"})); 119 | // "SHADE_CHECKER_VALUE2"})); 120 | // Fill color buffers 121 | fillColorBuffers(*program); 122 | parent.fillGeometryBuffers(*program); 123 | 124 | render::engine->setMaterial(*program, parent.getMaterial()); 125 | 126 | program->setTextureFromBuffer("t_ceps_image", textureBuffer.get()); 127 | } 128 | 129 | 130 | // Update range uniforms 131 | void SurfaceProjectiveSphericalParameterizationQuantity::setProgramUniforms( 132 | render::ShaderProgram& program) { 133 | 134 | glm::mat4 rotation = glm::eulerAngleYXZ( 135 | eulerAngles.get().x, eulerAngles.get().y, eulerAngles.get().z); 136 | program.setUniform("u_rot", &rotation[0][0]); 137 | } 138 | 139 | void SurfaceProjectiveSphericalParameterizationQuantity::buildCustomUI() { 140 | bool updated = false; 141 | if (ImGui::SliderFloat("alpha", &eulerAngles.get().x, 0.f, 2 * M_PI, 142 | "%.3f")) 143 | updated = true; 144 | if (ImGui::SliderFloat("beta", &eulerAngles.get().y, 0.f, 2 * M_PI, "%.3f")) 145 | updated = true; 146 | if (ImGui::SliderFloat("gamma", &eulerAngles.get().z, 0.f, 2 * M_PI, 147 | "%.3f")) 148 | updated = true; 149 | 150 | if (updated) setEulerAngles(getEulerAngles()); 151 | } 152 | 153 | 154 | SurfaceProjectiveSphericalParameterizationQuantity* 155 | SurfaceProjectiveSphericalParameterizationQuantity::setEulerAngles( 156 | const glm::vec3& newEulerAngles) { 157 | eulerAngles = newEulerAngles; 158 | program.reset(); 159 | requestRedraw(); 160 | return this; 161 | } 162 | 163 | glm::vec3 SurfaceProjectiveSphericalParameterizationQuantity::getEulerAngles() { 164 | return eulerAngles.get(); 165 | } 166 | void SurfaceProjectiveSphericalParameterizationQuantity::refresh() { 167 | program.reset(); 168 | Quantity::refresh(); 169 | } 170 | 171 | // ============================================================== 172 | // =============== Vertex Parameterization ==================== 173 | // ============================================================== 174 | 175 | 176 | SurfaceVertexProjectiveSphericalParameterizationQuantity:: 177 | SurfaceVertexProjectiveSphericalParameterizationQuantity( 178 | std::string name, std::vector coords_, SurfaceMesh& mesh_) 179 | : SurfaceProjectiveSphericalParameterizationQuantity(name, mesh_), 180 | coords(std::move(coords_)) {} 181 | 182 | std::string 183 | SurfaceVertexProjectiveSphericalParameterizationQuantity::niceName() { 184 | return name + " (vertex proj-sph parameterization)"; 185 | } 186 | 187 | void SurfaceVertexProjectiveSphericalParameterizationQuantity::fillColorBuffers( 188 | render::ShaderProgram& p) { 189 | std::vector coordVal; 190 | coordVal.reserve(3 * parent.nFacesTriangulation()); 191 | 192 | for (size_t iF = 0; iF < parent.nFaces(); iF++) { 193 | auto& face = parent.faces[iF]; 194 | size_t D = face.size(); 195 | 196 | // implicitly triangulate from root 197 | size_t vRoot = face[0]; 198 | for (size_t j = 1; (j + 1) < D; j++) { 199 | size_t vB = face[j]; 200 | size_t vC = face[(j + 1) % D]; 201 | 202 | coordVal.push_back(coords[vRoot]); 203 | coordVal.push_back(coords[vB]); 204 | coordVal.push_back(coords[vC]); 205 | } 206 | } 207 | 208 | // Store data in buffers 209 | p.setAttribute("a_value4", coordVal); 210 | } 211 | 212 | void SurfaceVertexProjectiveSphericalParameterizationQuantity:: 213 | buildVertexInfoGUI(size_t vInd) { 214 | ImGui::TextUnformatted(name.c_str()); 215 | ImGui::NextColumn(); 216 | ImGui::Text("<%g,%g,%g,%g>", coords[vInd].x, coords[vInd].y, coords[vInd].z, 217 | coords[vInd].w); 218 | ImGui::NextColumn(); 219 | } 220 | 221 | 222 | } // namespace polyscope 223 | -------------------------------------------------------------------------------- /src/SphericalUniformization/polyscope/SurfaceProjectiveSphericalParameterizationQuantity.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "polyscope/affine_remapper.h" 4 | #include "polyscope/histogram.h" 5 | #include "polyscope/render/color_maps.h" 6 | #include "polyscope/render/engine.h" 7 | #include "polyscope/surface_mesh.h" 8 | 9 | #include "polyscope/bindata.h" 10 | 11 | #include "stb_image.h" 12 | 13 | namespace polyscope { 14 | 15 | 16 | // ============================================================== 17 | // ======= Base Projective Spherical Parameterization ========== 18 | // ============================================================== 19 | 20 | class SurfaceProjectiveSphericalParameterizationQuantity 21 | : public SurfaceMeshQuantity { 22 | 23 | public: 24 | SurfaceProjectiveSphericalParameterizationQuantity(std::string name, 25 | SurfaceMesh& mesh_); 26 | 27 | void draw() override; 28 | 29 | virtual void buildCustomUI() override; 30 | 31 | virtual void refresh() override; 32 | 33 | 34 | // === Members 35 | 36 | // === Viz stuff 37 | // to keep things simple, has settings for all of the viz styles, even 38 | // though not all are used at all times 39 | 40 | 41 | // === Getters and setters for visualization options 42 | SurfaceProjectiveSphericalParameterizationQuantity* 43 | setEulerAngles(const glm::vec3& newEulerAngles); 44 | glm::vec3 getEulerAngles(); 45 | 46 | protected: 47 | // === Visualiztion options 48 | PersistentValue eulerAngles; 49 | std::shared_ptr program; 50 | std::shared_ptr textureBuffer; 51 | 52 | // Helpers 53 | void createProgram(); 54 | void setProgramUniforms(render::ShaderProgram& program); 55 | virtual void fillColorBuffers(render::ShaderProgram& p) = 0; 56 | }; 57 | 58 | 59 | // ============================================================== 60 | // =============== Vertex Parameterization ==================== 61 | // ============================================================== 62 | 63 | class SurfaceVertexProjectiveSphericalParameterizationQuantity 64 | : public SurfaceProjectiveSphericalParameterizationQuantity { 65 | 66 | public: 67 | SurfaceVertexProjectiveSphericalParameterizationQuantity( 68 | std::string name, std::vector values_, SurfaceMesh& mesh_); 69 | 70 | virtual void buildVertexInfoGUI(size_t vInd) override; 71 | virtual std::string niceName() override; 72 | 73 | // === Members 74 | std::vector coords; // on vertices 75 | 76 | protected: 77 | virtual void fillColorBuffers(render::ShaderProgram& p) override; 78 | }; 79 | 80 | template 81 | SurfaceVertexProjectiveSphericalParameterizationQuantity* 82 | addProjectiveSphericalParameterizationQuantity(SurfaceMesh& mesh, 83 | std::string name, const T& data); 84 | 85 | } // namespace polyscope 86 | 87 | #include "SurfaceProjectiveSphericalParameterizationQuantity.ipp" 88 | -------------------------------------------------------------------------------- /src/SphericalUniformization/polyscope/SurfaceProjectiveSphericalParameterizationQuantity.ipp: -------------------------------------------------------------------------------- 1 | namespace polyscope { 2 | 3 | template 4 | SurfaceVertexProjectiveSphericalParameterizationQuantity* 5 | addProjectiveSphericalParameterizationQuantity(SurfaceMesh& mesh, 6 | std::string name, 7 | const T& data) { 8 | 9 | SurfaceVertexProjectiveSphericalParameterizationQuantity* q = 10 | new SurfaceVertexProjectiveSphericalParameterizationQuantity( 11 | name, 12 | applyPermutation(standardizeVectorArray(data), 13 | mesh.vertexPerm), 14 | mesh); 15 | mesh.addQuantity(q); 16 | return q; 17 | } 18 | } // namespace polyscope 19 | -------------------------------------------------------------------------------- /src/SphericalUniformization/polyscope/bindata.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace polyscope { 6 | namespace gl { 7 | 8 | extern const std::array bindata_map_equirectangular; 9 | 10 | } // namespace gl 11 | } // namespace polyscope 12 | -------------------------------------------------------------------------------- /src/Tracing.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "Triangulation.h" 3 | #include "Utils.h" 4 | 5 | #include 6 | 7 | namespace CEPS { 8 | 9 | // Represents a point along a directed mesh edge, represented using homogeneous 10 | // (i.e. unnormalized) barycentric coordinates 11 | struct HalfedgePt { 12 | Halfedge halfedge; 13 | Vector2 bary; 14 | }; 15 | 16 | // Represents a point at a mesh vertex. (Due to some Delta-complex details, it 17 | // is useful to store a corner, rather than just a vertex, associated with the 18 | // point). 19 | // Ordinarily points at vertices have trivial barycentric coordinates of 1 20 | // associated with the vertex. But since we use unnormalized barycentric 21 | // coordinates, we actually have a nontrivial barycentric coordinate value to 22 | // store. 23 | struct CornerPt { 24 | Corner corner; 25 | double bary; 26 | }; 27 | 28 | // Represent a path along a mesh. The path emanates from the corner start, 29 | // crosses mesh edges at locations in the points list, and terminates at the 30 | // corner end. 31 | // The crossings in points are given in order. 32 | // baryCoords stores the normalized barycentric coordinates of each crossing 33 | // along the path itself (start and end always have barycentric coordinates of 0 34 | // and 1; they are not stored in the baryCoords list) 35 | class MeshPath { 36 | public: 37 | std::vector points; 38 | std::vector baryCoords; 39 | 40 | CornerPt start, end; 41 | }; 42 | 43 | // Topologically trace edge e1 of T1 over T2 44 | MeshPath traceEdge(Edge e1, Triangulation& T1, Triangulation& T2); 45 | 46 | // Trace T1 over T2 47 | EdgeData traceTopologicalTriangulation(Triangulation& T1, 48 | Triangulation& T2, 49 | bool verbose = false); 50 | 51 | // Trace T1 over T2 52 | EdgeData traceGeodesicTriangulation(Triangulation& T1, 53 | Triangulation& T2, 54 | GeometryType gType, 55 | bool verbose = false); 56 | 57 | // Trace T1 over T2, doing as much computation on T1 as possible 58 | // This turns out to be an easier problem in many instances 59 | EdgeData traceTransposedGeodesicTriangulation(Triangulation& T1, 60 | Triangulation& T2, 61 | GeometryType gType, 62 | bool verbose = false); 63 | 64 | // Straighten Euclidean geodesic over T 65 | void straightenEuclidean(MeshPath& curve, Triangulation& T); 66 | 67 | // Straighten Hyperbolic geodesic over T, according to given scale factors 68 | // return false if straightening fails 69 | bool straightenHyperbolic(MeshPath& curve, Triangulation& T, 70 | const VertexData& logScaleFactors); 71 | 72 | // Straighten Hyperbolic geodesic over T, according to given scale factors 73 | // return false if straightening fails 74 | bool straightenHyperbolicIteratively(MeshPath& curve, Triangulation& T, 75 | const VertexData& logScaleFactors, 76 | double tol = 1e-10); 77 | 78 | namespace ImplementationDetails { 79 | 80 | // Flip the MeshPath to go in the other direction 81 | MeshPath twin(MeshPath path); 82 | 83 | // Return an equivalent HalfedgePt where he = he.edge().halfedge() 84 | HalfedgePt canonicalize(HalfedgePt hPt); 85 | 86 | // Return the corresponding HalfedgePt on he.edge()'s other halfedge 87 | HalfedgePt twin(HalfedgePt hPt); 88 | 89 | // Take in edges of T1 traced over T2 and return edges of T2 traced over T1 90 | EdgeData transpose(const EdgeData& paths, Triangulation& T1, 91 | Triangulation& T2); 92 | 93 | bool normalCoordinateTriangleInequalityViolation(Face f, Halfedge& violatingHe, 94 | const EdgeData& n); 95 | 96 | // He is the twin of the halfedge in a face that the curve starts from, 97 | // index is the index of the curve, with 1 being closest to the source 98 | // vertex of he. The returned path starts on He, and ends at some vertex 99 | // WARNING: 1-indexed 100 | MeshPath traceTopologicalCurve(Halfedge he, size_t index, 101 | const EdgeData& n); 102 | 103 | std::tuple nextEdge(Halfedge he, size_t p, 104 | const EdgeData& n); 105 | 106 | 107 | /* Computes the Lorentz metric on R3, IE 108 | * v.x * w.x + v.y * w.y - v.z * w.z 109 | */ 110 | double lorentz(Vector3 v, Vector3 w); 111 | 112 | /* Returns lorentz(v, v) 113 | */ 114 | double lorentzNorm2(Vector3 v); 115 | 116 | /* Computes the squared distance between v and w in the Lorentz metric 117 | */ 118 | double lorentzDist2(Vector3 v, Vector3 w); 119 | 120 | /* Computes the distance between v and w in the Lorentz metric 121 | */ 122 | double lorentzDist(Vector3 v, Vector3 w); 123 | 124 | std::tuple computeLightConeCoords(double u, double v, 125 | double d); 126 | 127 | /* Takes in lightlike vectors v1, v2, v3 and distance l1, l2, l3. Computes a 128 | * point v4 on the light cone so that the (Minsk) distance to v1 is l1, etc 129 | */ 130 | Vector3 placeFourthLightConePoint(Vector3 v1, Vector3 v2, Vector3 v3, double l1, 131 | double l2, double l3); 132 | 133 | 134 | // Returns the intersection (v.x, v.y) of the projective line a1-a2 with 135 | // projective line b1-b2 in barycentric coordinates along line a1-a2 (i.e. 136 | // v.x * a1 + v.y * a2 lies on line b1-b2 as well). In this version, a1, a2, b1 137 | // and b2 must be lightlike vectors 138 | Vector2 homogeneousProjectiveIntersectionTime(Vector3 a1, Vector3 a2, 139 | Vector3 b1, Vector3 b2); 140 | 141 | // Returns the intersection of the projective line a1-a2 with projective line 142 | // b1-b2 in barycentric coordinates along line a1-a2 (i.2. t*a1 + (1-t)a2 lies 143 | // on line b1-b2 as well (up to scalar multiplication)) 144 | // a1, a2 must be lightlike vectors 145 | double projectiveIntersectionTime(Vector3 a1, Vector3 a2, Vector3 b1, 146 | Vector3 b2); 147 | } // namespace ImplementationDetails 148 | } // namespace CEPS 149 | -------------------------------------------------------------------------------- /src/Triangulation.cpp: -------------------------------------------------------------------------------- 1 | #include "Triangulation.h" 2 | 3 | namespace CEPS { 4 | Triangulation::Triangulation(ManifoldSurfaceMesh& mesh_, 5 | VertexPositionGeometry& geo_) { 6 | std::tie(mesh, geo) = copyGeometry(mesh_, geo_); 7 | 8 | mollifyIntrinsic(*mesh, geo->inputEdgeLengths, 1e-12); 9 | 10 | originalEdgeLengths = geo->inputEdgeLengths; 11 | logScaleFactors = VertexData(*mesh, 0); 12 | normalCoordinates = EdgeData(*mesh, 0); 13 | initializeRoundabouts(); 14 | } 15 | 16 | Triangulation::Triangulation(const Triangulation& otherTri, bool clearFlips) { 17 | std::tie(mesh, geo) = copyGeometry(*otherTri.mesh, *otherTri.geo); 18 | 19 | originalEdgeLengths = otherTri.originalEdgeLengths.reinterpretTo(*mesh); 20 | logScaleFactors = otherTri.logScaleFactors.reinterpretTo(*mesh); 21 | if (clearFlips) { 22 | normalCoordinates = EdgeData(*mesh, 0); 23 | initializeRoundabouts(); 24 | } else { 25 | normalCoordinates = otherTri.normalCoordinates.reinterpretTo(*mesh); 26 | roundaboutIndices = otherTri.roundaboutIndices.reinterpretTo(*mesh); 27 | roundaboutDegrees = otherTri.roundaboutDegrees.reinterpretTo(*mesh); 28 | } 29 | } 30 | 31 | size_t Triangulation::flipToDelaunay(GeometryType gType) { 32 | std::deque edgesToCheck; 33 | EdgeData inQueue(*mesh, true); 34 | for (Edge e : mesh->edges()) { 35 | edgesToCheck.push_back(e); 36 | } 37 | 38 | size_t nFlips = 0; 39 | while (!edgesToCheck.empty()) { 40 | 41 | // Get the top element from the queue of possibily non-Delaunay 42 | // edges 43 | Edge e = edgesToCheck.front(); 44 | edgesToCheck.pop_front(); 45 | inQueue[e] = false; 46 | 47 | if (!isDelaunay(e)) { 48 | bool wasFlipped = flipEdge(e, gType); 49 | if (!wasFlipped) { 50 | std::cerr << "I should never want to do this, right?" << vendl; 51 | 52 | std::cout << e << vendl; 53 | std::cout << e.halfedge().face() << "\t" 54 | << e.halfedge().twin().face() << vendl; 55 | 56 | geo->requireCornerAngles(); 57 | std::cout << "corner angles: " << vendl; 58 | for (Corner c : e.halfedge().face().adjacentCorners()) 59 | std::cout << "\t" << geo->cornerAngles[c] << vendl; 60 | geo->unrequireCornerAngles(); 61 | std::cout << "Edge lengths: " << vendl; 62 | for (Halfedge he : e.halfedge().face().adjacentHalfedges()) 63 | std::cout << "\t" << geo->inputEdgeLengths[he.edge()] 64 | << vendl; 65 | 66 | continue; 67 | } 68 | 69 | // Handle the aftermath of a flip 70 | nFlips++; 71 | 72 | // Add neighbors to queue, as they may need flipping now 73 | std::vector neighboringEdges = { 74 | e.halfedge().next().edge(), e.halfedge().next().next().edge(), 75 | e.halfedge().twin().next().edge(), 76 | e.halfedge().twin().next().next().edge()}; 77 | 78 | for (Edge nE : neighboringEdges) { 79 | if (!inQueue[nE]) { 80 | edgesToCheck.emplace_back(nE); 81 | inQueue[nE] = true; 82 | } 83 | } 84 | } 85 | } 86 | 87 | updateEdgeLengths(); 88 | 89 | return nFlips; 90 | } 91 | 92 | void Triangulation::setScaleFactors(const VertexData& u) { 93 | logScaleFactors = u; 94 | updateEdgeLengths(); 95 | } 96 | 97 | double Triangulation::currentEdgeLength(Edge e) const { 98 | double u1 = logScaleFactors[src(e)]; 99 | double u2 = logScaleFactors[dst(e)]; 100 | return exp((u1 + u2) / 2.) * originalEdgeLengths[e]; 101 | } 102 | 103 | void Triangulation::updateEdgeLengths() { 104 | for (Edge e : mesh->edges()) { 105 | geo->inputEdgeLengths[e] = currentEdgeLength(e); 106 | } 107 | geo->refreshQuantities(); 108 | } 109 | 110 | bool Triangulation::incidentOnDegreeOneVertex(Edge e) const { 111 | // Get half edges of first face 112 | Halfedge ha1 = e.halfedge(); 113 | Halfedge ha2 = ha1.next(); 114 | 115 | // Get halfedges of second face 116 | Halfedge hb1 = ha1.twin(); 117 | Halfedge hb2 = hb1.next(); 118 | 119 | // Check whether incident on degree 1 vertex 120 | return ha2 == hb1 || hb2 == ha1; 121 | } 122 | 123 | bool Triangulation::isDelaunay(Edge e) const { 124 | if (incidentOnDegreeOneVertex(e)) return true; 125 | 126 | double q, flippedQ; 127 | std::tie(q, flippedQ) = delaunayFlipQuantity(e); 128 | return q >= flippedQ; 129 | } 130 | 131 | // Delaunay flip quantity (eq 9) for edge e, and flip(e) 132 | std::pair Triangulation::delaunayFlipQuantity(Edge e) const { 133 | return delaunayCondition(currentNeighborhoodEdgeLengths(e)); 134 | } 135 | 136 | // Rotates edge clockwise 137 | bool Triangulation::flipEdge(Edge e, GeometryType gType) { 138 | double newLength; 139 | switch (gType) { 140 | case GeometryType::EUCLIDEAN: 141 | newLength = euclideanFlippedEdgeLength(e); 142 | break; 143 | case GeometryType::HYPERBOLIC: 144 | newLength = ptolemyFlippedEdgeLength(e); 145 | break; 146 | } 147 | 148 | Halfedge he = e.halfedge(); 149 | 150 | size_t newNormalCoord = flippedNormalCoordinate(e); 151 | std::array newRoundabouts = 152 | flippedRoundabouts(he, newNormalCoord); 153 | 154 | bool preventSelfEdges = false; 155 | bool wasFlipped = mesh->flip(e, preventSelfEdges); 156 | 157 | if (wasFlipped) { 158 | originalEdgeLengths[e] = newLength; 159 | normalCoordinates[e] = newNormalCoord; 160 | 161 | roundaboutIndices[he] = newRoundabouts[0]; 162 | roundaboutIndices[he.twin()] = newRoundabouts[1]; 163 | } else { 164 | std::cerr << "Huh? why weren't you flipped?" << vendl; 165 | } 166 | 167 | return wasFlipped; 168 | } 169 | 170 | size_t Triangulation::flippedNormalCoordinate(Edge e) const { 171 | return flipNormalCoordinate(neighborhoodNormalCoordinates(e)); 172 | } 173 | 174 | std::array 175 | Triangulation::flippedRoundabouts(Halfedge he, 176 | size_t flippedNormalCoord) const { 177 | // Gather data needed for flip 178 | size_t nlk = flippedNormalCoord; 179 | 180 | size_t dk = roundaboutDegrees[he.next().next().vertex()]; 181 | size_t dl = roundaboutDegrees[he.twin().next().next().vertex()]; 182 | 183 | size_t rki = roundaboutIndices[he.next().next()]; 184 | size_t rlj = roundaboutIndices[he.twin().next().next()]; 185 | 186 | return flipRoundabout(neighborhoodNormalCoordinates(he), nlk, dk, dl, rki, 187 | rlj); 188 | } 189 | 190 | double Triangulation::euclideanFlippedEdgeLength(Edge e) const { 191 | double currentLen = flipEuclideanLength(currentNeighborhoodEdgeLengths(e)); 192 | 193 | double u1 = logScaleFactors[e.halfedge().next().next().vertex()]; 194 | double u2 = logScaleFactors[e.halfedge().twin().next().next().vertex()]; 195 | 196 | double originalLen = currentLen / exp(0.5 * (u1 + u2)); 197 | 198 | return originalLen; 199 | } 200 | 201 | double Triangulation::ptolemyFlippedEdgeLength(Edge e) const { 202 | return flipHyperbolicLength(originalNeighborhoodEdgeLengths(e)); 203 | } 204 | 205 | void Triangulation::initializeRoundabouts() { 206 | roundaboutIndices = HalfedgeData(*mesh); 207 | roundaboutDegrees = VertexData(*mesh); 208 | for (Vertex v : mesh->vertices()) { 209 | size_t iHe = 0; 210 | size_t D = v.degree(); 211 | roundaboutDegrees[v] = D; 212 | 213 | // Explicitly loop over halfedges in counterclockwise order 214 | Halfedge he = v.halfedge(); 215 | do { 216 | roundaboutIndices[he] = iHe; 217 | 218 | iHe = (iHe + 1) % D; 219 | he = he.next().next().twin(); 220 | } while (he != v.halfedge()); 221 | } 222 | } 223 | 224 | // Helper functions to gather the data needed for all the miscellaneous flip 225 | // formulas 226 | std::array 227 | Triangulation::currentNeighborhoodEdgeLengths(Halfedge he) const { 228 | // Gather current lengths 229 | double lij = currentEdgeLength(he.edge()); 230 | double ljk = currentEdgeLength(he.next().edge()); 231 | double lki = currentEdgeLength(he.next().next().edge()); 232 | double lil = currentEdgeLength(he.twin().next().edge()); 233 | double llj = currentEdgeLength(he.twin().next().next().edge()); 234 | return {lij, ljk, lki, lil, llj}; 235 | } 236 | 237 | std::array 238 | Triangulation::currentNeighborhoodEdgeLengths(Edge e) const { 239 | return currentNeighborhoodEdgeLengths(e.halfedge()); 240 | } 241 | 242 | std::array 243 | Triangulation::originalNeighborhoodEdgeLengths(Halfedge he) const { 244 | // Gather original lengths 245 | double lij = originalEdgeLengths[he.edge()]; 246 | double ljk = originalEdgeLengths[he.next().edge()]; 247 | double lki = originalEdgeLengths[he.next().next().edge()]; 248 | double lil = originalEdgeLengths[he.twin().next().edge()]; 249 | double llj = originalEdgeLengths[he.twin().next().next().edge()]; 250 | return {lij, ljk, lki, lil, llj}; 251 | } 252 | 253 | std::array 254 | Triangulation::originalNeighborhoodEdgeLengths(Edge e) const { 255 | return originalNeighborhoodEdgeLengths(e.halfedge()); 256 | } 257 | 258 | std::array 259 | Triangulation::neighborhoodNormalCoordinates(Halfedge he) const { 260 | // Gather normal coordinates 261 | size_t nij = normalCoordinates[he.edge()]; 262 | size_t njk = normalCoordinates[he.next().edge()]; 263 | size_t nki = normalCoordinates[he.next().next().edge()]; 264 | size_t nil = normalCoordinates[he.twin().next().edge()]; 265 | size_t nlj = normalCoordinates[he.twin().next().next().edge()]; 266 | return {nij, njk, nki, nil, nlj}; 267 | } 268 | 269 | std::array 270 | Triangulation::neighborhoodNormalCoordinates(Edge e) const { 271 | return neighborhoodNormalCoordinates(e.halfedge()); 272 | } 273 | } // namespace CEPS 274 | -------------------------------------------------------------------------------- /src/Triangulation.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | #include "geometrycentral/surface/edge_length_geometry.h" 5 | #include "geometrycentral/surface/intrinsic_mollification.h" 6 | #include "geometrycentral/surface/manifold_surface_mesh.h" 7 | #include "geometrycentral/surface/vertex_position_geometry.h" 8 | 9 | #include "geometrycentral/utilities/elementary_geometry.h" // placeTriangleVertex 10 | 11 | #include "FlipFormulas.h" 12 | #include "GeometryUtils.h" 13 | #include "Utils.h" 14 | 15 | using namespace geometrycentral; 16 | using namespace geometrycentral::surface; 17 | 18 | namespace CEPS { 19 | 20 | class Triangulation { 21 | public: 22 | Triangulation(ManifoldSurfaceMesh& mesh, VertexPositionGeometry& geo); 23 | Triangulation(const Triangulation& otherTri, bool clearFlips = false); 24 | 25 | // connectivity 26 | std::unique_ptr mesh; 27 | // geometry of rescaled edge lengths 28 | std::unique_ptr geo; 29 | 30 | // Flip to intrinsic Delaunay if gType is EUCLIDEAN and ideal Delaunay if 31 | // gType is HYPERBOLIC 32 | size_t flipToDelaunay(GeometryType gType); 33 | 34 | void setScaleFactors(const VertexData& u); 35 | 36 | VertexData logScaleFactors; 37 | EdgeData originalEdgeLengths; 38 | EdgeData normalCoordinates; 39 | HalfedgeData roundaboutIndices; 40 | VertexData roundaboutDegrees; 41 | 42 | double currentEdgeLength(Edge e) const; 43 | void updateEdgeLengths(); 44 | 45 | // Updates mesh, normal coordinates, roundabouts, and edge lengths 46 | // Uses Euclidean formula if gType is EUCLIDEAN and Ptolemy formula if gType 47 | // is HYPERBOLIC 48 | // Returns true if edge was flipped, false if edge was unflippable 49 | bool flipEdge(Edge e, GeometryType gType); 50 | 51 | bool incidentOnDegreeOneVertex(Edge e) const; 52 | 53 | bool isDelaunay(Edge e) const; 54 | 55 | // Delaunay flip quantity (eq 9) for edge e, and flip(e) 56 | std::pair delaunayFlipQuantity(Edge e) const; 57 | 58 | double euclideanFlippedEdgeLength(Edge e) const; 59 | double ptolemyFlippedEdgeLength(Edge e) const; 60 | size_t flippedNormalCoordinate(Edge e) const; 61 | std::array flippedRoundabouts(Halfedge he, 62 | size_t flippedNormalCoord) const; 63 | 64 | // Helper functions to gather the data needed for all the miscellaneous flip 65 | // formulas 66 | std::array currentNeighborhoodEdgeLengths(Halfedge he) const; 67 | std::array currentNeighborhoodEdgeLengths(Edge e) const; 68 | std::array originalNeighborhoodEdgeLengths(Halfedge he) const; 69 | std::array originalNeighborhoodEdgeLengths(Edge e) const; 70 | std::array neighborhoodNormalCoordinates(Halfedge he) const; 71 | std::array neighborhoodNormalCoordinates(Edge e) const; 72 | 73 | void initializeRoundabouts(); 74 | }; 75 | } // namespace CEPS 76 | -------------------------------------------------------------------------------- /src/Utils.cpp: -------------------------------------------------------------------------------- 1 | #include "Utils.h" 2 | 3 | verbose_runtime_error::verbose_runtime_error(const std::string& arg, 4 | const char* file, int line) 5 | : std::runtime_error(arg) { 6 | 7 | std::string filePath(file); 8 | 9 | // stolen from polyscope/utilities.cpp 10 | size_t startInd = 0; 11 | for (std::string sep : {"/", "\\"}) { 12 | size_t pos = filePath.rfind(sep); 13 | if (pos != std::string::npos) { 14 | startInd = std::max(startInd, pos + 1); 15 | } 16 | } 17 | 18 | std::string niceName = filePath.substr(startInd, filePath.size()); 19 | 20 | std::ostringstream o; 21 | o << arg << " At " << niceName << ":" << line; 22 | msg = o.str(); 23 | } 24 | 25 | namespace CEPS { 26 | Vertex src(Edge e) { return e.halfedge().vertex(); } 27 | Vertex dst(Edge e) { return e.halfedge().next().vertex(); } 28 | 29 | int positivePart(int x) { return fmax(x, 0); } 30 | int negativePart(int x) { return fmin(x, 0); } 31 | 32 | Vector2 bary2(double t) { 33 | if (t <= 0) { 34 | return Vector2{0, 1}; 35 | } else if (t >= 1) { 36 | return Vector2{1, 0}; 37 | } else { 38 | return Vector2{t, 1 - t}; 39 | } 40 | } 41 | 42 | Vector2 normalizeBary(Vector2 b) { return b / (b.x + b.y); } 43 | Vector3 normalizeBary(Vector3 b) { return b / (b.x + b.y + b.z); } 44 | 45 | // Returns the intersection of the line a1-a2 with line b1-b2 in barycentric 46 | // coordinates along line a1-a2 (i.2. t*a1 + (1-t)a2 lies on line b1-b2 as 47 | // well) 48 | double intersectionTime(Vector2 a1, Vector2 a2, Vector2 b1, Vector2 b2) { 49 | // TODO: are these checks time-consuming? 50 | if ((a1 - b1).norm() < 1e-12 || (a1 - b2).norm() < 1e-12) { 51 | return 1; 52 | } else if ((a2 - b1).norm() < 1e-12 || (a2 - b2).norm() < 1e-12) { 53 | return 0; 54 | } else if ((a1 - a2).norm() < 1e-12 && (b1 - b2).norm() < 1e-12) { 55 | // If a and b coincided, we would have caught that in a previous case. 56 | // So the "lines" don't intersect, and we just return a big number 57 | return 50; 58 | } else if ((b1 - b2).norm() < 1e-12) { 59 | Vector2 dA = (a2 - a1).normalize(); 60 | return 1 - dot(b1 - a1, dA) / dot(a2 - a1, dA); 61 | } else if ((a1 - a2).norm() < 1e-12) { 62 | Vector2 dB = (b2 - b1).normalize(); 63 | double t = dot(a1 - b1, dB) / dot(b2 - b1, dB); 64 | 65 | if (0 <= t && t <= 1) { 66 | Vector2 projAB = b1 + t * dB; 67 | if ((projAB - a1).norm() < 1e-12) { 68 | return 0.5; 69 | } else { 70 | return 50; 71 | } 72 | } else { 73 | return 50; 74 | } 75 | 76 | } else { 77 | double m1 = a2.x - a1.x; 78 | double m2 = b2.x - b1.x; 79 | double m3 = a2.y - a1.y; 80 | double m4 = b2.y - b1.y; 81 | double vx = b1.x - a1.x; 82 | double vy = b1.y - a1.y; 83 | double t = (m2 * vy - m4 * vx) / (m2 * m3 - m1 * m4); 84 | 85 | return 1 - t; 86 | } 87 | } 88 | 89 | // Returns the barycentric coordinate for pt along line a-b 90 | // i.e. minimizes \|b + (a-b)t - pt\|_2^2 91 | // i.e pt = ta + (1-t)b 92 | // Clamps t to the interval [0, 1] 93 | double barycentricCoordinate(Vector2 pt, Vector2 a, Vector2 b) { 94 | double t = dot(b - a, b - pt) / norm2(b - a); 95 | if (t >= 0 && t <= 1) { 96 | return t; 97 | } else { 98 | if (t < -1e-12 || t > 1 + 1e-12) { 99 | // cerr << "error Barycentric coordinate " << t 100 | // << " invalid [barcentricCoordinate(Vector2 ...)]" << 101 | // endl; 102 | } 103 | if (t < 0) { 104 | return 0; 105 | } else { 106 | return 1; 107 | } 108 | } 109 | } 110 | 111 | double lobachevsky(double a) { return 0.5 * clausen(2 * a); } 112 | double safeSqrt(double x) { return sqrt(fmax(x, 0)); } 113 | 114 | void symmetrizeVizRange(polyscope::SurfaceVertexIsolatedScalarQuantity& q) { 115 | q.vizRangeLow = fmin(q.dataRangeLow, -q.dataRangeHigh); 116 | q.vizRangeHigh = fmax(q.dataRangeHigh, -q.dataRangeLow); 117 | } 118 | } // namespace CEPS 119 | 120 | 121 | string getFilename(string filePath, bool withExtension, char seperator) { 122 | // Get last dot position 123 | size_t dotPos = filePath.rfind('.'); 124 | size_t sepPos = filePath.rfind(seperator); 125 | 126 | if (sepPos != string::npos) { 127 | return filePath.substr( 128 | sepPos + 1, 129 | filePath.size() - 130 | (withExtension || dotPos != std::string::npos ? 1 : dotPos)); 131 | } 132 | return ""; 133 | } 134 | -------------------------------------------------------------------------------- /src/Utils.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "geometrycentral/surface/edge_length_geometry.h" 4 | #include "geometrycentral/surface/manifold_surface_mesh.h" 5 | #include "geometrycentral/surface/vertex_position_geometry.h" 6 | 7 | #include "polyscope/polyscope.h" 8 | #include "polyscope/surface_count_quantity.h" 9 | 10 | #include "Clausen.h" 11 | 12 | #include 13 | #include 14 | #include 15 | 16 | using std::string; 17 | 18 | using namespace geometrycentral; 19 | using namespace geometrycentral::surface; 20 | 21 | // Custom runtime error which will report the offending file and line number 22 | class verbose_runtime_error : public std::runtime_error { 23 | std::string msg; 24 | 25 | public: 26 | verbose_runtime_error(const std::string& arg, const char* file, int line); 27 | ~verbose_runtime_error() throw() {} 28 | const char* what() const throw() { return msg.c_str(); } 29 | }; 30 | 31 | #define throw_verbose_runtime_error(arg) \ 32 | throw verbose_runtime_error(arg, __FILE__, __LINE__); 33 | 34 | #define verbose_assert(arg, msg) \ 35 | if (!(arg)) throw_verbose_runtime_error(msg); 36 | 37 | 38 | namespace CEPS { 39 | enum class GeometryType { EUCLIDEAN, HYPERBOLIC }; 40 | Vertex src(Edge e); 41 | Vertex dst(Edge e); 42 | 43 | int positivePart(int x); 44 | int negativePart(int x); 45 | 46 | Vector2 bary2(double t); 47 | 48 | Vector2 normalizeBary(Vector2 b); 49 | Vector3 normalizeBary(Vector3 b); 50 | 51 | // Returns the intersection of the line a1-a2 with line b1-b2 in barycentric 52 | // coordinates along line a1-a2 (i.2. t*a1 + (1-t)a2 lies on line b1-b2 as 53 | // well) 54 | double intersectionTime(Vector2 a1, Vector2 a2, Vector2 b1, Vector2 b2); 55 | 56 | // Returns the barycentric coordinate for pt along line a-b 57 | // i.e. minimizes \|b + (a-b)t - pt\|_2^2 58 | // i.e pt = ta + (1-t)b 59 | // Clamps t to the interval [0, 1] 60 | double barycentricCoordinate(Vector2 pt, Vector2 a, Vector2 b); 61 | 62 | double lobachevsky(double a); 63 | double safeSqrt(double x); 64 | 65 | void symmetrizeVizRange(polyscope::SurfaceVertexIsolatedScalarQuantity& q); 66 | 67 | } // namespace CEPS 68 | 69 | //== Convenient printing helpers 70 | 71 | string getFilename(string filePath, bool withExtension = true, 72 | char seperator = '/'); 73 | 74 | // Verbose endl 75 | #define vendl \ 76 | "\t\t(" << getFilename(__FILE__) << ":" << __LINE__ << ")" << std::endl 77 | 78 | // == Horrible macro to print variable name + variable 79 | // https://stackoverflow.com/a/6623090 80 | #define WATCHSTR_WNAME(os, name, a) \ 81 | do { \ 82 | (os) << (name) << " is value " << (a) << vendl; \ 83 | } while (false) 84 | 85 | #define WATCHSTR(os, a) WATCHSTR_WNAME((os), #a, (a)) 86 | #define WATCH(a) WATCHSTR_WNAME(std::cout, #a, (a)) 87 | #define WATCH2(a, b) \ 88 | WATCH(a); \ 89 | WATCH(b) 90 | #define WATCH3(a, b, c) \ 91 | WATCH(a); \ 92 | WATCH2(b, c) 93 | #define WATCH4(a, b, c, d) \ 94 | WATCH(a); \ 95 | WATCH3(b, c, d) 96 | #define WATCH5(a, b, c, d, e) \ 97 | WATCH(a); \ 98 | WATCH4(b, c, d, e) 99 | 100 | #define HERE() \ 101 | do { \ 102 | std::cout << "HERE at " << __LINE__ << " in " << getFilename(__FILE__) \ 103 | << ":" << __FUNCTION__ << std::endl; \ 104 | } while (false) 105 | #define HERE1(name) \ 106 | do { \ 107 | std::cout << "HERE (" << (name) << ") at " << __LINE__ << " in " \ 108 | << getFilename(__FILE__) << ":" << __FUNCTION__ \ 109 | << std::endl; \ 110 | } while (false) 111 | 112 | namespace CEPS { 113 | namespace ImplementationDetails { 114 | 115 | template 116 | std::ostream& operator<<(std::ostream& out, const std::vector& data); 117 | 118 | template 119 | std::ostream& operator<<(std::ostream& out, const std::array& data); 120 | 121 | template 122 | std::ostream& operator<<(std::ostream& out, const std::pair& data); 123 | 124 | } // namespace ImplementationDetails 125 | } // namespace CEPS 126 | 127 | 128 | #include "Utils.ipp" 129 | -------------------------------------------------------------------------------- /src/Utils.ipp: -------------------------------------------------------------------------------- 1 | namespace CEPS { 2 | namespace ImplementationDetails { 3 | 4 | template 5 | std::ostream& operator<<(std::ostream& out, const std::vector& data) { 6 | out << "{"; 7 | for (size_t i = 0; i < fmin(data.size(), 5); ++i) { 8 | out << data[i]; 9 | if (i + 1 < data.size()) out << ", "; 10 | } 11 | if (data.size() >= 5) out << "... (length " << data.size() << ")"; 12 | out << "}"; 13 | return out; 14 | } 15 | 16 | template 17 | std::ostream& operator<<(std::ostream& out, const std::array& data) { 18 | out << "{"; 19 | for (size_t i = 0; i < fmin(N, 5); ++i) { 20 | out << data[i]; 21 | if (i + 1 < N) out << ", "; 22 | } 23 | if (N >= 5) out << "... (length " << N << ")"; 24 | out << "}"; 25 | return out; 26 | } 27 | 28 | template 29 | std::ostream& operator<<(std::ostream& out, const std::pair& data) { 30 | out << "(" << std::get<0>(data) << ", " << std::get<1>(data) << ")"; 31 | return out; 32 | } 33 | 34 | } // namespace ImplementationDetails 35 | } // namespace CEPS 36 | -------------------------------------------------------------------------------- /src/polyscope/SurfaceProjectiveParameterizationQuantity.cpp: -------------------------------------------------------------------------------- 1 | #include "SurfaceProjectiveParameterizationQuantity.h" 2 | 3 | #include "polyscope/file_helpers.h" 4 | #include "polyscope/polyscope.h" 5 | #include "polyscope/render/engine.h" 6 | 7 | #include "imgui.h" 8 | 9 | using std::cout; 10 | using std::endl; 11 | 12 | namespace polyscope { 13 | 14 | // == Custom shader rule for projective texture interpolation 15 | const render::ShaderReplacementRule MESH_PROPAGATE_PROJECTIVE_VALUE2( 16 | /* rule name */ "MESH_PROPAGATE_PROJECTIVE_VALUE2", 17 | { 18 | /* replacement sources */ 19 | {"VERT_DECLARATIONS", R"( 20 | in vec3 a_value3; 21 | out vec3 a_value3ToFrag; 22 | )"}, 23 | {"VERT_ASSIGNMENTS", R"( 24 | a_value3ToFrag = a_value3; 25 | )"}, 26 | {"FRAG_DECLARATIONS", R"( 27 | in vec3 a_value3ToFrag; 28 | )"}, 29 | {"GENERATE_SHADE_VALUE", R"( 30 | vec2 shadeValue2 = a_value3ToFrag.xy / a_value3ToFrag.z; 31 | )"}, 32 | }, 33 | /* uniforms */ {}, 34 | /* attributes */ 35 | { 36 | {"a_value3", render::DataType::Vector3Float}, 37 | }, 38 | /* textures */ {}); 39 | 40 | // ============================================================== 41 | // ================ Base Parameterization ===================== 42 | // ============================================================== 43 | 44 | SurfaceProjectiveParameterizationQuantity:: 45 | SurfaceProjectiveParameterizationQuantity(std::string name, 46 | ParamCoordsType type_, 47 | ParamVizStyle style_, 48 | SurfaceMesh& mesh_) 49 | : SurfaceParameterizationQuantity(name, type_, style_, mesh_) 50 | 51 | { 52 | // Register a custom shader rule for projective interpolation 53 | render::engine->registerShaderRule("MESH_PROPAGATE_PROJECTIVE_VALUE2", 54 | MESH_PROPAGATE_PROJECTIVE_VALUE2); 55 | } 56 | 57 | 58 | void SurfaceProjectiveParameterizationQuantity::draw() { 59 | if (!isEnabled()) return; 60 | 61 | if (program == nullptr) { 62 | createProgram(); 63 | } 64 | 65 | // Set uniforms 66 | parent.setTransformUniforms(*program); 67 | setProgramUniforms(*program); 68 | parent.setStructureUniforms(*program); 69 | 70 | program->draw(); 71 | } 72 | 73 | void SurfaceProjectiveParameterizationQuantity::createProgram() { 74 | // Create the program to draw this quantity 75 | 76 | switch (getStyle()) { 77 | case ParamVizStyle::CHECKER: 78 | program = render::engine->requestShader( 79 | "MESH", 80 | parent.addStructureRules( 81 | {"MESH_PROPAGATE_PROJECTIVE_VALUE2", "SHADE_CHECKER_VALUE2"})); 82 | break; 83 | case ParamVizStyle::GRID: 84 | program = render::engine->requestShader( 85 | "MESH", 86 | parent.addStructureRules( 87 | {"MESH_PROPAGATE_PROJECTIVE_VALUE2", "SHADE_GRID_VALUE2"})); 88 | break; 89 | case ParamVizStyle::LOCAL_CHECK: 90 | program = render::engine->requestShader( 91 | "MESH", parent.addStructureRules( 92 | {"MESH_PROPAGATE_PROJECTIVE_VALUE2", 93 | "SHADE_COLORMAP_ANGULAR2", "CHECKER_VALUE2COLOR"})); 94 | program->setTextureFromColormap("t_colormap", cMap.get()); 95 | break; 96 | case ParamVizStyle::LOCAL_RAD: 97 | program = render::engine->requestShader( 98 | "MESH", 99 | parent.addStructureRules( 100 | {"MESH_PROPAGATE_PROJECTIVE_VALUE2", "SHADE_COLORMAP_ANGULAR2", 101 | "SHADEVALUE_MAG_VALUE2", "ISOLINE_STRIPE_VALUECOLOR"})); 102 | program->setTextureFromColormap("t_colormap", cMap.get()); 103 | break; 104 | } 105 | 106 | // Fill color buffers 107 | fillColorBuffers(*program); 108 | parent.fillGeometryBuffers(*program); 109 | 110 | render::engine->setMaterial(*program, parent.getMaterial()); 111 | } 112 | 113 | // ============================================================== 114 | // ============ Corner Projective Parameterization ============ 115 | // ============================================================== 116 | 117 | 118 | SurfaceCornerProjectiveParameterizationQuantity:: 119 | SurfaceCornerProjectiveParameterizationQuantity( 120 | std::string name, std::vector coords_, ParamCoordsType type_, 121 | ParamVizStyle style_, SurfaceMesh& mesh_) 122 | : SurfaceProjectiveParameterizationQuantity(name, type_, style_, mesh_), 123 | coords(std::move(coords_)) {} 124 | 125 | std::string SurfaceCornerProjectiveParameterizationQuantity::niceName() { 126 | return name + " (corner projective parameterization)"; 127 | } 128 | 129 | 130 | void SurfaceCornerProjectiveParameterizationQuantity::fillColorBuffers( 131 | render::ShaderProgram& p) { 132 | std::vector coordVal; 133 | coordVal.reserve(3 * parent.nFacesTriangulation()); 134 | 135 | size_t cornerCount = 0; 136 | for (size_t iF = 0; iF < parent.nFaces(); iF++) { 137 | auto& face = parent.faces[iF]; 138 | size_t D = face.size(); 139 | 140 | // implicitly triangulate from root 141 | size_t cRoot = cornerCount; 142 | for (size_t j = 1; (j + 1) < D; j++) { 143 | size_t cB = cornerCount + j; 144 | size_t cC = cornerCount + ((j + 1) % D); 145 | 146 | coordVal.push_back(coords[cRoot]); 147 | coordVal.push_back(coords[cB]); 148 | coordVal.push_back(coords[cC]); 149 | } 150 | 151 | cornerCount += D; 152 | } 153 | 154 | // Store data in buffers 155 | p.setAttribute("a_value3", coordVal); 156 | } 157 | 158 | void SurfaceCornerProjectiveParameterizationQuantity::buildHalfedgeInfoGUI( 159 | size_t heInd) { 160 | ImGui::TextUnformatted(name.c_str()); 161 | ImGui::NextColumn(); 162 | ImGui::Text("<%g,%g,%g>", coords[heInd].x, coords[heInd].y, 163 | coords[heInd].z); 164 | ImGui::NextColumn(); 165 | } 166 | 167 | } // namespace polyscope 168 | -------------------------------------------------------------------------------- /src/polyscope/SurfaceProjectiveParameterizationQuantity.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "polyscope/affine_remapper.h" 4 | #include "polyscope/histogram.h" 5 | #include "polyscope/render/color_maps.h" 6 | #include "polyscope/render/engine.h" 7 | #include "polyscope/surface_mesh.h" 8 | #include "polyscope/surface_parameterization_enums.h" 9 | 10 | 11 | namespace polyscope { 12 | 13 | 14 | // ============================================================== 15 | // ================ Base Parameterization ===================== 16 | // ============================================================== 17 | 18 | 19 | class SurfaceProjectiveParameterizationQuantity 20 | : public SurfaceParameterizationQuantity { 21 | 22 | public: 23 | SurfaceProjectiveParameterizationQuantity(std::string name, 24 | ParamCoordsType type_, 25 | ParamVizStyle style, 26 | SurfaceMesh& mesh_); 27 | 28 | void draw() override; 29 | 30 | protected: 31 | // Helpers 32 | void createProgram(); 33 | }; 34 | 35 | 36 | // ============================================================== 37 | // =============== Corner Parameterization ==================== 38 | // ============================================================== 39 | 40 | class SurfaceCornerProjectiveParameterizationQuantity 41 | : public SurfaceProjectiveParameterizationQuantity { 42 | 43 | public: 44 | SurfaceCornerProjectiveParameterizationQuantity( 45 | std::string name, std::vector values_, ParamCoordsType type_, 46 | ParamVizStyle style, SurfaceMesh& mesh_); 47 | 48 | virtual void buildHalfedgeInfoGUI(size_t heInd) override; 49 | virtual std::string niceName() override; 50 | 51 | // === Members 52 | std::vector coords; // on corners 53 | 54 | protected: 55 | virtual void fillColorBuffers(render::ShaderProgram& p) override; 56 | }; 57 | 58 | template 59 | SurfaceCornerProjectiveParameterizationQuantity* 60 | addProjectiveParameterizationQuantity( 61 | SurfaceMesh& mesh, std::string name, const T& data, 62 | ParamCoordsType type = ParamCoordsType::UNIT); 63 | 64 | } // namespace polyscope 65 | 66 | #include "SurfaceProjectiveParameterizationQuantity.ipp" 67 | -------------------------------------------------------------------------------- /src/polyscope/SurfaceProjectiveParameterizationQuantity.ipp: -------------------------------------------------------------------------------- 1 | namespace polyscope { 2 | 3 | template 4 | SurfaceCornerProjectiveParameterizationQuantity* 5 | addProjectiveParameterizationQuantity(SurfaceMesh& mesh, std::string name, 6 | const T& data, ParamCoordsType type) { 7 | SurfaceCornerProjectiveParameterizationQuantity* q = 8 | new SurfaceCornerProjectiveParameterizationQuantity( 9 | name, 10 | applyPermutation(standardizeVectorArray(data), 11 | mesh.cornerPerm), 12 | type, ParamVizStyle::CHECKER, mesh); 13 | mesh.addQuantity(q); 14 | return q; 15 | } 16 | } // namespace polyscope 17 | -------------------------------------------------------------------------------- /src/testConeFlattening.cpp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarkGillespie/CEPS/b734d2cdc2afad7dc6afe9d609a108bc9a68c24f/src/testConeFlattening.cpp -------------------------------------------------------------------------------- /src/tests/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 2.8.9) 2 | 3 | # Maybe stop from CMAKEing in the wrong place 4 | if (CMAKE_BINARY_DIR STREQUAL CMAKE_SOURCE_DIR) 5 | message(FATAL_ERROR "Source and build directories cannot be the same. Go use the /build directory.") 6 | endif() 7 | 8 | # https://cliutils.gitlab.io/modern-cmake/chapters/testing/googletest.html 9 | add_subdirectory("${PROJECT_SOURCE_DIR}/deps/googletest" "deps/googletest") 10 | 11 | # "optional" 12 | mark_as_advanced( 13 | BUILD_GMOCK BUILD_GTEST BUILD_SHARED_LIBS 14 | gmock_build_tests gtest_build_samples gtest_build_tests 15 | gtest_disable_pthreads gtest_force_shared_crt gtest_hide_internal_symbols 16 | ) 17 | set_target_properties(gtest PROPERTIES FOLDER extern) 18 | set_target_properties(gtest_main PROPERTIES FOLDER extern) 19 | set_target_properties(gmock PROPERTIES FOLDER extern) 20 | set_target_properties(gmock_main PROPERTIES FOLDER extern) 21 | 22 | # This will allow you to quickly and simply add tests. Feel free to adjust to suit your 23 | # needs. If you haven't seen it before, ARGN is "every argument after the listed ones". 24 | # ADD YOUR LIBRARIES HERE 25 | macro(package_add_test TESTNAME) 26 | add_executable(${TESTNAME} ${ARGN}) 27 | target_link_libraries(${TESTNAME} gtest gmock gtest_main polyscope geometry-central CEPS) 28 | target_include_directories(${TESTNAME} PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/../../include" "${CMAKE_CURRENT_SOURCE_DIR}/../" "${CMAKE_CURRENT_SOURCE_DIR}") 29 | add_test(NAME ${TESTNAME} COMMAND ${TESTNAME}) 30 | set_target_properties(${TESTNAME} PROPERTIES FOLDER tests) 31 | endmacro() 32 | 33 | package_add_test(test test.cpp) 34 | -------------------------------------------------------------------------------- /src/tests/CommonRefinementTests.h: -------------------------------------------------------------------------------- 1 | #include "test_utils.h" 2 | 3 | #include "CommonRefinement.h" 4 | 5 | using namespace CEPS; 6 | using namespace CEPS::ImplementationDetails; 7 | 8 | class CommonRefinementTests : public testing::Test {}; 9 | 10 | TEST_F(CommonRefinementTests, doubleRefineTemplatingIsOkay) { 11 | 12 | std::array, 3> aLines{ 13 | std::vector{0, 0.2, 0.4, 1}, 14 | std::vector{0, 0.3, 0.4, 0.5, 0.6, 0.8, 0.9, 1}, 15 | std::vector{0, 0.1, 0.8, 1}}; 16 | std::array, 3> bLines{ 17 | std::vector{0, 0.3, 0.6, 0.9, 1}, 18 | std::vector{0, 0.1, 0.7, 1}, 19 | std::vector{0, 0.2, 0.4, 0.5, 1}, 20 | }; 21 | 22 | std::array>, 3> val1; 23 | std::array>, 3> val2; 24 | 25 | for (size_t iE = 0; iE < 3; ++iE) { 26 | val1[iE] = std::vector>(aLines[iE].size()); 27 | val2[iE] = std::vector>(bLines[iE].size()); 28 | } 29 | 30 | Vector2 vZero{0, 0}; 31 | std::vector longSideCornerVal1{vZero, vZero, vZero}; 32 | std::vector longSideCornerVal2{}; 33 | 34 | 35 | size_t nVertices; 36 | std::vector> polygons; 37 | std::vector> cornerVal1; 38 | std::vector> cornerVal2; 39 | std::tie(nVertices, polygons, cornerVal1, cornerVal2) = sliceTri( 40 | aLines, bLines, val1, val2, longSideCornerVal1, longSideCornerVal2); 41 | } 42 | 43 | TEST_F(CommonRefinementTests, splitTriangle) { 44 | 45 | std::array, 3> aLines{ 46 | std::vector{0, 0.2, 0.4, 1}, 47 | std::vector{0, 0.3, 0.4, 0.5, 0.6, 0.8, 0.9, 1}, 48 | std::vector{0, 0.1, 0.8, 1}}; 49 | std::array, 3> bLines{ 50 | std::vector{0, 0.3, 0.6, 0.9, 1}, 51 | std::vector{0, 0.1, 0.7, 1}, 52 | std::vector{0, 0.2, 0.4, 0.5, 1}, 53 | }; 54 | 55 | std::array>, 3> val1, val2; 56 | for (size_t i = 0; i < 3; ++i) { 57 | val1[i].reserve(aLines[i].size()); 58 | for (auto l : aLines[i]) { 59 | val1[i].push_back(std::array{0, 0}); 60 | } 61 | val2[i].reserve(bLines[i].size()); 62 | for (auto l : bLines[i]) { 63 | val2[i].push_back(std::array{0, 0}); 64 | } 65 | } 66 | 67 | std::vector longSideCornerVal1{0, 0, 0}; 68 | std::vector longSideCornerVal2{}; 69 | 70 | 71 | size_t nVertices; 72 | std::vector> polygons; 73 | std::vector> cornerVal1, cornerVal2; 74 | std::tie(nVertices, polygons, cornerVal1, cornerVal2) = sliceTri( 75 | aLines, bLines, val1, val2, longSideCornerVal1, longSideCornerVal2); 76 | 77 | // Computed with a working version of the function 78 | ASSERT_EQ(nVertices, 31); 79 | ASSERT_EQ(polygons.size(), 21); 80 | 81 | auto distinct = [](const std::vector& a, 82 | const std::vector& b) { 83 | if (a.size() != b.size()) { 84 | return true; 85 | } else { 86 | for (size_t i : a) { 87 | bool bContainsI = false; 88 | for (size_t j : b) { 89 | if (i == j) bContainsI = true; 90 | } 91 | if (!bContainsI) { 92 | return true; 93 | } 94 | } 95 | return false; 96 | } 97 | }; 98 | 99 | // Check that polygons are unique 100 | for (size_t iP = 0; iP < polygons.size(); ++iP) { 101 | std::vector p = polygons[iP]; 102 | for (size_t iQ = 0; iQ < iP; ++iQ) { 103 | std::vector q = polygons[iQ]; 104 | ASSERT_TRUE(distinct(p, q)); 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/tests/ExampleTests.h: -------------------------------------------------------------------------------- 1 | #include "test_utils.h" 2 | 3 | class ExampleTest : public ::testing::Test { 4 | public: 5 | static std::unique_ptr globalDouble; 6 | double localDouble; 7 | 8 | protected: 9 | static void SetUpTestSuite() { 10 | double *ten = new double(); 11 | *ten = 10; 12 | globalDouble = std::unique_ptr(ten); 13 | } 14 | 15 | void SetUp() override { localDouble = 10; } 16 | }; 17 | 18 | std::unique_ptr ExampleTest::globalDouble = nullptr; 19 | 20 | TEST_F(ExampleTest, ExampleTestCase) { 21 | EXPECT_DOUBLE_EQ(*globalDouble, localDouble); 22 | } 23 | -------------------------------------------------------------------------------- /src/tests/FlippingTests.h: -------------------------------------------------------------------------------- 1 | #include "test_utils.h" 2 | 3 | #include "NumericalNormalCoordinatesComputations.h" 4 | #include "Tracing.h" 5 | #include "Triangulation.h" 6 | #include "Utils.h" 7 | 8 | #include "geometrycentral/surface/meshio.h" 9 | 10 | #include "polyscope/curve_network.h" 11 | #include "polyscope/surface_mesh.h" 12 | 13 | using namespace CEPS; 14 | 15 | class FlippingTest : public ::testing::Test { 16 | public: 17 | static std::unique_ptr tri; 18 | static std::unique_ptr> vertexPositions; 19 | 20 | protected: 21 | static void SetUpTestSuite() { 22 | auto example = loadMesh("../src/tests/bunny_tiny.obj"); 23 | tri = std::make_unique(*std::get<0>(example), 24 | *std::get<1>(example)); 25 | vertexPositions = std::make_unique>( 26 | *tri->mesh, std::get<1>(example)->inputVertexPositions.raw()); 27 | } 28 | }; 29 | 30 | std::unique_ptr FlippingTest::tri = nullptr; 31 | std::unique_ptr> FlippingTest::vertexPositions = nullptr; 32 | 33 | TEST_F(FlippingTest, FlippingTwiceJustReversesEdge) { 34 | Edge e = tri->mesh->edge(12); 35 | EdgeData oldEdgeLengths = tri->originalEdgeLengths; 36 | EdgeData oldNormalCoordinates = tri->normalCoordinates; 37 | HalfedgeData oldRoundabouts = tri->roundaboutIndices; 38 | 39 | for (int i = 0; i < 2; ++i) { 40 | tri->flipEdge(e, GeometryType::EUCLIDEAN); 41 | } 42 | 43 | // Reversing the edge orientation switched the roundabouts 44 | std::swap(oldRoundabouts[e.halfedge()], 45 | oldRoundabouts[e.halfedge().twin()]); 46 | 47 | EXPECT_MAT_NEAR(oldEdgeLengths.toVector().cast(), 48 | tri->originalEdgeLengths.toVector().cast(), 1e-8); 49 | EXPECT_MAT_EQ(oldNormalCoordinates.toVector().cast(), 50 | tri->normalCoordinates.toVector().cast()); 51 | EXPECT_MAT_EQ(oldRoundabouts.toVector().cast(), 52 | tri->roundaboutIndices.toVector().cast()); 53 | } 54 | 55 | TEST_F(FlippingTest, EuclideanFlipEdgeLength) { 56 | Vector2 qi{0, 0}; 57 | Vector2 qj{1, 0}; 58 | Vector2 qk{0.5, 1}; 59 | Vector2 ql{0.5, -1}; 60 | 61 | double lij = (qi - qj).norm(); 62 | double ljk = (qk - qj).norm(); 63 | double lki = (qi - qk).norm(); 64 | double lil = (ql - qi).norm(); 65 | double llj = (qj - ql).norm(); 66 | std::array lengths{lij, ljk, lki, lil, llj}; 67 | 68 | double flippedLen = flipEuclideanLength(lengths); 69 | 70 | EXPECT_DOUBLE_EQ(flippedLen, 2); 71 | } 72 | 73 | TEST_F(FlippingTest, RandomEuclideanFlipEdgeLength) { 74 | for (size_t i = 0; i < 10; ++i) { 75 | Vector2 qi{0, 0}; 76 | Vector2 qj{fRand(-1, 1), fRand(-1, 1)}; 77 | Vector2 qk{fRand(-1, 1), fRand(-1, 1)}; 78 | Vector2 ql{fRand(-1, 1), fRand(-1, 1)}; 79 | 80 | // Make sure ql is on the opposite side of the line qi-qj as qk is 81 | double c1 = cross(qj, qk); 82 | double c2 = cross(qj, ql); 83 | if (c1 * c2 > 0) { 84 | // Reflect ql across line qj 85 | // n is the normal vector to the line; 86 | Vector2 n{-qj.y, qj.x}; 87 | n = n.normalize(); 88 | 89 | ql = ql - 2 * dot(n, ql) * n; 90 | } 91 | 92 | double lij = (qi - qj).norm(); 93 | double ljk = (qk - qj).norm(); 94 | double lki = (qi - qk).norm(); 95 | double lil = (ql - qi).norm(); 96 | double llj = (qj - ql).norm(); 97 | std::array lengths{lij, ljk, lki, lil, llj}; 98 | 99 | double flippedLen = flipEuclideanLength(lengths); 100 | 101 | if (flippedLen > 0) { 102 | EXPECT_NEAR(flippedLen, (qk - ql).norm(), 1e-8); 103 | } 104 | } 105 | } 106 | 107 | 108 | TEST_F(FlippingTest, SimpleNormalCoordinateExample1) { 109 | size_t nij = 4; 110 | size_t njk = 3; 111 | size_t nki = 5; 112 | size_t nil = 1; 113 | size_t nlj = 0; 114 | 115 | size_t nlk = flipNormalCoordinate({nij, njk, nki, nil, nlj}); 116 | 117 | ASSERT_EQ(nlk, 2); 118 | } 119 | 120 | TEST_F(FlippingTest, SimpleNormalCoordinateExample2) { 121 | // Tricky because 1 edge goes from l to k 122 | size_t nij = 49; 123 | size_t njk = 1; 124 | size_t nki = 40; 125 | size_t nil = 19; 126 | size_t nlj = 8; 127 | 128 | size_t nlk = flipNormalCoordinate({nij, njk, nki, nil, nlj}); 129 | 130 | ASSERT_EQ(nlk, 0); 131 | } 132 | 133 | TEST_F(FlippingTest, RandomNormalCoordinates) { 134 | srand(0); // use the same seed every time the test is run 135 | 136 | size_t maxN = 50; 137 | for (size_t i = 0; i < 1000; ++i) { 138 | size_t a = rand() % maxN; 139 | size_t b = rand() % maxN; 140 | size_t c = rand() % maxN; 141 | size_t d = rand() % maxN; 142 | size_t e = rand() % maxN; 143 | 144 | // If abe satisfies the triangle inequality, then there should be an 145 | // even number of crossings (i.e. a + b + e is even) since every 146 | // line crosses two edges. If our random numbers satisfy the 147 | // triangle inequality but sum to an odd number, we can just add one 148 | // to the minimum. Note that if we satisfy the triangle inequality 149 | // with equality, the sum is even, so we don't have to worry about 150 | // accidentally causing a triangle inequality violation 151 | if (triangleIneq(a, b, e) && (a + b + e) % 2 == 1) { 152 | if (a <= b) { 153 | ++a; 154 | } else { 155 | ++b; 156 | } 157 | } 158 | 159 | if (triangleIneq(c, d, e) && (c + d + e) % 2 == 1) { 160 | if (c <= d) { 161 | ++c; 162 | } else { 163 | ++d; 164 | } 165 | } 166 | 167 | testCoords(a, b, c, d, e); 168 | } 169 | } 170 | 171 | TEST_F(FlippingTest, NormalCoordinatesAgreeWithRoundaboutDegrees) { 172 | tri->flipToDelaunay(GeometryType::EUCLIDEAN); 173 | auto em = [&](Corner c) -> size_t { 174 | return positivePart( 175 | tri->normalCoordinates[c.halfedge().next().edge()] - 176 | tri->normalCoordinates[c.halfedge().next().next().edge()] - 177 | tri->normalCoordinates[c.halfedge().edge()]); 178 | }; 179 | for (Vertex v : tri->mesh->vertices()) { 180 | size_t normalCoordinateDegree = 0; 181 | for (Halfedge he : v.outgoingHalfedges()) { 182 | normalCoordinateDegree += em(he.corner()); 183 | if (tri->normalCoordinates[he.edge()] == 0) 184 | normalCoordinateDegree++; 185 | } 186 | EXPECT_EQ(normalCoordinateDegree, tri->roundaboutDegrees[v]); 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /src/tests/TracingTests.h: -------------------------------------------------------------------------------- 1 | #include "test_utils.h" 2 | 3 | #include "Tracing.h" 4 | #include "Triangulation.h" 5 | #include "Utils.h" 6 | 7 | #include "geometrycentral/surface/meshio.h" 8 | 9 | #include "polyscope/curve_network.h" 10 | #include "polyscope/surface_mesh.h" 11 | 12 | using namespace CEPS; 13 | 14 | class TracingTest : public ::testing::Test { 15 | public: 16 | static std::unique_ptr T1; 17 | static std::unique_ptr T2; 18 | static std::unique_ptr> T1Positions; 19 | double localDouble; 20 | 21 | protected: 22 | static void SetUpTestSuite() { 23 | auto example = loadMesh("../src/tests/bunny_tiny.obj"); 24 | T1 = std::make_unique(*std::get<0>(example), 25 | *std::get<1>(example)); 26 | T2 = std::make_unique(*T1); 27 | T2->flipToDelaunay(GeometryType::EUCLIDEAN); 28 | T1Positions = std::make_unique>( 29 | *T2->mesh, std::get<1>(example)->inputVertexPositions.raw()); 30 | } 31 | 32 | void SetUp() override { localDouble = 10; } 33 | }; 34 | 35 | std::unique_ptr TracingTest::T1 = nullptr; 36 | std::unique_ptr TracingTest::T2 = nullptr; 37 | std::unique_ptr> TracingTest::T1Positions = nullptr; 38 | 39 | TEST_F(TracingTest, TrivialPathsGetTraced) { 40 | EdgeData paths = traceTopologicalTriangulation(*T1, *T1); 41 | for (Edge e : T1->mesh->edges()) { 42 | ASSERT_TRUE(paths[e].start.corner == e.halfedge().corner()); 43 | ASSERT_TRUE(paths[e].points.empty()); 44 | } 45 | } 46 | 47 | TEST_F(TracingTest, PathsGetTraced) { 48 | EdgeData paths = traceTopologicalTriangulation(*T1, *T2); 49 | for (Edge e : T2->mesh->edges()) { 50 | ASSERT_TRUE(paths[e].start.corner != Corner()); 51 | } 52 | } 53 | 54 | TEST_F(TracingTest, PathsIntersectHalfedgesPositively) { 55 | 56 | EdgeData paths = traceTopologicalTriangulation(*T1, *T2); 57 | for (Edge e : T2->mesh->edges()) { 58 | for (size_t iH = 0; iH + 1 < paths[e].points.size(); ++iH) { 59 | Halfedge heCurr = paths[e].points[iH].halfedge; 60 | Halfedge heNext = paths[e].points[iH + 1].halfedge; 61 | EXPECT_TRUE(heNext == heCurr.twin().next() || 62 | heNext == heCurr.twin().next().next()); 63 | } 64 | } 65 | } 66 | 67 | TEST_F(TracingTest, TransposedPathsIntersectHalfedgesPositively) { 68 | 69 | EdgeData paths = traceTopologicalTriangulation(*T1, *T2); 70 | EdgeData transposedPaths = 71 | ImplementationDetails::transpose(paths, *T1, *T2); 72 | for (Edge e : T1->mesh->edges()) { 73 | for (size_t iH = 0; iH + 1 < transposedPaths[e].points.size(); ++iH) { 74 | Halfedge heCurr = transposedPaths[e].points[iH].halfedge; 75 | Halfedge heNext = transposedPaths[e].points[iH + 1].halfedge; 76 | EXPECT_TRUE(heNext == heCurr.twin().next() || 77 | heNext == heCurr.twin().next().next()); 78 | } 79 | } 80 | } 81 | 82 | TEST_F(TracingTest, TracedEdgesHaveCorrectNumberOfIntersections) { 83 | 84 | EdgeData T1overT2 = traceTopologicalTriangulation(*T1, *T2); 85 | EdgeData T2overT1 = 86 | ImplementationDetails::transpose(T1overT2, *T1, *T2); 87 | 88 | EdgeData paths = traceTopologicalTriangulation(*T1, *T2); 89 | EdgeData tracedIntersections(*T2->mesh, 0); 90 | for (Edge e : T1->mesh->edges()) { 91 | for (HalfedgePt hPt : paths[e].points) { 92 | tracedIntersections[hPt.halfedge.edge()]++; 93 | } 94 | } 95 | for (Edge e : T2->mesh->edges()) { 96 | EXPECT_EQ(tracedIntersections[e], T2->normalCoordinates[e]); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/tests/test.cpp: -------------------------------------------------------------------------------- 1 | #include "test_utils.h" 2 | 3 | #include 4 | 5 | // clang-format off 6 | // #include "ExampleTests.h" 7 | #include "FlippingTests.h" 8 | #include "TracingTests.h" 9 | #include "CommonRefinementTests.h" 10 | // clang-format on 11 | 12 | int main(int argc, char** argv) { 13 | testing::InitGoogleTest(&argc, argv); 14 | return RUN_ALL_TESTS(); 15 | } 16 | -------------------------------------------------------------------------------- /src/tests/test_utils.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | 7 | #include "geometrycentral/surface/manifold_surface_mesh.h" 8 | #include "geometrycentral/utilities/vector2.h" 9 | #include "geometrycentral/utilities/vector3.h" 10 | 11 | #include /* rand */ 12 | 13 | #define EXPECT_VEC2_EQ(a, b) EXPECT_PRED2(Vector2Eq, a, b) 14 | #define EXPECT_VEC3_EQ(a, b) EXPECT_PRED2(Vector3Eq, a, b) 15 | #define EXPECT_VEC2_NEAR(a, b, tol) EXPECT_PRED3(Vector2Near, a, b, tol) 16 | #define EXPECT_VEC3_NEAR(a, b, tol) EXPECT_PRED3(Vector3Near, a, b, tol) 17 | #define EXPECT_MAT_EQ(a, b) ExpectMatEq(a, b) 18 | #define EXPECT_MAT_NEAR(a, b, tol) ExpectMatEq(a, b, tol) 19 | #define ASSERT_MAT_FINITE(m) AssertMatFinite(m) 20 | 21 | using namespace geometrycentral; 22 | using namespace geometrycentral::surface; 23 | using std::cerr; 24 | using std::cout; 25 | using std::endl; 26 | using std::string; 27 | 28 | double fRand(double fMin, double fMax) { 29 | double f = (double)rand() / (RAND_MAX + 1.0); 30 | return fMin + f * (fMax - fMin); 31 | } 32 | 33 | // ============================================================= 34 | // Template Magic for Eigen 35 | // ============================================================= 36 | // googletest doesn't like printing out eigen matrices when they fail tests 37 | // so I stole this code from 38 | // https://stackoverflow.com/questions/25146997/teach-google-test-how-to-print-eigen-matrix 39 | 40 | template 41 | class EigenPrintWrap : public Base { 42 | friend void PrintTo(const EigenPrintWrap& m, ::std::ostream* o) { 43 | size_t width = (m.cols() < 10) ? m.cols() : 10; 44 | size_t height = (m.rows() < 10) ? m.rows() : 10; 45 | *o << "\n" << m.topLeftCorner(height, width) << "..."; 46 | } 47 | }; 48 | 49 | template 50 | const EigenPrintWrap& print_wrap(const Base& base) { 51 | return static_cast&>(base); 52 | } 53 | 54 | // TODO: upgrade to work on arbitrary matrix types (or at least integer 55 | // matrices) 56 | bool MatrixEq(const EigenPrintWrap& lhs_, 57 | const EigenPrintWrap& rhs_, double difference, 58 | double threshold = -1) { 59 | Eigen::MatrixXd lhs = static_cast(lhs_); 60 | Eigen::MatrixXd rhs = static_cast(rhs_); 61 | double err = (lhs - rhs).norm(); 62 | if (threshold > 0) { 63 | bool equal = abs(err) < threshold; 64 | if (!equal) cerr << "norm of difference: " << err << endl; 65 | return equal; 66 | } else { 67 | const ::testing::internal::FloatingPoint difference(err), 68 | zero(0); 69 | bool equal = difference.AlmostEquals(zero); 70 | if (!equal) cerr << "norm of difference: " << err << endl; 71 | return equal; 72 | } 73 | } 74 | 75 | void ExpectMatEq(const Eigen::MatrixXd& a_, const Eigen::MatrixXd& b_, 76 | double threshold = -1) { 77 | EXPECT_PRED4(MatrixEq, print_wrap(a_), print_wrap(b_), (a_ - b_).norm(), 78 | threshold); 79 | } 80 | 81 | // Checks that matrix is finite and not nan 82 | bool MatFinite(const Eigen::MatrixXd& m) { 83 | return std::isfinite(m.squaredNorm()); 84 | } 85 | 86 | void AssertMatFinite(const Eigen::MatrixXd& m_) { 87 | ASSERT_PRED1(MatFinite, print_wrap(m_)); 88 | } 89 | 90 | // ============================================================= 91 | // Floating Point Comparison for Geometry-Central Vectors 92 | // ============================================================= 93 | 94 | // Vector2 floating point equality (up to sign) 95 | bool Vector2EqPm(const Vector2& a, const Vector2& b) { 96 | const ::testing::internal::FloatingPoint ax(a.x), ay(a.y), bx(b.x), 97 | by(b.y), nbx(-b.x), nby(-b.y); 98 | return (ax.AlmostEquals(bx) && ay.AlmostEquals(by)) || 99 | (ax.AlmostEquals(nbx) && ay.AlmostEquals(nby)); 100 | } 101 | 102 | // Vector2 floating point equality 103 | bool Vector2Eq(const Vector2& a, const Vector2& b) { 104 | const ::testing::internal::FloatingPoint ax(a.x), ay(a.y), bx(b.x), 105 | by(b.y); 106 | return ax.AlmostEquals(bx) && ay.AlmostEquals(by); 107 | } 108 | 109 | // Vector2 floating point near 110 | bool Vector2Near(const Vector2& a, const Vector2& b, const double& tol) { 111 | return (a - b).norm() < tol; 112 | } 113 | 114 | // Vector3 floating point equality 115 | bool Vector3Eq(const Vector3& a, const Vector3& b) { 116 | const ::testing::internal::FloatingPoint ax(a.x), ay(a.y), bx(b.x), 117 | by(b.y), az(a.z), bz(b.z); 118 | return ax.AlmostEquals(bx) && ay.AlmostEquals(by) && az.AlmostEquals(bz); 119 | } 120 | 121 | // Vector3 floating point near 122 | bool Vector3Near(const Vector3& a, const Vector3& b, const double& tol) { 123 | return (a - b).norm() < tol; 124 | } 125 | --------------------------------------------------------------------------------