├── .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 | 
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 | 
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 |
--------------------------------------------------------------------------------