├── .clang-format ├── .gitignore ├── .gitmodules ├── CMakeLists.txt ├── LICENSE ├── README.md ├── media ├── gui_screencap.png └── make_geodesic.jpg └── src └── main.cpp /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | AlignAfterOpenBracket: Align 3 | AlignOperands: 'true' 4 | AllowShortBlocksOnASingleLine: 'false' 5 | AllowShortIfStatementsOnASingleLine: 'true' 6 | AllowShortLoopsOnASingleLine: 'true' 7 | AlwaysBreakTemplateDeclarations: 'true' 8 | BinPackParameters: 'true' 9 | BreakBeforeBraces: Attach 10 | ColumnLimit: '120' 11 | IndentWidth: '2' 12 | KeepEmptyLinesAtTheStartOfBlocks: 'true' 13 | MaxEmptyLinesToKeep: '2' 14 | PointerAlignment: Left 15 | ReflowComments: 'true' 16 | SpacesInAngles: 'false' 17 | SpacesInParentheses: 'false' 18 | SpacesInSquareBrackets: 'false' 19 | Standard: Cpp11 20 | UseTab: Never 21 | 22 | ... 23 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "deps/polyscope"] 2 | path = deps/polyscope 3 | url = https://github.com/nmwsharp/polyscope.git 4 | [submodule "deps/geometry-central"] 5 | path = deps/geometry-central 6 | url = https://github.com/nmwsharp/geometry-central.git 7 | branch = flip_geodesics_release 8 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.10.0) 2 | 3 | project(flip_geodesics_demo) 4 | 5 | ### Configure output locations 6 | set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) 7 | set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) 8 | set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) 9 | 10 | # Print the build type 11 | if(NOT CMAKE_BUILD_TYPE) 12 | set(CMAKE_BUILD_TYPE "Release" CACHE STRING "Choose the type of build, options are: Debug Release" FORCE) 13 | endif() 14 | message(STATUS "cmake build type: ${CMAKE_BUILD_TYPE}") 15 | 16 | ### Configure the compiler 17 | # This is a basic, decent setup that should do something sane on most compilers 18 | 19 | if ("${CMAKE_CXX_COMPILER_ID}" MATCHES "Clang" OR "${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU") 20 | 21 | # using Clang (linux or apple) or GCC 22 | message("Using clang/gcc compiler flags") 23 | SET(BASE_CXX_FLAGS "-std=c++11 -Wall -Wextra") 24 | SET(DISABLED_WARNINGS " -Wno-unused-parameter -Wno-unused-variable -Wno-unused-function -Wno-deprecated-declarations -Wno-missing-braces -Wno-unused-private-field") 25 | SET(TRACE_INCLUDES " -H -Wno-error=unused-command-line-argument") 26 | 27 | if ("${CMAKE_CXX_COMPILER_ID}" MATCHES "Clang") 28 | message("Setting clang-specific options") 29 | SET(BASE_CXX_FLAGS "${BASE_CXX_FLAGS} -ferror-limit=3 -fcolor-diagnostics") 30 | SET(CMAKE_CXX_FLAGS_DEBUG "-g3 -fsanitize=address -fno-limit-debug-info") 31 | elseif ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU") 32 | message("Setting gcc-specific options") 33 | SET(BASE_CXX_FLAGS "${BASE_CXX_FLAGS} -fmax-errors=5") 34 | SET(CMAKE_CXX_FLAGS_DEBUG "-g3") 35 | SET(DISABLED_WARNINGS "${DISABLED_WARNINGS} -Wno-maybe-uninitialized -Wno-format-zero-length -Wno-unused-but-set-parameter -Wno-unused-but-set-variable") 36 | endif() 37 | 38 | SET(CMAKE_CXX_FLAGS "${BASE_CXX_FLAGS} ${DISABLED_WARNINGS}") 39 | SET(CMAKE_CXX_FLAGS_RELEASE "-O3 -march=native -DNDEBUG") 40 | 41 | elseif ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "MSVC") 42 | # using Visual Studio C++ 43 | message("Using Visual Studio compiler flags") 44 | set(BASE_CXX_FLAGS "${BASE_CXX_FLAGS} /W4") 45 | set(BASE_CXX_FLAGS "${BASE_CXX_FLAGS} /MP") # parallel build 46 | SET(DISABLED_WARNINGS "${DISABLED_WARNINGS} /wd\"4267\"") # ignore conversion to smaller type (fires more aggressively than the gcc version, which is annoying) 47 | SET(DISABLED_WARNINGS "${DISABLED_WARNINGS} /wd\"4244\"") # ignore conversion to smaller type (fires more aggressively than the gcc version, which is annoying) 48 | SET(DISABLED_WARNINGS "${DISABLED_WARNINGS} /wd\"4305\"") # ignore truncation on initialization 49 | SET(CMAKE_CXX_FLAGS "${BASE_CXX_FLAGS} ${DISABLED_WARNINGS}") 50 | set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} /MD") 51 | set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} /MDd") 52 | 53 | add_definitions(/D "_CRT_SECURE_NO_WARNINGS") 54 | add_definitions(-DNOMINMAX) 55 | add_definitions(-D_USE_MATH_DEFINES) 56 | else() 57 | # unrecognized 58 | message( FATAL_ERROR "Unrecognized compiler [${CMAKE_CXX_COMPILER_ID}]" ) 59 | endif() 60 | 61 | 62 | # == Deps 63 | add_subdirectory(deps/geometry-central) 64 | add_subdirectory(deps/polyscope) 65 | 66 | # == Build our project stuff 67 | 68 | set(SRCS 69 | src/main.cpp 70 | ) 71 | 72 | add_executable(flip_geodesics "${SRCS}") 73 | target_include_directories(flip_geodesics PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/include/") 74 | target_link_libraries(flip_geodesics geometry-central polyscope) 75 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Nick Sharp 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 | C++ demo code and application for "[You Can Find Geodesic Paths in Triangle Meshes by Just Flipping Edges](https://nmwsharp.com/research/flip-geodesics/)", by [Nicholas Sharp](https://nmwsharp.com/) and [Keenan Crane](http://keenan.is/here) at SIGGRAPH Asia 2020. 2 | 3 | - PDF: [link](https://nmwsharp.com/media/papers/flip-geodesics/flip_geodesics.pdf) 4 | - Project: [link](https://nmwsharp.com/research/flip-geodesics/) 5 | - Talk: coming soon! 6 | 7 | ![shorten path to geodesic](https://raw.githubusercontent.com/nmwsharp/flip-geodesics-demo/master/media/make_geodesic.jpg) 8 | 9 | This algorithm takes as input a path (or loop/network of paths) along the edges of a triangle mesh, and as output straightens that path to be a _geodesic_ (i.e. a straight line, or equivalently a locally-shortest path along a surface). The procedure runs in milliseconds, is quite robust, comes with a strong guarantee that no new crossings will be created in the path, and as an added benefit also generates a triangulation on the surface which conforms to the geodesic. Additionally, it even enables the construction of Bézier curves on a surface! 10 | 11 | The main algorithm is implemented in [geometry-central](http://geometry-central.net/surface/algorithms/flip_geodesics/). This repository contains a simple demo application including a GUI to invoke that implementation. 12 | 13 | If this code contributes to academic work, please cite: 14 | 15 | ``` 16 | @article{sharp2020you, 17 | title={You can find geodesic paths in triangle meshes by just flipping edges}, 18 | author={Sharp, Nicholas and Crane, Keenan}, 19 | journal={ACM Transactions on Graphics (TOG)}, 20 | volume={39}, 21 | number={6}, 22 | pages={1--15}, 23 | year={2020}, 24 | publisher={ACM New York, NY, USA} 25 | } 26 | ``` 27 | 28 | ## Cloning and building 29 | 30 | On unix-like environments, run: 31 | ```sh 32 | git clone --recursive https://github.com/nmwsharp/flip-geodesics-demo.git 33 | cd flip-geodesics-demo 34 | mkdir build && cd build 35 | cmake -DCMAKE_BUILD_TYPE=Release .. 36 | make -j4 37 | ./bin/flip_geodesics /path/to/your/mesh.obj 38 | ``` 39 | 40 | The provided `CMakeLists.txt` should also generate solutions which compile in Visual Studio (see many tutorials online). 41 | 42 | ## Usage 43 | 44 | After building, run the demo application like `./bin/flip_geodesics /path/to/your/mesh.obj`. The accepted mesh types are [documented](http://geometry-central.net/surface/utilities/io/) in geometry-central (for now: obj, ply, off, stl). The input should be a manifold triangle mesh. 45 | 46 | ![gui screencap](https://raw.githubusercontent.com/nmwsharp/flip-geodesics-demo/master/media/gui_screencap.png) 47 | 48 | ### Basic input 49 | 50 | The simplest way to construct a path is to select two endpoints; the app will run Dijkstra's algorithm to generate an initial end path between the points. Click construct new Dijkstra path from endpoints -- the app will then guide you to ctrl-click on two vertices (or instead enter vertex indices). 51 | 52 | ### Advanced input 53 | 54 | The app also offers several methods to construct more interesting initial paths. 55 | 56 |
57 | Click to expand! 58 | 59 | #### Fancy paths 60 | 61 | This method allows you to manually construct more interesting paths along the surface beyond just Dijkstra paths between endpoints. Open the menu via the construct fancy path dropdown. 62 | 63 | You can input a path by selecting a sequential list of points on the surface. Once some sequence of points has been added, selecting new path from these points will run Dijkstra's algorithm between each consecutive pair of points in the list to create the initial path. The push vertex button adds a point to the sequence, while pop vertex removes the most recent point. 64 | 65 | Checking created closed path will connect the first and last points of the path to form a closed loop. Checking mark interior vertices will pin the curve to the selected vertex list during shortening. 66 | 67 | #### Speciality loaders 68 | 69 | Additionally, several loaders are included for other possible file formats. These interfaces are a bit ad-hoc, but are included to hopefully facilitate your own experiments and testing! 70 | 71 | - load edge set Create a path by specifying a list of collection of edges which make up the path. Loads from a file in the current directory called `path_edges.txt`, where each line contains two, space-separated 0-indexed vertex indices which are the endpoints of some edge in the path. Additionally, if `marked_vertices.txt` is present it should hold one vertex index per line, which will be pinned during straightening. 72 | - load line list obj Create a path network from [line elements](https://en.wikipedia.org/wiki/Wavefront_.obj_file#Line_elements) in an .obj file. Loads from the same file as the initial input to the program, which must be an .obj file. The line indices in this file must correspond to mesh vertex indices. 73 | - load Dijkstra list Create a path network from one or more Dijkstra paths between vertices. Loads from a file in the current directory called `path_pairs.txt`, where each line contains two, space-separated 0-indexed vertex indices which are the endpoints of the path. If this file has many lines, a network will be created. 74 | - load UV cut Create a path network from cuts (aka discontinuities aka island boundaries) in a UV map. Loads from the same file as the initial input to the program, which must be an .obj file with UVs specified. 75 | - load seg cut Create a path network from the boundary of a per-face segmentation. Loads from a plaintext file in the current directory called `cut.seg`, where each line corresponds gives an integer segmentation ID for a face. 76 | 77 |
78 | 79 | ### FlipOut straightening 80 | 81 | Once a path/loop/network has been loaded, the make geodesic button will straighten it to a geodesics. The optional checkboxes limit the number of `FlipOut()` iterations, or the limit the total length decrease. See the Visualization section 82 | 83 | To verify the resulting path is really an exact polyhedral geodesic, the check path button will measure the swept angles on either side of the path, and print the smallest such angle to the terminal. Mathematically, the FlipOut procedure is guaranteed to yield a geodesic; (very rare) failures in practice are due to the inaccuracies of floating point computation on degenerate meshes. 84 | 85 | Expanding the extras dropdown gives additional options: 86 | 87 | - **Bézier subdivision** iteratively constructs a smooth Bézier curve, treating the input path as control points. This option should be used when a single path between two endpoints is registered. 88 | - **Mesh improvement** performs intrinsic refinement to improve the quality of the resulting triangulation. 89 | 90 | ### Visualization 91 | 92 | The app uses [polyscope](http://polyscope.run/) for visualization; see the documentation there for general details about the interface. 93 | 94 | Once as path is loaded, it will be drawn with a red curve along the surface. Expanding the path edges dropdown on the leftmost menu allows modifying the color and curve size, etc. 95 | 96 | By default, only the path itself is drawn, the show intrinsic edges checkbox draws _all_ edges in the underlying intrinsic triangulation, in yellow (which can again be tweaked via the options on the left). 97 | 98 | The export path lines button writes a file called `lines_out.obj`, containing line entries for the path network. Note that you probably want to export _after_ straightening, to export the geodesic path network. 99 | 100 | ## Command line interface 101 | 102 | **coming soon :)** 103 | 104 | The executable also supports scripted usage via a simple command line interface. See the `flip_geodesics --help` for additional documentation. This functionality essentially mimics the GUI usage described above; see there for details. 105 | -------------------------------------------------------------------------------- /media/gui_screencap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmwsharp/flip-geodesics-demo/610af954b2c331e9162b2d83a8dd9f22aee74d44/media/gui_screencap.png -------------------------------------------------------------------------------- /media/make_geodesic.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmwsharp/flip-geodesics-demo/610af954b2c331e9162b2d83a8dd9f22aee74d44/media/make_geodesic.jpg -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | #include "geometrycentral/surface/edge_length_geometry.h" 2 | #include "geometrycentral/surface/flip_geodesics.h" 3 | #include "geometrycentral/surface/manifold_surface_mesh.h" 4 | #include "geometrycentral/surface/mesh_graph_algorithms.h" 5 | #include "geometrycentral/surface/meshio.h" 6 | #include "geometrycentral/surface/polygon_soup_mesh.h" 7 | #include "geometrycentral/surface/vertex_position_geometry.h" 8 | #include "geometrycentral/utilities/timing.h" 9 | 10 | #include "polyscope/point_cloud.h" 11 | #include "polyscope/polyscope.h" 12 | #include "polyscope/surface_mesh.h" 13 | 14 | #include "args/args.hxx" 15 | #include "imgui.h" 16 | 17 | using namespace geometrycentral; 18 | using namespace geometrycentral::surface; 19 | 20 | // == Geometry-central data 21 | std::unique_ptr mesh; 22 | std::unique_ptr geometry; 23 | 24 | // Polyscope visualization handle, to quickly add data to the surface 25 | polyscope::SurfaceMesh* psMesh; 26 | 27 | // An edge network while processing flips 28 | std::unique_ptr edgeNetwork; 29 | 30 | // UI parameters 31 | std::string loadedFilename = ""; 32 | bool withGUI = true; 33 | bool iterativeShortenUseIterationCap = false; 34 | int iterativeShortenIterationCap = 1; 35 | bool straightenAtMarked = true; 36 | bool useIterativeShortenLengthLim = false; 37 | float iterativeShortenLengthLim = 0.5; 38 | 39 | int nBezierIters = 3; 40 | 41 | bool vizAllIntrinsicEdges = false; 42 | float angleEPS = 1e-5; 43 | float splitAngleDeg = 10; 44 | float refineAreaThresh = std::numeric_limits::infinity(); 45 | float refineAngleThresh = 25.; 46 | int maxInsertions = -1; 47 | 48 | // ====== Path related stuff 49 | 50 | void updatePathViz() { 51 | if (!edgeNetwork) { 52 | polyscope::error("tried to visualize path, but no path exists"); 53 | return; 54 | } 55 | 56 | // remove everything 57 | psMesh->removeAllQuantities(); 58 | 59 | auto pathQ = psMesh->addSurfaceGraphQuantity("path edges", edgeNetwork->getPathPolyline3D()); 60 | pathQ->setEnabled(true); 61 | pathQ->setColor(polyscope::render::RGB_RED); 62 | pathQ->setRadius(0.001); 63 | 64 | if (vizAllIntrinsicEdges) { 65 | auto edgeQ = psMesh->addSurfaceGraphQuantity("intrinsic edges", edgeNetwork->getAllEdgePolyline3D()); 66 | edgeQ->setEnabled(true); 67 | edgeQ->setColor(polyscope::render::RGB_ORANGE); 68 | edgeQ->setRadius(0.0005); 69 | } 70 | 71 | { // Marked vertices 72 | std::vector cloud; 73 | for (Vertex v : edgeNetwork->mesh.vertices()) { 74 | if (edgeNetwork->isMarkedVertex[v]) { 75 | SurfacePoint p = edgeNetwork->tri->vertexLocations[v]; 76 | Vector3 p3d = p.interpolate(geometry->inputVertexPositions); 77 | cloud.push_back(p3d); 78 | } 79 | } 80 | // Visualize balls at marked 81 | auto psCloud = polyscope::registerPointCloud("marked vertices", cloud); 82 | psCloud->setPointColor(polyscope::render::RGB_BLACK); 83 | } 84 | } 85 | 86 | void createPathFromPoints() { 87 | 88 | long long int iVStart = psMesh->selectVertex(); 89 | long long int iVEnd = psMesh->selectVertex(); 90 | 91 | if (iVStart == -1 || iVEnd == -1) return; 92 | 93 | edgeNetwork = 94 | FlipEdgeNetwork::constructFromDijkstraPath(*mesh, *geometry, mesh->vertex(iVStart), mesh->vertex(iVEnd)); 95 | if (edgeNetwork == nullptr) { 96 | polyscope::warning("could not initialize edge path between vertices"); 97 | return; 98 | } 99 | edgeNetwork->posGeom = geometry.get(); 100 | 101 | updatePathViz(); 102 | } 103 | 104 | void createPathFromEdgeSet() { 105 | 106 | EdgeData edgeSet(*mesh, false); 107 | VertexData extraMarkedVertices(*mesh, false); 108 | 109 | { // Load the edge set from file 110 | std::ifstream inStream("path_edges.txt"); 111 | if (!inStream) { 112 | polyscope::error("could not read path_edges.txt"); 113 | return; 114 | } 115 | 116 | for (std::string line; std::getline(inStream, line);) { 117 | std::istringstream lineStream(line); 118 | 119 | size_t indA, indB; 120 | lineStream >> indA; 121 | lineStream >> indB; 122 | 123 | for (Halfedge he : mesh->vertex(indA).incomingHalfedges()) { 124 | if (he.vertex().getIndex() == indB) { 125 | edgeSet[he.edge()] = true; 126 | } 127 | } 128 | } 129 | } 130 | 131 | { // Load extra marked vertices from file 132 | std::ifstream inStream("marked_vertices.txt"); 133 | if (!inStream) { 134 | polyscope::warning("could not read marked_vertices.txt"); 135 | } 136 | 137 | for (std::string line; std::getline(inStream, line);) { 138 | std::istringstream lineStream(line); 139 | 140 | size_t ind; 141 | lineStream >> ind; 142 | extraMarkedVertices[mesh->vertex(ind)] = true; 143 | } 144 | } 145 | 146 | edgeNetwork = FlipEdgeNetwork::constructFromEdgeSet(*mesh, *geometry, edgeSet, extraMarkedVertices); 147 | if (edgeNetwork == nullptr) { 148 | polyscope::warning("could not initialize path from file"); 149 | return; 150 | } 151 | edgeNetwork->posGeom = geometry.get(); 152 | 153 | updatePathViz(); 154 | } 155 | 156 | void createPathFromObjLines() { 157 | 158 | auto findHalfedge = [&](Vertex vA, Vertex vB) { 159 | for (Halfedge he : vA.outgoingHalfedges()) { 160 | if (he.twin().vertex() == vB) return he; 161 | } 162 | return Halfedge(); 163 | }; 164 | 165 | std::vector> paths; 166 | 167 | { // Load the edge set from file 168 | std::ifstream inStream(loadedFilename); 169 | if (!inStream) { 170 | polyscope::error("could not read: " + loadedFilename); 171 | return; 172 | } 173 | 174 | for (std::string line; std::getline(inStream, line);) { 175 | if (line.size() < 2 || line[0] != 'l' || line[1] != ' ') continue; // look for line ('l' first char) 176 | 177 | std::istringstream lineStream(line.substr(2)); // all but first char and space 178 | 179 | // parse out list of indices 180 | std::vector inds; 181 | std::string token; 182 | while (std::getline(lineStream, token, ' ')) { 183 | std::stringstream tokenStream(token); 184 | size_t i; 185 | tokenStream >> i; 186 | inds.push_back(i - 1); 187 | } 188 | 189 | // build vertices 190 | paths.emplace_back(); 191 | std::vector& path = paths.back(); 192 | for (size_t i = 1; i < inds.size(); i++) { 193 | Vertex vA = mesh->vertex(inds[i - 1]); 194 | Vertex vB = mesh->vertex(inds[i]); 195 | 196 | Halfedge he = findHalfedge(vA, vB); 197 | if (he == Halfedge()) { 198 | polyscope::warning("vertices " + std::to_string(inds[i - 1]) + " and " + std::to_string(inds[i]) + 199 | " are not connected"); 200 | return; 201 | } 202 | path.push_back(he); 203 | } 204 | 205 | // Try to close a loop 206 | Halfedge lastHe = findHalfedge(mesh->vertex(inds.back()), mesh->vertex(inds.front())); 207 | if (lastHe != Halfedge()) { 208 | std::cout << " closing loop with halfedge " << lastHe << std::endl; 209 | path.push_back(lastHe); 210 | } 211 | 212 | std::cout << " ...found path with " << path.size() << " segments." << std::endl; 213 | } 214 | } 215 | 216 | std::cout << "Loaded line list with " << paths.size() << " paths." << std::endl; 217 | 218 | edgeNetwork.reset(new FlipEdgeNetwork(*mesh, *geometry, paths)); 219 | if (edgeNetwork == nullptr) { 220 | polyscope::warning("could not initialize path from file"); 221 | return; 222 | } 223 | edgeNetwork->posGeom = geometry.get(); 224 | 225 | updatePathViz(); 226 | } 227 | 228 | void createPathFromDijkstraList() { 229 | 230 | // Create an (initially-empty) edge network 231 | edgeNetwork = std::unique_ptr(new FlipEdgeNetwork(*mesh, *geometry, {})); 232 | edgeNetwork->posGeom = geometry.get(); 233 | 234 | { // Load the edge set from file 235 | std::ifstream inStream("path_pairs.txt"); 236 | if (!inStream) { 237 | polyscope::error("could not read path_pairs.txt"); 238 | return; 239 | } 240 | 241 | for (std::string line; std::getline(inStream, line);) { 242 | std::istringstream lineStream(line); 243 | 244 | size_t indA, indB; 245 | lineStream >> indA; 246 | lineStream >> indB; 247 | 248 | Vertex vA = edgeNetwork->tri->intrinsicMesh->vertex(indA); 249 | Vertex vB = edgeNetwork->tri->intrinsicMesh->vertex(indB); 250 | 251 | std::cout << "loading path from " << vA << " to " << vB << std::endl; 252 | 253 | std::vector path = shortestEdgePath(*edgeNetwork->tri, vA, vB); 254 | edgeNetwork->addPath(path); 255 | } 256 | } 257 | 258 | updatePathViz(); 259 | } 260 | 261 | void createPathFromSeg() { 262 | 263 | // read face segmentation inds 264 | std::ifstream inStream("cut.seg"); 265 | if (!inStream) { 266 | polyscope::warning("could not read cut.seg"); 267 | } 268 | FaceData segs(*mesh); 269 | size_t iF = 0; 270 | for (std::string line; std::getline(inStream, line);) { 271 | std::istringstream lineStream(line); 272 | int ind; 273 | lineStream >> ind; 274 | if (iF >= mesh->nFaces()) { 275 | polyscope::warning("segmentation file doesn't match number of faces"); 276 | return; 277 | } 278 | segs[iF] = ind; 279 | iF++; 280 | } 281 | 282 | psMesh->addFaceScalarQuantity("segmentation", segs); 283 | 284 | // Make cut along boundary 285 | EdgeData edgeSet(*mesh, false); 286 | for (Halfedge he : mesh->halfedges()) { 287 | if (he.edge().isBoundary()) continue; 288 | if (segs[he.face()] != segs[he.twin().face()]) { 289 | edgeSet[he.edge()] = true; 290 | } 291 | } 292 | 293 | VertexData extraMarkedVertices(*mesh, false); // none 294 | 295 | edgeNetwork = FlipEdgeNetwork::constructFromEdgeSet(*mesh, *geometry, edgeSet, extraMarkedVertices); 296 | if (edgeNetwork == nullptr) { 297 | polyscope::warning("could not initialize path from file"); 298 | return; 299 | } 300 | edgeNetwork->posGeom = geometry.get(); 301 | 302 | updatePathViz(); 303 | } 304 | 305 | void createPathFromUVCut() { 306 | 307 | // (re)-load the obj file, and pull out corner coords 308 | PolygonSoupMesh reloadMesh(loadedFilename); 309 | if (reloadMesh.paramCoordinates.empty()) { 310 | polyscope::warning("could not load UVs from mesh file"); 311 | return; 312 | } 313 | 314 | CornerData uvCoords(*mesh); 315 | for (Face f : mesh->faces()) { 316 | size_t iFace = f.getIndex(); 317 | size_t iC = 0; 318 | for (Corner c : f.adjacentCorners()) { 319 | Vector2 uv = reloadMesh.paramCoordinates[iFace][iC]; 320 | uvCoords[c] = uv; 321 | iC++; 322 | } 323 | } 324 | psMesh->addParameterizationQuantity("loaded UVs", uvCoords); 325 | 326 | // Detect cut as gap in UVs 327 | EdgeData edgeSet(*mesh, false); 328 | for (Halfedge he : mesh->halfedges()) { 329 | if (he.edge().isBoundary()) continue; 330 | if (uvCoords[he.corner()] != uvCoords[he.twin().next().corner()]) { 331 | edgeSet[he.edge()] = true; 332 | } 333 | } 334 | 335 | VertexData extraMarkedVertices(*mesh, false); // none 336 | 337 | edgeNetwork = FlipEdgeNetwork::constructFromEdgeSet(*mesh, *geometry, edgeSet, extraMarkedVertices); 338 | if (edgeNetwork == nullptr) { 339 | polyscope::warning("could not initialize path from file"); 340 | return; 341 | } 342 | edgeNetwork->posGeom = geometry.get(); 343 | 344 | updatePathViz(); 345 | } 346 | 347 | void checkPath() { 348 | if (edgeNetwork == nullptr) { 349 | polyscope::warning("no path network"); 350 | return; 351 | } 352 | 353 | edgeNetwork->validate(); 354 | 355 | double minAngle = edgeNetwork->minAngleIsotopy(); 356 | std::cout << "min angle = " << minAngle << std::endl; 357 | if (minAngle < (M_PI - edgeNetwork->EPS_ANGLE)) { 358 | polyscope::warning("min angle in path is " + std::to_string(minAngle)); 359 | } 360 | } 361 | 362 | void makeDelaunay() { 363 | if (edgeNetwork == nullptr) { 364 | polyscope::warning("no path network"); 365 | return; 366 | } 367 | edgeNetwork->makeDelaunay(); 368 | updatePathViz(); 369 | } 370 | 371 | void locallyShorten() { 372 | if (edgeNetwork == nullptr) { 373 | polyscope::warning("no path network"); 374 | return; 375 | } 376 | 377 | // reset counters 378 | edgeNetwork->nFlips = 0; 379 | edgeNetwork->nShortenIters = 0; 380 | 381 | edgeNetwork->EPS_ANGLE = angleEPS; 382 | edgeNetwork->straightenAroundMarkedVertices = straightenAtMarked; 383 | 384 | size_t iterLim = iterativeShortenUseIterationCap ? iterativeShortenIterationCap : INVALID_IND; 385 | double lengthLim = useIterativeShortenLengthLim ? iterativeShortenLengthLim : 0.; 386 | 387 | // re-add all wedges to angle queue 388 | // (this is generally extra unneeded work, but makes things simpler in case e.g. settings ^^^ changes after previous flips) 389 | edgeNetwork->addAllWedgesToAngleQueue(); 390 | 391 | if (iterativeShortenUseIterationCap) { 392 | edgeNetwork->iterativeShorten(iterLim, lengthLim); 393 | } else { 394 | 395 | START_TIMING(shorten) 396 | edgeNetwork->iterativeShorten(iterLim, lengthLim); 397 | edgeNetwork->getPathPolyline(); 398 | FINISH_TIMING_PRINT(shorten) 399 | 400 | checkPath(); 401 | } 402 | 403 | std::cout << "shortening performed " << edgeNetwork->nShortenIters << " iterations, with a total of " 404 | << edgeNetwork->nFlips << " flips. " << std::endl; 405 | 406 | updatePathViz(); 407 | } 408 | 409 | void bezierSubdivide() { 410 | if (edgeNetwork == nullptr) { 411 | polyscope::warning("no path network"); 412 | return; 413 | } 414 | 415 | START_TIMING(bezier) 416 | edgeNetwork->bezierSubdivide(nBezierIters); 417 | edgeNetwork->getPathPolyline(); 418 | FINISH_TIMING_PRINT(bezier) 419 | 420 | updatePathViz(); 421 | } 422 | 423 | void delaunayRefine() { 424 | 425 | if (edgeNetwork == nullptr) { 426 | polyscope::warning("no path network"); 427 | return; 428 | } 429 | 430 | edgeNetwork->delaunayRefine(refineAreaThresh, maxInsertions == -1 ? INVALID_IND : maxInsertions, refineAngleThresh); 431 | std::cout << "refined mesh has " << edgeNetwork->tri->intrinsicMesh->nVertices() << " verts\n"; 432 | 433 | updatePathViz(); 434 | } 435 | 436 | // ====== General viz 437 | 438 | void clearData() { 439 | psMesh->removeAllQuantities(); 440 | edgeNetwork.reset(); 441 | } 442 | 443 | void exportPathLines() { 444 | if (edgeNetwork == nullptr) { 445 | polyscope::warning("no path network"); 446 | return; 447 | } 448 | 449 | edgeNetwork->savePathOBJLine(""); 450 | } 451 | 452 | // Fancy path construction 453 | bool fancyPathClosed = false; 454 | std::vector fancyPathVerts; 455 | std::vector> fancyPathVertsPs; 456 | VertexData fancyPathVertexNumbers; 457 | bool fancyPathMarkVerts = false; 458 | void buildFancyPathUI() { 459 | 460 | auto updateFancyPathViz = [&]() { psMesh->addVertexCountQuantity("fancy path vertices", fancyPathVertsPs); }; 461 | 462 | if (ImGui::Button("Push Vertex")) { 463 | 464 | long long int iV = psMesh->selectVertex(); 465 | if (iV != -1) { 466 | Vertex v = mesh->vertex(iV); 467 | fancyPathVerts.push_back(v); 468 | fancyPathVertsPs.emplace_back((size_t)iV, (int)fancyPathVertsPs.size()); 469 | updateFancyPathViz(); 470 | } 471 | } 472 | ImGui::SameLine(); 473 | if (ImGui::Button("Pop Vertex")) { 474 | if (!fancyPathVerts.empty()) { 475 | fancyPathVerts.pop_back(); 476 | fancyPathVertsPs.pop_back(); 477 | } 478 | updateFancyPathViz(); 479 | } 480 | 481 | if (ImGui::Button("New Path From These Points")) { 482 | edgeNetwork = FlipEdgeNetwork::constructFromPiecewiseDijkstraPath(*mesh, *geometry, fancyPathVerts, fancyPathClosed, 483 | fancyPathMarkVerts); 484 | if (edgeNetwork == nullptr) { 485 | polyscope::warning("could not initialize fancy edge path between vertices"); 486 | return; 487 | } 488 | edgeNetwork->posGeom = geometry.get(); 489 | 490 | updatePathViz(); 491 | } 492 | ImGui::Checkbox("Create Closed Path", &fancyPathClosed); 493 | ImGui::Checkbox("Mark interior vertices", &fancyPathMarkVerts); 494 | } 495 | 496 | // A user-defined callback, for creating control panels (etc) 497 | void myCallback() { 498 | 499 | ImGui::TextUnformatted("Input"); 500 | 501 | if (ImGui::Button("Construct new Dijkstra path from endpoints")) { 502 | clearData(); 503 | createPathFromPoints(); 504 | } 505 | 506 | if (ImGui::TreeNode("Construct fancy path")) { 507 | buildFancyPathUI(); 508 | ImGui::TreePop(); 509 | } 510 | 511 | if (ImGui::TreeNode("Specialty loaders")) { 512 | if (ImGui::Button("Load edge set")) { 513 | clearData(); 514 | createPathFromEdgeSet(); 515 | } 516 | ImGui::SameLine(); 517 | if (ImGui::Button("Load line list obj")) { 518 | clearData(); 519 | createPathFromObjLines(); 520 | } 521 | if (ImGui::Button("Load Dijkstra list")) { 522 | clearData(); 523 | createPathFromDijkstraList(); 524 | } 525 | 526 | if (ImGui::Button("Load UV cut")) { 527 | clearData(); 528 | createPathFromUVCut(); 529 | } 530 | ImGui::SameLine(); 531 | if (ImGui::Button("Load seg cut")) { 532 | clearData(); 533 | createPathFromSeg(); 534 | } 535 | ImGui::TreePop(); 536 | } 537 | 538 | ImGui::PushItemWidth(150); 539 | 540 | ImGui::Separator(); 541 | ImGui::TextUnformatted("Algorithm"); 542 | 543 | // Straightening 544 | if (ImGui::Button("Make geodesic")) { 545 | locallyShorten(); 546 | } 547 | ImGui::SameLine(); 548 | if (ImGui::Button("Check Path")) { 549 | checkPath(); 550 | } 551 | 552 | ImGui::Checkbox("Straighten at marked", &straightenAtMarked); 553 | ImGui::Checkbox("Limit Iteration Count", &iterativeShortenUseIterationCap); 554 | if (iterativeShortenUseIterationCap) { 555 | ImGui::SameLine(); 556 | ImGui::InputInt("Iters: ", &iterativeShortenIterationCap); 557 | } 558 | 559 | ImGui::Checkbox("Limit Length Decrease", &useIterativeShortenLengthLim); 560 | if (useIterativeShortenLengthLim) { 561 | ImGui::SameLine(); 562 | ImGui::InputFloat("Relative lim: ", &iterativeShortenLengthLim); 563 | } 564 | 565 | // ==== Extras 566 | if (ImGui::TreeNode("Extras")) { 567 | 568 | if (ImGui::TreeNode("Bezier curves")) { 569 | 570 | if (ImGui::Button("Bezier subdivide")) { 571 | bezierSubdivide(); 572 | } 573 | ImGui::InputInt("Bezier rounds", &nBezierIters); 574 | 575 | ImGui::TreePop(); 576 | } 577 | 578 | if (ImGui::TreeNode("Intrinsic mesh improvement")) { 579 | if (ImGui::Button("Flip to Delaunay")) { 580 | makeDelaunay(); 581 | } 582 | ImGui::InputInt("Max insert", &maxInsertions); 583 | ImGui::InputFloat("Area thresh", &refineAreaThresh); 584 | ImGui::InputFloat("Angle thresh", &refineAngleThresh); 585 | if (ImGui::Button("Delaunay refine")) { 586 | delaunayRefine(); 587 | } 588 | ImGui::TreePop(); 589 | } 590 | 591 | ImGui::TreePop(); 592 | } 593 | 594 | // ==== Visualization 595 | ImGui::Separator(); 596 | ImGui::TextUnformatted("Visualization & Export"); 597 | 598 | if (ImGui::Checkbox("Show Intrinsic Edges", &vizAllIntrinsicEdges)) { 599 | if (edgeNetwork) { 600 | updatePathViz(); 601 | } 602 | } 603 | 604 | if (ImGui::Button("Export path lines")) { 605 | if (edgeNetwork) { 606 | exportPathLines(); 607 | } else { 608 | polyscope::warning("no path registered"); 609 | } 610 | } 611 | 612 | ImGui::PopItemWidth(); 613 | } 614 | 615 | int main(int argc, char** argv) { 616 | 617 | // Configure the argument parser 618 | args::ArgumentParser parser("Flip edges to find geodesic paths."); 619 | args::Positional inputFilename(parser, "mesh", "A mesh file."); 620 | 621 | args::Group output(parser, "ouput"); 622 | // args::Flag noGUI(output, "noGUI", "exit after processing and do not open the GUI", {"noGUI"}); 623 | 624 | // Parse args 625 | try { 626 | parser.ParseCLI(argc, argv); 627 | } catch (args::Help&) { 628 | std::cout << parser; 629 | return 0; 630 | } catch (args::ParseError& e) { 631 | std::cerr << e.what() << std::endl; 632 | std::cerr << parser; 633 | return 1; 634 | } 635 | 636 | // Make sure a mesh name was given 637 | if (!inputFilename) { 638 | std::cerr << "Please specify a mesh file as argument" << std::endl; 639 | return EXIT_FAILURE; 640 | } 641 | 642 | // Set options 643 | // withGUI = !noGUI; 644 | withGUI = true; 645 | 646 | // Initialize polyscope 647 | if (withGUI) { 648 | polyscope::init(); 649 | polyscope::state::userCallback = myCallback; 650 | } 651 | 652 | // Load mesh 653 | loadedFilename = args::get(inputFilename); 654 | std::tie(mesh, geometry) = readManifoldSurfaceMesh(loadedFilename); 655 | 656 | if (withGUI) { 657 | // Register the mesh with polyscope 658 | psMesh = polyscope::registerSurfaceMesh("input mesh", geometry->inputVertexPositions, mesh->getFaceVertexList(), 659 | polyscopePermutations(*mesh)); 660 | } 661 | 662 | // Perform any operations requested via command line arguments 663 | 664 | // Give control to the gui 665 | if (withGUI) { 666 | // Give control to the polyscope gui 667 | polyscope::show(); 668 | } 669 | 670 | return EXIT_SUCCESS; 671 | } 672 | --------------------------------------------------------------------------------