From d442022a01bc9a33d79ca14c318dd7eff779b890 Mon Sep 17 00:00:00 2001 From: Jirawat I Date: Thu, 27 Aug 2020 09:43:05 +0700 Subject: [PATCH] init clean branch --- .gitignore | 5 + .gitmodules | 14 + .travis.yml | 39 + CMakeLists.txt | 60 + LICENSE | 21 + README.md | 78 + azure-pipelines.yml | 42 + cmake/FindDIPlib.cmake | 42 + cmake/FindHighFive.cmake | 34 + cmake/FindLIBIGL.cmake | 34 + cmake/FindTBB.cmake | 35 + cmake/FindVCG.cmake | 38 + cmake/update_deps_file.cmake | 27 + external/diplib | 1 + external/libigl | 1 + external/tbb | 1 + external/vcglib | 1 + images/examples.jpg | Bin 0 -> 47388 bytes images/patterns.jpg | Bin 0 -> 36382 bytes include/MaxHeap.hpp | 45 + include/Mesh.h | 45 + include/MeshOperation.h | 296 ++++ include/OptimalSlice.hpp | 1302 +++++++++++++++ include/ProgressBar.hpp | 63 + include/QuadricSimplification.h | 119 ++ include/Scaffolder_2.h | 148 ++ include/cxxopts.hpp | 2216 +++++++++++++++++++++++++ include/dualmc/dualmc.h | 254 +++ include/dualmc/dualmc.tpp | 474 ++++++ include/dualmc/dualmc_tables.tpp | 347 ++++ include/implicit_function.h | 281 ++++ include/utils.h | 98 ++ scripts/README.md | 26 + scripts/config/bcc.json | 9 + scripts/config/coff_01.json | 110 ++ scripts/config/coff_02.json | 110 ++ scripts/config/coff_04.json | 110 ++ scripts/config/coff_20.json | 110 ++ scripts/config/coff_30.json | 110 ++ scripts/config/coff_50.json | 110 ++ scripts/config/diamond.json | 26 + scripts/config/double-d.json | 26 + scripts/config/double-p.json | 9 + scripts/config/double_gyroid.json | 26 + scripts/config/grid_size_study_1.json | 40 + scripts/config/grid_size_study_2.json | 29 + scripts/config/grid_size_study_3.json | 24 + scripts/config/gyroid.json | 24 + scripts/config/iwp.json | 9 + scripts/config/lidinoid.json | 20 + scripts/config/manual.json | 12 + scripts/config/neovius.json | 9 + scripts/config/primitive.json | 9 + scripts/config/thickness.json | 110 ++ scripts/config/tubular_g_ab.json | 10 + scripts/config/tubular_g_c.json | 10 + scripts/merge_result.py | 74 + scripts/run.py | 188 +++ scripts/stl/cube20mm.stl | 116 ++ scripts/stl/cube2mm.stl | 86 + src/FixSelfIntersect.cpp | 195 +++ src/Main.cpp | 621 +++++++ src/SliceTest.cpp | 206 +++ 63 files changed, 8735 insertions(+) create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 .travis.yml create mode 100644 CMakeLists.txt create mode 100644 LICENSE create mode 100644 README.md create mode 100644 azure-pipelines.yml create mode 100644 cmake/FindDIPlib.cmake create mode 100644 cmake/FindHighFive.cmake create mode 100644 cmake/FindLIBIGL.cmake create mode 100644 cmake/FindTBB.cmake create mode 100644 cmake/FindVCG.cmake create mode 100644 cmake/update_deps_file.cmake create mode 160000 external/diplib create mode 160000 external/libigl create mode 160000 external/tbb create mode 160000 external/vcglib create mode 100644 images/examples.jpg create mode 100644 images/patterns.jpg create mode 100644 include/MaxHeap.hpp create mode 100644 include/Mesh.h create mode 100644 include/MeshOperation.h create mode 100644 include/OptimalSlice.hpp create mode 100644 include/ProgressBar.hpp create mode 100644 include/QuadricSimplification.h create mode 100644 include/Scaffolder_2.h create mode 100644 include/cxxopts.hpp create mode 100644 include/dualmc/dualmc.h create mode 100644 include/dualmc/dualmc.tpp create mode 100644 include/dualmc/dualmc_tables.tpp create mode 100644 include/implicit_function.h create mode 100644 include/utils.h create mode 100644 scripts/README.md create mode 100644 scripts/config/bcc.json create mode 100644 scripts/config/coff_01.json create mode 100644 scripts/config/coff_02.json create mode 100644 scripts/config/coff_04.json create mode 100644 scripts/config/coff_20.json create mode 100644 scripts/config/coff_30.json create mode 100644 scripts/config/coff_50.json create mode 100644 scripts/config/diamond.json create mode 100644 scripts/config/double-d.json create mode 100644 scripts/config/double-p.json create mode 100644 scripts/config/double_gyroid.json create mode 100644 scripts/config/grid_size_study_1.json create mode 100644 scripts/config/grid_size_study_2.json create mode 100644 scripts/config/grid_size_study_3.json create mode 100644 scripts/config/gyroid.json create mode 100644 scripts/config/iwp.json create mode 100644 scripts/config/lidinoid.json create mode 100644 scripts/config/manual.json create mode 100644 scripts/config/neovius.json create mode 100644 scripts/config/primitive.json create mode 100644 scripts/config/thickness.json create mode 100644 scripts/config/tubular_g_ab.json create mode 100644 scripts/config/tubular_g_c.json create mode 100644 scripts/merge_result.py create mode 100644 scripts/run.py create mode 100644 scripts/stl/cube20mm.stl create mode 100644 scripts/stl/cube2mm.stl create mode 100644 src/FixSelfIntersect.cpp create mode 100644 src/Main.cpp create mode 100644 src/SliceTest.cpp diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e473af1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +out +.vs +CMakeSettings.json +diplib/lib +main_sources.cmake diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..c7036f5 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,14 @@ +[submodule "external/diplib"] + path = external/diplib + url = https://github.com/DIPlib/diplib +[submodule "external/libigl"] + path = external/libigl + url = https://github.com/libigl/libigl.git + branch = dev +[submodule "external/vcglib"] + path = external/vcglib + url = https://github.com/cnr-isti-vclab/vcglib + branch = devel +[submodule "external/tbb"] + path = external/tbb + url = https://github.com/wjakob/tbb diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..b864b94 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,39 @@ +language: cpp +os: linux +dist: xenial +branches: + only: + - master + - dev +jobs: + include: + - os: linux + addons: + apt: + sources: + - ubuntu-toolchain-r-test + packages: + - g++-7 + env: COMPILER=g++-7 CPP_VER=11 + - os: osx + osx_image: xcode10 + env: COMPILER=g++-7 CPP_VER=11 + before_install: + - brew update + - brew install gcc@7 + - os: windows + +install: + - if [[ "${COMPILER}" != "" ]]; then export CXX=${COMPILER}; fi + - ${CXX} --version + +before_script: + - mkdir -p -v build + - cd build + - cmake .. + +script: + - cmake --build . + +notifications: + email: off \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..a6c94ad --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,60 @@ +cmake_minimum_required (VERSION 3.8) + +project ("Scaffolder") + +set(CGAL_DO_NOT_WARN_ABOUT_CMAKE_BUILD_TYPE ON) +set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${CMAKE_CURRENT_SOURCE_DIR}/cmake) +set(CMAKE_CXX_STANDARD 11) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +# libigl +option(LIBIGL_WITH_OPENGL "Use OpenGL" OFF) +option(LIBIGL_WITH_OPENGL_GLFW "Use GLFW" OFF) + +message(STATUS "PROJECT_SOURCE_DIR=${PROJECT_SOURCE_DIR}") + +find_package(LIBIGL REQUIRED) +find_package(TBB REQUIRED) +find_package(VCG REQUIRED) +find_package(DIPlib REQUIRED) + +# Add your project files +file(GLOB MAIN_SOURCES + "${PROJECT_SOURCE_DIR}/include/*.c" + "${PROJECT_SOURCE_DIR}/include/*.cpp" + "${PROJECT_SOURCE_DIR}/include/*.tpp" + "${PROJECT_SOURCE_DIR}/include/*.h" + "${PROJECT_SOURCE_DIR}/include/*.hpp" + "${PROJECT_SOURCE_DIR}/include/*/*.c" + "${PROJECT_SOURCE_DIR}/include/*/*.cpp" + "${PROJECT_SOURCE_DIR}/include/*/*.tpp" + "${PROJECT_SOURCE_DIR}/include/*/*.h" + "${PROJECT_SOURCE_DIR}/include/*/*.hpp" + "${PROJECT_SOURCE_DIR}/include/vcglib/*.h" + "${PROJECT_SOURCE_DIR}/include/vcglib/*/*.h" + "${PROJECT_SOURCE_DIR}/include/vcglib/*/*/*.h" + "${PROJECT_SOURCE_DIR}/include/vcglib/*/*/*/*.h" + "${VCG_INCLUDE_DIR}/wrap/ply/plylib.cpp" +) +include(update_deps_file) +update_deps_file("main_sources" "${MAIN_SOURCES}") + +if(MSVC) + # Enable parallel compilation for Visual Studio + add_compile_options(/MP /bigobj) + add_definitions(-DNOMINMAX -D_USE_MATH_DEFINES) +endif() + +add_definitions(-DDIP__IS_STATIC -DDIP__ENABLE_ASSERT -DDIP__HAS_JPEG -DDIP__EXCEPTIONS_RECORD_STACK_TRACE) +add_executable(${PROJECT_NAME} ${MAIN_SOURCES} ${PROJECT_SOURCE_DIR}/src/Main.cpp) +target_include_directories(${PROJECT_NAME} PRIVATE "${PROJECT_SOURCE_DIR}/include" ${TBB_INCLUDE_DIR} ${VCG_INCLUDE_DIR} ${DIPlib_INCLUDE_DIR} ${EIGEN_INCLUDE_DIR}) +target_link_libraries(${PROJECT_NAME} PRIVATE igl::core DIP tbb_static) + +add_executable(SliceTest ${MAIN_SOURCES} ${PROJECT_SOURCE_DIR}/src/SliceTest.cpp) +target_include_directories(SliceTest PRIVATE "${PROJECT_SOURCE_DIR}/include" ${TBB_INCLUDE_DIR} ${VCG_INCLUDE_DIR} ${EIGEN_INCLUDE_DIR}) +target_link_libraries(SliceTest PRIVATE tbb_static) + +add_executable(Fixer ${MAIN_SOURCES} ${PROJECT_SOURCE_DIR}/src/FixSelfIntersect.cpp) +target_include_directories(Fixer PRIVATE "${PROJECT_SOURCE_DIR}/include" ${TBB_INCLUDE_DIR} ${VCG_INCLUDE_DIR} ${EIGEN_INCLUDE_DIR}) +target_link_libraries(Fixer tbb_static) diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..1fa081a --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Jirawat I. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..a38fd42 --- /dev/null +++ b/README.md @@ -0,0 +1,78 @@ +# Scaffolder +[![Build Status](https://travis-ci.org/nodtem66/Scaffolder.svg?branch=master)](https://travis-ci.org/nodtem66/Scaffolder) [![Build Status](https://travis-ci.org/nodtem66/Scaffolder.svg?branch=dev)](https://travis-ci.org/nodtem66/Scaffolder) [![Build Status](https://dev.azure.com/n66/Public%20CI/_apis/build/status/nodtem66.Scaffolder?branchName=master)](https://dev.azure.com/n66/Public%20CI/_build/latest?definitionId=1&branchName=master) [![Build Status](https://dev.azure.com/n66/Public%20CI/_apis/build/status/nodtem66.Scaffolder?branchName=dev)](https://dev.azure.com/n66/Public%20CI/_build/latest?definitionId=1&branchName=dev) + +Generate scaffold from STL file with implicit function (Schwarz P/ Gyroid). + +``` +Scaffolder - generate 3D scaffold from STL file +Usage: + Scaffolder [OPTION...] [option args] + + -h, --help Print help + -q, --quiet Disable verbose output [default: false] + -m, --microstructure Analysis microstructure ( [default: false] + --m1 Export and analysis microstructure 1 (Image + processing technique) [default: false] + --m2 Export and analysis microstructure 2 (Slice + coutour technique) [default: false] + -f, --format arg Output format (OFF,PLY,STL,OBJ) [default: + ply] + -i, --input FILE Input file (STL) + -o, --output FILENAME Output filename without extension [default: + out] + -c, --coff DOUBLE default:4*PI + -s, --shell INT [default:0] + -n, --surface NAME rectlinear, schwarzp, schwarzd, gyroid, + double-p, double-d, double-gyroiod, lidinoid, + schoen_iwp, neovius, bcc, tubular_g_ab, tubular_g_c + [default: schwarzp] + -t, --thickness DOUBLE Thickness [default: 0] + -g, --grid_size INT Grid size [default: 100] + --grid_offset INT [default:3] + --smooth_step INT Smooth with laplacian (default: 5) + --method 0,1 Method of microstructure analysis: 0 (Image + processing technique) or 1 (Slice contour + technique) [default: 0] + --output_inverse additional output inverse scaffold + --inverse Enable build inverse 3D scaffold (for pore + connectivity analysis) + --dirty Disable autoclean + --fix_self_intersect Enable experimental fixing for self-intersect problems (default: false) + --qsim DOUBLE (0-1) % Quadric simplification for reducing the resolution of 3D mesh (default: false) + --minimum_diameter DOUBLE + used for removing small orphaned (between + 0-1) [default: 0.25] +``` + +## Screenshots + +** The figure of patterns implemented in this program + +![TPMS Patterns](https://github.com/nodtem66/Scaffolder/raw/master/images/patterns.jpg) + +** The examples of generated porous scaffold + +![Examples porous scaffold](https://github.com/nodtem66/Scaffolder/raw/master/images/examples.jpg) + +## Dependencies +- [libigl](https://libigl.github.io/) +- [vcglib](https://github.com/cnr-isti-vclab/vcglib) +- [diplib](https://github.com/DIPlib/diplib) + +## How it works +- Read STL file and finding the boundary box +- Generate the grid and calculate the winding number with STL mesh +- Use winding number to determine the condition for [implicit isosurface function](https://wewanttolearn.wordpress.com/2019/02/03/triply-periodic-minimal-surfaces/) +- Generate the isosurface field in the same-size grid +- Perform [Dual marching cube](https://github.com/dominikwodniok/dualmc) to construct the manifold +- Clean up the duplicated vertices or faces, and abandon the group of connected faces having the diameter below the setting +- Export to the target 3D format + +## Reference +- [dualmc](https://github.com/dominikwodniok/dualmc) +- [cxxopts](https://github.com/jarro2783/cxxopts) +- [ProgressBar](https://github.com/prakhar1989/progress-cpp) +- [Minimal surface Blog](https://minimalsurfaces.blog/) + +## Citation + diff --git a/azure-pipelines.yml b/azure-pipelines.yml new file mode 100644 index 0000000..216a913 --- /dev/null +++ b/azure-pipelines.yml @@ -0,0 +1,42 @@ +# Starter pipeline +# Start with a minimal pipeline that you can customize to build and deploy your code. +# Add steps that build, run tests, deploy, and more: +# https://aka.ms/yaml + +trigger: +- master +- dev + +variables: + build_type: Debug + build_std: 11 + CMAKE_BUILD_PARALLEL_LEVEL: 4 + +strategy: + matrix: + Linux14: + vmImage: 'ubuntu-latest' + macOS17: + vmImage: 'macOS-latest' + build_std: 17 + macOS11: + vmImage: 'macOS-latest' + build_std: 11 + Windows17: + vmImage: 'vs2017-win2016' + build_std: 17 + Windows11: + vmImage: 'vs2017-win2016' + build_std: 11 +pool: + vmImage: $(vmImage) +steps: +- checkout: self + submodules: true +- task: CMake@1 + inputs: + cmakeArgs: .. -DCMAKE_CXX_STANDARD=$(build_std) -DCMAKE_BUILD_TYPE=$(build_type) + displayName: 'Configure' +- script: cmake --build . + displayName: 'Build' + workingDirectory: build \ No newline at end of file diff --git a/cmake/FindDIPlib.cmake b/cmake/FindDIPlib.cmake new file mode 100644 index 0000000..37d39de --- /dev/null +++ b/cmake/FindDIPlib.cmake @@ -0,0 +1,42 @@ +# - Try to find the DIPlib library +# Once done this will define +# +# DIPlib_FOUND - system has DIPlib +# DIPlib_INCLUDE_DIR - **the** DIPlib include directory +if(DIPlib_FOUND) + return() +endif() + +find_path(DIPlib_INCLUDE_DIR diplib.h + HINTS + ${DIPlib_DIR} + ENV DIPlib_DIR + PATHS + ${PROJECT_SOURCE_DIR}/external/diplib + ${PROJECT_SOURCE_DIR}/../external/diplib + ${PROJECT_SOURCE_DIR}/../../external/diplib + PATH_SUFFIXES include + NO_DEFAULT_PATH +) +# TODO: Rewrite this cmake to add the source without compiled lib +set(DIPlib_LIB_DIR ${DIPlib_INCLUDE_DIR}/../lib) +set(DIPlib_ROOT_DIR ${DIPlib_INCLUDE_DIR}/../) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(DIPlib + "\nDIPlib not found --- You can download it using:\n\tgit clone https://github.com/DIPlib/diplib ${CMAKE_SOURCE_DIR}/../diplib" + DIPlib_INCLUDE_DIR) +mark_as_advanced(DIPlib_INCLUDE_DIR) + +set(DIP_BUILD_DIPIMAGE OFF CACHE INTERNAL "") +set(DIP_BUILD_JAVAIO OFF CACHE INTERNAL "") +set(DIP_BUILD_PYDIP OFF CACHE INTERNAL "") +set(DIP_SHARED_LIBRARY OFF CACHE INTERNAL "") +set(DIP_ENABLE_UNICODE OFF CACHE INTERNAL "") +set(DIP_ENABLE_DOCTEST OFF CACHE INTERNAL "") +set(DIP_ENABLE_STACK_TRACE OFF CACHE INTERNAL "") +#set(DIP_ENABLE_MULTITHREADING ON CACHE INTERNAL "") + +list(APPEND CMAKE_MODULE_PATH "${DIPlib_ROOT_DIR}") +message(STATUS "USE DIR: ${DIPlib_ROOT_DIR}") +add_subdirectory(${DIPlib_ROOT_DIR}) diff --git a/cmake/FindHighFive.cmake b/cmake/FindHighFive.cmake new file mode 100644 index 0000000..fbb9dcb --- /dev/null +++ b/cmake/FindHighFive.cmake @@ -0,0 +1,34 @@ +# - Try to find the HighFive library +# Once done this will define +# +# HighFive_FOUND - system has HighFive +# HighFive_INCLUDE_DIR - **the** HighFive include directory +if(HighFive_FOUND) + return() +endif() +find_path(HighFive_INCLUDE_DIR H5Easy.hpp + HINTS + ${HighFive_DIR} + ENV HighFive_DIR + PATHS + ${CMAKE_SOURCE_DIR}/HighFive + ${CMAKE_SOURCE_DIR}/../HighFive + ${CMAKE_SOURCE_DIR}/../../HighFive + ${CMAKE_SOURCE_DIR}/../.. + ${CMAKE_SOURCE_DIR}/.. + ${CMAKE_SOURCE_DIR} + /usr + /usr/local + /usr/local/igl/HighFive + PATH_SUFFIXES include/highfive +) + +# find_path(H5_INCLUDE_DIR H5Ppublic.h) +# LIST(APPEND HighFive_INCLUDE_DIR ${H5_INCLUDE_DIR}) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(HighFive + "\nHighFive not found --- You can download it using:\n\tgit clone https://github.com/BlueBrain/HighFive ${CMAKE_SOURCE_DIR}/../HighFive" + HighFive_INCLUDE_DIR) +mark_as_advanced(HighFive_INCLUDE_DIR) +message(STATUS "USE DIR: ${HighFive_INCLUDE_DIR}/") \ No newline at end of file diff --git a/cmake/FindLIBIGL.cmake b/cmake/FindLIBIGL.cmake new file mode 100644 index 0000000..83ee31e --- /dev/null +++ b/cmake/FindLIBIGL.cmake @@ -0,0 +1,34 @@ +# - Try to find the LIBIGL library +# Once done this will define +# +# LIBIGL_FOUND - system has LIBIGL +# LIBIGL_INCLUDE_DIR - **the** LIBIGL include directory +if(LIBIGL_FOUND) + return() +endif() +find_path(LIBIGL_INCLUDE_DIR igl/readOBJ.h + HINTS + ${LIBIGL_DIR} + ENV LIBIGL_DIR + PATHS + ${PROJECT_SOURCE_DIR}/external/libigl + ${PROJECT_SOURCE_DIR}/../external/libigl + ${PROJECT_SOURCE_DIR}/../../external/libigl + PATH_SUFFIXES include + NO_DEFAULT_PATH +) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(LIBIGL + "\nlibigl not found --- You can download it using:\n\tgit clone https://github.com/libigl/libigl.git ${CMAKE_SOURCE_DIR}/../libigl" + LIBIGL_INCLUDE_DIR) +mark_as_advanced(LIBIGL_INCLUDE_DIR) + +set(LIBIGL_WITH_OPENGL OFF CACHE INTERNAL "turn off OPENGL in LIBIGL") +set(LIBIGL_WITH_OPENGL_GLFW OFF CACHE INTERNAL "turn off OPENGL GLFW in LIBIGL") +set(LIBIGL_USE_STATIC_LIBRARY OFF CACHE INTERNAL "prefer STATIC build") + +list(APPEND CMAKE_MODULE_PATH "${LIBIGL_INCLUDE_DIR}/../cmake") +message(STATUS "USE DIR: ${LIBIGL_INCLUDE_DIR}") +set(EIGEN_INCLUDE_DIR "${LIBIGL_INCLUDE_DIR}/../external/eigen") +include(libigl) \ No newline at end of file diff --git a/cmake/FindTBB.cmake b/cmake/FindTBB.cmake new file mode 100644 index 0000000..9da2d37 --- /dev/null +++ b/cmake/FindTBB.cmake @@ -0,0 +1,35 @@ +# - Try to find the TBB library +# Once done this will define +# +# TBB_FOUND - system has TBB +# TBB_INCLUDE_DIR - **the** TBB include directory +if(TBB_FOUND) + return() +endif() + +find_path(TBB_INCLUDE_DIR tbb/tbb.h + HINTS + ${TBB_DIR} + ENV TBB_DIR + PATHS + ${PROJECT_SOURCE_DIR}/external/tbb + ${PROJECT_SOURCE_DIR}/../external/tbb + ${PROJECT_SOURCE_DIR}/../../external/tbb + PATH_SUFFIXES include + NO_DEFAULT_PATH +) +set(TBB_ROOT_DIR ${TBB_INCLUDE_DIR}/../) + +set(TBB_BUILD_SHARED OFF CACHE INTERNAL "") +set(TBB_BUILD_STATIC ON CACHE INTERNAL "") +set(TBB_BUILD_TESTS OFF CACHE INTERNAL "") + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(TBB + "\nTBB not found --- You can download it using:\n\tgit clone https://github.com/TBB/TBB ${CMAKE_SOURCE_DIR}/../TBB" + TBB_INCLUDE_DIR) +mark_as_advanced(TBB_INCLUDE_DIR) + +list(APPEND CMAKE_MODULE_PATH "${TBB_ROOT_DIR}") +message(STATUS "USE DIR: ${TBB_ROOT_DIR}") +add_subdirectory(${TBB_ROOT_DIR}) \ No newline at end of file diff --git a/cmake/FindVCG.cmake b/cmake/FindVCG.cmake new file mode 100644 index 0000000..1a4b07e --- /dev/null +++ b/cmake/FindVCG.cmake @@ -0,0 +1,38 @@ +# - Try to find the VCG library +# Once done this will define +# +# VCG_FOUND - system has VCG +# VCG_INCLUDE_DIR - **the** VCG include directory +if(VCG_FOUND) + return() +endif() + +find_path(VCG_INCLUDE_DIR vcg/complex/base.h + HINTS + ${VCG_DIR} + ENV VCG_DIR + PATHS + ${PROJECT_SOURCE_DIR}/external/vcglib + ${PROJECT_SOURCE_DIR}/../external/vcglib + ${PROJECT_SOURCE_DIR}/../../external/vcglib + PATH_SUFFIXES include + NO_DEFAULT_PATH +) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(VCG + "\nVCG not found --- You can download it using:\n\tgit clone https://github.com/cnr-isti-vclab/vcglib.git ${CMAKE_SOURCE_DIR}/../vcglib" + VCG_INCLUDE_DIR) +mark_as_advanced(VCG_INCLUDE_DIR) + +message(STATUS "USE DIR: ${VCG_INCLUDE_DIR}/") +if (VCG_INCLUDE_DIR AND LIBIGL_INCLUDE_DIR) + set(eigen_version "") + if (EXISTS "${VCG_INCLUDE_DIR}/eigenlib/eigen_version.txt") + file(READ "${VCG_INCLUDE_DIR}/eigenlib/eigen_version.txt" eigen_version) + endif() + if (NOT eigen_version STREQUAL LIBIGL_EIGEN_VERSION) + file(COPY "${LIBIGL_EXTERNAL}/eigen/Eigen" "${LIBIGL_EXTERNAL}/eigen/unsupported" DESTINATION "${VCG_INCLUDE_DIR}/eigenlib/") + file(WRITE "${VCG_INCLUDE_DIR}/eigenlib/eigen_version.txt" "${LIBIGL_EIGEN_VERSION}") + endif() +endif() \ No newline at end of file diff --git a/cmake/update_deps_file.cmake b/cmake/update_deps_file.cmake new file mode 100644 index 0000000..3d40d02 --- /dev/null +++ b/cmake/update_deps_file.cmake @@ -0,0 +1,27 @@ +# Utility for automatically updating targets when a file is added to the source directories +# from: https://stackoverflow.com/a/39971448/7328782, but modified +# Creates a file called ${name} with the list of dependencies in it. +# The file is updated when the list of dependencies changes. +# If the file is updated, cmake will automatically reload. +function(update_deps_file name deps) + # Normalize the list so it's the same on every machine + list(REMOVE_DUPLICATES deps) + foreach(dep IN LISTS deps) + file(RELATIVE_PATH rel_dep "${CMAKE_CURRENT_SOURCE_DIR}" ${dep}) + list(APPEND rel_deps ${rel_dep}) + endforeach(dep) + list(SORT rel_deps) + # Split the list into lines, and add some CMake-valid syntax so it's ignored + string(REPLACE ";" "\n" new_deps "${rel_deps}") + set(new_deps "# Automatically generated, don't edit!\nset(${name}_bogus\n${new_deps}\n)\n") + # Compare with the old file + set(old_deps "") + if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/${name}.cmake") + file(READ "${CMAKE_CURRENT_SOURCE_DIR}/${name}.cmake" old_deps) + endif() + if(NOT old_deps STREQUAL new_deps) + file(WRITE "${CMAKE_CURRENT_SOURCE_DIR}/${name}.cmake" "${new_deps}") + endif() + # Include the file so it's tracked as a generation dependency (we don't need the content). + include("${CMAKE_CURRENT_SOURCE_DIR}/${name}.cmake") +endfunction(update_deps_file) diff --git a/external/diplib b/external/diplib new file mode 160000 index 0000000..768eb92 --- /dev/null +++ b/external/diplib @@ -0,0 +1 @@ +Subproject commit 768eb92646b3f7279906a73e16be3f836186d398 diff --git a/external/libigl b/external/libigl new file mode 160000 index 0000000..4ce917d --- /dev/null +++ b/external/libigl @@ -0,0 +1 @@ +Subproject commit 4ce917d424efd941174c6ff46adf15790c2ad12a diff --git a/external/tbb b/external/tbb new file mode 160000 index 0000000..20357d8 --- /dev/null +++ b/external/tbb @@ -0,0 +1 @@ +Subproject commit 20357d83871e4cb93b2c724fe0c337cd999fd14f diff --git a/external/vcglib b/external/vcglib new file mode 160000 index 0000000..5c21b15 --- /dev/null +++ b/external/vcglib @@ -0,0 +1 @@ +Subproject commit 5c21b15d3687b57133aa67f5b540b4e318da4bc1 diff --git a/images/examples.jpg b/images/examples.jpg new file mode 100644 index 0000000000000000000000000000000000000000..bdbd3b9eabc43b6ad6a2793dc22458cc15314ce3 GIT binary patch literal 47388 zcmb5VbyOSQ6E+@PT3m`t2@)u@xO=hS!M#P=BEbX2p~XtkV8u1Kwm|R}cXxujQ>-}r z_0g|1FPy03^6*5OhNfw3h&M5;P1Fw8u^W{ga)T82_33 z{{#yg2lr_-biAi#Rbl`d1_nCje}aMiWC0c$ItC^Hiv*h)hm=>A?4|a5TymEn3ck1; z7CA^YrDYqC-}TGb-xs;7C#>@DpHyr*ZsP)Pt>W9Clu@DmPyXLPPY38v13k5{kUUxN zpA}F4|KzC!4V{FUSC&-!{Yw|VAPlm{dBAfFv?seUNC47+5?vaOY>vEgax8Ms+`!yw zqil}c2=s`jbN}rCz!h|L2VGAszJjEwoDi_9+aypmd{|b8m zpV6GxDHb95AZLM2VL-BQ!sf(Gn|-wrRo{bFwuUQO({!3%Sg1EtC`uYF&0T0*w3$5G zGXYz4!9cHQ{fwr}rB-Oyh}#0IC~(0OCRJt0dDB9OqH4^R#E4+RIPO_TZrm-%~5u+Yd24Z!%| zJ?`pu&%r`JHJ?`*dJ9M_W1J*XvU_XK@_rI=K(b!RTt=iFQpC`Oj#yL`+)r$Sn8P$e zVmK4r{uOM^d6Dv@&FGi2wdc(pI&*6Wog%UMOc-^78ZN~r>WHY{tIF8Pk>83|h_VI;C@M0Rc$xc=^zObgiOAaY-O^jeXkUlTjHcDJH7oSle z*M{UMU`JpG9){Av_h@Wu#r8%xwSwUYhCgW2Nj2tSEhpGFQ<-gOsNl^nKNB31Mscz1 zwdX*XQNyU9$%v+Au`BQDzxL`ceJX^Vrvg7$ENDmvIxDeAk7o>P>Up97y4}+z2=+$j zZN1s@tndZ*sU=K+vvuE>eQ5NL{sQ2p&i<1-Emv-e-LG_cqiL4Gi^Nuj0E^k%Irt=*0hKbYW8T%D<|as$`)7 zYwx$13_84O8^WOlP; zzLSeL`o|-Hg^_^{hG5}Fj|G4*-@T(FPk%l=##>sKw& z$HR~a0+Q-#Uf)$s@i>j0 zvSLnJu!o~0dBe_86(=7>Hg;$o?AY4Ycrv{rL&0dpV7>qpe#7ae%wtBg~(sOnZb zv-*A5uR>NNiSi2^r3h41Iz^!*&Zz#iuul7>OXAzIDkW{8aqolK8VM8g9NW4JF8`OnU-ZiqAS}DhEeqbhjZJp{{PP`7yF%Dm zcf?LTH(~@{K-SylqJVdgx)u>Zr zF2*J91Y9>f=R)bN$FIP(2;Tunb!@B*hO34mdc~MQSX4Uyn72+?@?}5=<$%NIlj%u8 zUOJqFznXt%)`^5CxkPvDV)W+tqy$a|Ps?Puv81-Bgr8lX}vsfAVQvFP|e{1h^SDvUq9*CQ*#J zqMHc4GFy}Qm)CswjdG_lu#&;I&X}Okq4-^CiU{QM}{aS@GA zE-hL=zt;iMqxHzquN6d!Jx`FJ);CDN+1ae|o6_))4kMa$TslMZ2 z0@HoIWcwyVCoY>-_g|!t>&1U6I`65675AQAD||C<*D@sa8}>K#zgR?!VTqCC9Q0il zUh}D2>%B;%-6Mc#ZhU?^F`GOgF!v4O-MbC?lIA<(uIjgwbxGMGg*?SE50kr}*t-6tIWKh93uFgQV*6{hu#w|;`xcX$j0ohh zcee(~6v5izS!){hpoyf<5!A682(b{WTu3C>rEvhbGLh*7sYf7R8u`IsgmMAi?sD&S zmNkj?2;f$lYG0i362vj)$gR#zaNo+zDt#S$NLlwhWjwhTW#!lj*L(*6X8xhkD0TV6 zKuJ%KKXkfDm-`6t{6x-dHADkyQ|=amoDBLkC@#J8Q2)6osaP$)x|R985V}vqnzg13 z$Ci433NXG8{ew1(whTlldvUT#@QE>Ta5EVX? zH<@YW;D-+TG#FeiH8T1g+!NBs%Ce`-=zXliYJ}k(eKqB&YeGWMNCNln( z-r)l3?(wyl11&1x>;o3!BV6pGgnxX{f=qR_jjR9X^u`Mv(eFgf1@n>)b=>5gYxbSa za$c7v_L*kKYQf?7qU~txfM^H0h&ds~1&Sv2kZG#XJS8Y$A8wrF-$=33-d?$H54zU* zXh@wYrPr6AfBY-;+>E1l;Glc;ktxPX#W-G3Q~kOpQ`NC%?-ZHhOy74d{xDhgkZP|2 z$~IxX#1X<9p89-bCTjBQ29+Z)-7xm8&aC_p9J&7fwLt!9CbYRt9+NK6^^Hdfd(SX+ z+o}O^{@h_QQ78AlEgv3x)Ak@FGxue#2t-_|=B&fj9sU*Q~_>U*>002DT?Acik_IX}7r*LLM(aSPK zIY*m}e@fIXv#ooTp z>Rp;yiM913?a1@7j8>5o zv&06*>XyIjt5ExA8d5Ixa!dfBUfApVrk34$yF$YQMSD@GSh`jaU+Zj(G8-SeN3lkrV)si8DZA%9~{Nj_>CjXl-uN}jO#_+x$ zAWCb^Fk5W=Ra;|n21_mT#B1#hApqcqQ`~nuy|``ytV&e|9tQ0ykn6sJ{Rh3Da7OXW zs==4otE^&HGpAy3%|&5CaR{lD?Rulh?w43$tzPkbcR_q(ZpblG*<V(wJX%@~uJGbf{0k|pg z`N0aIeQx|Og;v_Lp0BLL$_X>8V7<*J=qyAZ5!)K%GV3gpa(Suhy9-Ix_`~vp6mLJB zEP~(3U{QIvz|_N#@P&1bX_5G8kp~;RW~)U%gHS1hAT3e{8G1!6d!f=Td!w_hXV0aW zRLh3?gzWmDt@$!(UZPi1{&#WrLoM0zfp8jOWPG_Pl1HIYyr^F;&d zN};FDjRendE*CG1&fb-&el0>POmA4O5n)>2{pYgezKemQxhc35nrZ9B)te~r^hg;8 z6ONpP_h%*+am-FU_RDDOSTw{5F?^TXpDkLi(8Znas)=z~JUJolr@145MXoIyG4vk_ z1JihSap^2W|NW3I}Ij~ zDoXTkYKVXFhP}93;Njyz#~?AlriIe=8nNJG;839)rx^aay*2qPoSzm&`K;hrv=ed7 z6K&ILEEl0Ga~ICw!0!9eRpWds29*HN8IHA1=kh ztX%g=5^Q7f!aMNPSd}E97E9nbJVuH)l1S*!$OcKTqU=)Ra{#>ubCw zLHmh+_5E>d?$yi%Ka}{UA>>PM_%m(jn+`P@h@4ZFD0Xs+E5tr1PY(`2WRhw)_=ew5 z2IrqMe-_^sJ)Ci}XJ=;r`kTPTp(<+Mq}_J&$8PL`o=9#z>dY%vEHy z)k)i|)u-K+OUsD4DLwz}#Y8_E7E|yMp9hBT0heQLgKWxsmz{6^3SCg&J`T@L+sHt~ zN9p8UbyUdwp;v3yiwiP>?yuzqAXQtKMD~gmQ%3Ysr|v9tA=#!iLHctmr37|E9xHUC zKIB?6;euWfb>Iy(LA`k9!!1b)w5NGlU-&$+-0BAxj6u)3@bsK5E%o$mX}SmNn{T`h z+H-jCU9TZQfhvH*IriN;A6=|Ye}M%UI5h^}w}YN{iu%$z79l}6OTEBOFMeb%#!3iu zX_v}ybr(<;_JZi@){$1OK4000yLOeCbvhq1cy{qmKd-U3Ud@7kxKsr5G}{C0s?m|1 zf5YXg^*=|{Mg9U6 zhvK}E{T894@9=}3bau;JrR;oL(sVg5$Zp?d<=YKsBrN6lnI4L%Afj2 zTT)}IC3q|dgu~LpcU4?UmUmv3JX)0!6NAm?_I1#^D4?CszhgRMM9f56^iRp}ZhEJO z*OQs#utbxU&4TJNyO9*D{Xm?m%cB0>&mGULzW5dM_KiDO$W4+ zC45_^ADWY>Y%pLP=|tK80jkUdQk)J?qSQ4_N~r#>+BagBuY9Ut>SuDe?aD{^5wZ-y zQ>T;q?4-9PCLfvB5qwgN{tUT)L7IynM_zENf&x2AV}6lL43cZjv2ga`^+!u{{Gs2% zsfZdhI&oH!9u}K1<^6fou=z1u@zT=8Iuqi3aj5<*G~L*>%xam!YTJZa;%gE-GP<>v zeqY>53&kl1LEDYK@T4{!-9aXZ1`3xfcKRG&eKlOL0K@BOECTyfdO4I5NG{r>y{moY zn_jE$KCA7@CB22Z3iy%dk`kfPr0t5Qy3eA2jO6`y2Y9|b@jrgfItp?#%?foUj`qgf z9BgmfBR6WIi$@%-`qML0F8&B#LKa@mm9G}{Lc`;TsXhqk28PB8fgy2KfOlwm?5k;- zp-|7#F$b}`ke9T|Dh6+$K<QNHi^80L%5@lGpAxH6J_7huR z-FEykOA&Do^ygMI``_zm-MjT(F3nGIf|WEB$92(v_v@0zKLRMg(OF>x&x(K**TpZp zy#-t9@gpXumsz`;z&~Y3~ z92TpI@zi8)ddudMzC@>Fcnd?FE&Z_^rjf0S79X`$@;MK8q zG8rt_re?z$!Mj>Vj-(g`2m_}UfZ_9%C-GV(i-qg+qigYOmJJJlb6d%nDZ@v=t{^@P zvFKDxQF8HWAXL;C(m<_rUMHk^?_UsGnh1;6(pI2|qJE;Sy3iEp?I)B^=)j(5#1eVz z2OR7%A(`ZR&sxjE%Hq+w4?IRFpnZlIJw9YaV@%U?!Y#8Wu5XZG|oFY zxI#v|udh$!FmprCO4yT=?>ASXj@H3@PB+s*K>^pq08w0~paVP&yPT64L(SiI10gNj z-@c-olvmA4MX&%bMdr(6hy{U)Ib!=kyyea$gGu9Wh|&6_3fcW`rf8Z>>6#YVG(10A zHzN~pO1`n*PH$ME-dAf(UKZ)6+2hAcSrywP#ZQ6RF?J+1-?x}CkogMQM$?;9Cwj!D z{VV>mJecYH+;G?$t$+Cp#43-ky~hV&(q;aenE(1r1YtetT>cvTOE#hC^aGGkKcOxG zM+dI-h8?iZBGOx`gRS=cL-r&e8){LWusOJv<9HB_De7b?&(-_))$p&G7sA~5;5-$Z z>1jv}WxvyM>USR_vYN#Vro!4KIDPK3cy$2V}F;U(=86JZ~yD z4DY*mO~%@L{1VQfu_1J)=gTdoJ=@j8%fHo4DiVVVob2n(Dk2w6wofo{9b$;%>h(4L zFjmMMYKJk39Ez!z4{+DH#ihlHJ|kJ+*mP>pm-I&4jW_dknVguvd!J@XO~lEB>u;z* z6P46{X`tb#Q?=(mWngNYVPrh1pX;id2e*IS?8qCzk&CdbHu}FLbl(4AgY>0WF2nS{ zUpxZtl$^b0*iG^o;h{@?TVOPu?~*+kOa#dNUA?}fq}w8BKB$K>J0S;yQ(F>1KjcTX zT9Fpj!9e>qh-p=*cU2YMj;7P~*(8`z1IBPq%7sy4eH;7|H3n5qrgjYJHPYr`T6fe^ zHa7qa3W;Zqe0g=e(@j==Nb&ihFs0pO$G@;w`{Ljh z`AAjQ@ng8~H6(I9&DE<)nNyMv%URZcMo9c@)6|-C|HYNVoAo`&IDQ?ZI%-md8iVT3 zMWOE}EeDr1g)H%n(N!HN-wp%IcTOBhY;R(z-1-fp?LARX_VEOn-~rZriFQaKc{5z& z4L{1#|AN1~U~u4-NF5t@yp!TIGGg(w5}GZZR)4HaUmt2Zt=wonkx#VBj4lqFgAc>? zk6YX3Y*~Ify2gsP(|kI?e0g!?fP)#Tc`4O}r?_^5BK>0m2Fm3T;8nyPmE06VnCX%h z(640YD5st1J3ts9B-Ez7J5!8fr~lTkn~-g2^6~}u-~xR-K_ZApj3T(%ZHG`)>GD?B z%7|TZr^x+@ffD*J+wff5DEG^ixl_LbDEm#`QY;JWGKt13YpxczUXx+E#@xJPlb2G* z8<1}@JCa;=ORjJkkuGQ(daaObmlNKQ)>H6ZLN_5M*+#EGT2Q%(f@t8&VisFu_blj{ z6??9MHyEabuJFutXkLrOXGR#u++aS`k{Hmncw{~WO$fOPoVINunu#0OJ&e1iWYZb4 zh5`pYJOi=vh)7PB70xm}0`&9N92x`<`fXzOpzXvbER-3 zjho`>^TuWMyDYq$r0&f6^4+)o^~mVNf~7H}5Ua$s9p%2wjO)CTz0Tz3X1xw{v`;cz zwkiUYjUJemxJj0 zamI<~LcruS5UZ#HqWwDkqoFu#^)__;;=oVq-4esEen)X__&P2pE0+b|DH>+XDq5h@ z)^UWrZ`gNvydmjA?#THAWlK8TIb55(j6NV-WdW`(a_DRH0n@$PC5u={6hg`yVcAwU zSE2vUedF^Uzt(c-WH$<{qhA>ES9lj5;BDKk7XDL}k2pI&-FgPPAI&(KQX<%yq0&1o z!4|j(+EJG^zSNImeV63|{o_b#%Daxv2Wjckmjv9OK!T}`24Dq0pc>6B@ZckL$f1%~ zy;)&>${Br-pD+ zHn(Bax3OO%Dqx;cMDGO=^^TpjHBxqGplc~koe<`NiPdb58#m^+0X;^>88C|6@jWtv zKYOu%Hqek@84L#p&zsBqeF-izxQwamKGQ+PNBpykA!fD76Zm>?%OLu-3hi;aWo+%m zrbiQtlMuOTbg+X<)VGmC^GxNl8-Kef^zZ#c-7_w(bO%J7iAkn6(b=~#$OHU*9cRxD7MamYmNx}d z1{8;Gsf~%?R$uo{oy0lwp!i$Dcp3u+;xlOB3|J@`z;jhF^$Q|dDk$%yIH0az=$X>R z@D?F{#;CqucVX%muC#`I%h~lI<9&LtFrUdT*cb;W5I?AwRiu2IT2agHt7s=&%Sg^v zet|iB$sSYdqIr>Y}!oftUBPX8A_z&NGi zX7L4W4Xr|HhwIu;GnZ{tKg&;PzRRab0YK!p_xp~*w$!~`>Tnmr;JQh;wr66)Q_PJV z9w*z9nKPmv$?gIF6i$N{Pg|>b)vdi^HRsv^h~(e6F=Sx59h??#kqOPlE__K#|7HiS zO1_5UYw&Jv;8FeS29azz5%Xm7ZP&UZ%Yu^@HMz>~i2@qVE9hy@_}?wC{&vz;E_Nd^ zb9q-hc^h+|ru}e7?T5pLjSa!$^g^EEnRk$sj5Q{XlRCwn|7@ zBK)o!aQ5{;-P7ZfA^ADBA|S0c?A9Ol-28d9Q{&V3(+h*UNeGHg*i(2hpk_{5w{Ig8bCgj|u~6Da-P8uOG={Vzog90}uQTKc&*lqtRw zTXce#`isi(nZNbM$_j;ROe@z8K4L-2oaUv;4->A_=K1M#W~$xR9^|gx`pTt}>V%lw zegJ&HUvfL)&->0*J9*Owhs+=arGr?X$q{NIlR`5?=R^iiVN@U-EF}dQ^w~%*ZJa@W$jY8$V2DBVSG9n5q-4KI{x|OmrXAI&!V|QsT00Y zY1u3}=N~p)vd}_RB5P@SCtkIM>wm+Ab+QkwGKIJi=N)Ny-ay`0H$a2`#JIT-g>?+` zjYk6$8F`J``dBbf6yo>XNApuH_u6lMX3=k@ZNm3=7|;#iUh#38-Rn-Rc5G{!9Y9A8 zGoBY`Zmx^movt-?S)`U<(gFBjxlm`7_UMg!PD8(jSZK^2^_JX?*n&t{SpW&XGY0HJwdfcO4gMZgsO8aqLyDerGi)MxAGn`&Tt z)?(a;e8wZf_4)#&@r^iN*J~}67|ykM=!v|YDpW<@F?f>`%eGgd1f;Ek$`LfXSnhCF zFg*ITJ|s^Q%14)4G-_G7VP?yeMF3!D{)6p`jfYBr75zGI2(X({5Q7Cgzn~2JPG~c| zRJWokN7>m)_#t^P^BwL_(FFqpZ_tEyKUsf{tZx0O<^yO6O%&~w)g$0MH}597 zl?`eED$f}LJAaFvh~KN>$bVR70X+qLOf^&mmrzP5GETZM1vnv|w-WHbZp`ziHs&W|;+0V6v z*L(Wg!LCZRBGA8dT6Ec2*%fIv34?HAY#cq3U5)deD5prUmyFE*e1lD9MP7x-p<1i( zXB~oDewuDBl@Ethh?MlN_qR%T;KaTB^yfEZJG`p)JDObtb4_(Ylf?5D{83#lT~@Ph z0SMoJPl0QBr{4ONP%Y{e9k+VBESt@|eZ?8tPpvru1n?g6>k%NR-QOg^jI5YyH{N7Sqcl$e?AYOb0%XOi%}1E77y3pVY_`<1@e9kod^JJ;R)lvQa|7My zBXYbLyTz|TZ0X@lT&Q7$!*AMtr?q1G&8BfOC4=h@$l!g%byM-24o%X*csKbs$Nc*w zJAQLuKSyqZ7=B9@5fZ4fqW)7=D}zWVqFP^wlxKEZkd+8vMPGlgEtHQIkTIs0i!fE`sl2x( zy+klCST;BrlNX@03Ak0aS!mH!sKsFq=U)q0?SITD<~~rJ@sd_0(|O{7but^+eSJRU z8x_kMfs{2XTRbybMSbOC>s8$q29o5pG38u_!-9Z*pbNJZ^Yke(ef8*#!{~4v1EW0?n`HMoMWRh7knDk z)hyM&YaODc#`IZY5a+wyV9R#GG{Z>OOCn%>(d2TEZpe{zgIn5#L)ZNsYt_%l zVvvRkAfaeI8wXmgZJo(!mOKAcU?mskeS?#iut7kU|NacboF<`5Oo;6Z+*se&#eVhi zL;nYHTpp>@4rq>~Zmu)VzJ(l5byusrcHF~Z=;$cM7zpE@v-%M*?)tZLDn``3C6~1S zu7??nlR&sFQs@yAWPjkn!fi{Z9w#zP>-)n$84HnDQca`l_->JF;SDE2r zrjyj&V^2;*wsX_yAN#{qU09TiPc!p@HinF{dg6{_+Yt&SkufKNsO=YPVA4gxB6rSb zqWO)xjT&ad+dP*4OvChd8TPcL_f4mUrrxzahw-hPFwG{d4=L5V_HKQ=nZuWOawi~g zVwQ6%QtVbuebUyf@l|JgG$r|MDRp~5KH+u&N)K&CzeFQy4H3Fl!`(el60Nnwry9;( zSd2l}k9mvVY|N;&aP%*2;*zRDppLDvM=TV5dWp0uOAFs6Gi4;F>~`fK!@X`-oL?)R zRUxUdyvsWNAq%X6+$&W$7iH`LkFRV(ysF9=!7$r$L5BvT%y`k7-)xqyKU*fp30GMr z9%?408u>L0SzhX5f7jr1HByl{a=QDy^>4mup|Y=s+kbJU{AK9N0%)dAzcV`;GI1oT zMG4w;Gb5BR>4`IM!KvTTCt~cD@dyCBp{&Hx?14G&6#%Cq_ICF3so%3efS$7ukw`gb_5}bi<90oR8Ia!tNE+g5IR$E5>y%#i= zPLc3nvFf!@Wx3dBEwVwyjRTEaSYf<{NXmG+7XhwAQ&K37hU^SZ1E;`1%aWA6f%xfP zBOOEe6383`rN7WTAkhIJiwyL4OIHt8S$ER}o$qXF#$n7i2kbSWT4E`}9ZOy9Dwtz0A!k$E{aIU)s9+ zgeqP=0>a|3U)5`B+9ZgaA=tzsNR!d^(ix?c>D}TM4AhV;nUbY;JkP7nKjr64mUH4# z(=~ShBjm%1euPtdKpVCz(Kt7vsNOTNvWssgGo*+g2AM0&&tSia7bAnpnFc52yM4U+ zuCeWyvV2jYKlwNE9AzKXX|rZ(%)?Zf;D)yO2lXz%!funR+~$X>t9L~KW)g)!*{MjN zyiprwCH0Op&mZfHay~QI_m?X#7ddq%&^uE=?ce=yMB4gjx5pzV*l`=Rt(Wc6iV^?jFeI2Q0#d14cx6oobxh|HN^j-nliF-<`h36xw=oagNQ6OACK|mXt^{s1> z&eHBGUPp*)Q&HT@c8Z*WNpxU=*i~ppmV&_LYmmo2<|p-CC?k&!BoOL}WlD)q^pqa{ zf{VACNZ%OMIF+iq(e@ogsf-`zVwD!h6Zcc+E_(u zJ0d`Bvf^Lj1|@HF?d|4&XoH3KG4J_ZU9tMUGN`^IO%K7s82!SBE0r6O$kQRbSx&VD zv?BMN;oZNTkeU$m7nPey~2U+B2F%IkjPFFHBL^q`xt^BzT$+hg$a<<#b!2&Vl=R-pZc(x5i(5Y9e zAn3?ySyryT=6vf9d1Yf)+_v+|_(X3hs;kw9S7Ys1b3AwV>4NmTN~}EjA=r~DeOv8> ze%(GR=E4TW{f@81tm2PN;)rmp;eI#z;Rd}#%$%eqZ42+^niSgkdJ?yKZ)W@&b`dVq zc|YAuX215Mxt~T%JGy8|j1vPc3oec$gv2Rz))ALIZ}Y$EKpk`T5_84h7KNtJyH)Ec zrCxp;teRi#fN{EEI(TT_tG^Mnpe!(l7)Ruy?2Zzh)IK&Fsd7Rhl05s0;Tk_TTM68R zd&Z4}`Z&4dvRe&~lcrJ;Q=hJo?jBPer4<^wH#&5TR|#nMzw})5Sl3rd!wO)kDy!Ww z9-`n;?jb&B4}1q#4nHrrU8WxVO1@ro(m3S%^JGK45_Ec>HkgqS*Bt_ML@M_X(*Ny1 z?}Xn-5NJz)AS4gtP4~>_w)b#pc+M#zQ9A4_W<~V~Fi!YfozL7eA-7FmH=j&li|AdCUpPax2ExwRYEEBV*9N!YL{=UAiPt zs+VlR!}{CIj1Iajg|!^@bF=ls%E+soeq;BQgl(I5H~;Q^W_(`kJA{xUL7DIOa*Tj2 z(EFFP!kCO9f{BC1^jd%~>y>q&${fp4Ky_Ka z6?056kNr?>qC+eWLSOTHX^s6hArEg@dMB)Kw9H-0P$NKrrLhERtcf~D#Z5{u0 zuq^9PYnicD_z@7N-x2+l&1Xl6O%W`+#m?|GfvCQ0VTRzSRPj@8#KxYxb2XmZw?-8% zx<27g4Gh1#)y-NY?V3*TAFRLppsdAmlSsrNk8hP|VpRX_T_8bTr`+UXuhJ8l>50`< z7%n|J-!Sdp2aYneH4YiVv9pv=;I2(L6Ynnrfq@%jJ7N%{Yglr7UG0S9OBNE!?LqFi zz*ch$6dQ6V!_m6~y+6q~VLX=j{3F?57>)uKJMGt2C2vR6RU*SGGq&v}=YtKG`0YfD zys<+(jAD3iy?C2D1}zzHQ!d(TQn+mxWd*%lc6?au;ke%R2~LCV`M&)+z#tz*l&h*8eCfQB1$R>%W(g zhbIhEAN&w#JgQtVoYGQ<%6RPTzALA2@m2YyA_b{>P-*5kozhRKL3kHAa`YB13||Feu;anKCX|iroJoI@VMU}Dh)kzFH((la$JB=Y}tO={ByxzmAaekW%-fra>&+m zTr=s&S$?S4x7gHWbMvzACScdhSGsC<-NS5t4snCb6g_ghsdAA~xmn z;gy!s!oHFedZ_g5J2P_8XcJ1n@v7(sO{drq)uJx-VJ}$o&fg6w zqJb(dH9gZZ<6^)JAbZ)jW4+-@=(%Tyk6?=rS#bz2g^Lp zwVkH|DH*+{t5;*O+J#4ri2nFf6MEtc z0|@_;(0KN?o0RI*%gD|5_4WU1xRfkqowC`7%uVbzicL1H&Dp4({ zi$^-2aSL}f?fqc-xobQ5Ue*MZ&oJfW_-{vFT$ShL>$Rg{s}7SsIo+B(f14++jrL+k zY)a!QqD~kioS=FB`oR^0h|A%yP>TWUDss>{b)UyQ%7*^;mD!SpgEnfd)XAnzFf9Ucg{z>J$N9ck+nxALS{5L2J5 z72a$XFd40Y6pYt!H2X(vC?9Sam`1hdQgt(jmT$h-+$NI{|8_HA-jmN)jwU7 z>sQxU%|b=hM|pVN;ZO~6(GXX1IzfAcM*k6Spe;mUNzGzSYGAikVqVv53$!^r%j6V` zQ|sDMpL1IuLfws0(~F^=an|%hO8s`yglYwl#d*L|kVO;a8bW%j*yBXu6wwe#-prOi zU9g_l(H^`7^Nq$9soxy^nL2`0krb<+aisJw-1>tAlqZqj$w_m-8Bo;s zDN!(f03P|ZRY+RisNf>}RY$98#Jax#8#a(l?DWg37GsK}2TtL1&7f(M-jY`Xl2Y&+ z=WFJF^K&2Z09^R}i8I#$$-9P4osCM#pD2GEsHkk&-bNzOYginzD)?`2#vcLw?N%C1 zDcqo>@m!$fofah@|JRJOS^ZCM7wlK_BrHcy*SaF+U&l(vKwZ>9RX7R7&9OodXYt7O z0*=DeEwjVq~dc>8+2pRT!G_Tc{rrlJT@uQh)b^c&8eC^U?X-{s{0s=69X`-Yj=zJD#rW za+`=a{(HZBWK&kd*D)YD9gVu+=z%D+Ww$8(jq3Elhg+domT6d6QPzQFKCYEufA@ZAU^hSoiSyY`kF};rFI#ZZ5BWj$W|C z7_VgodA@U3hW-n0bkkJPtxPiVw&^WQ7>FA!BJ2gcXPKCUtNu3HZ)(}0qyV+iJy>G| z`qy-EXY?)1a8X=4tk&+oNRTu4?Fo1UOg$+0`4MombmIN3n#i$3i4KF{CisUYawAN# z5`CI7KDq-gCyjT})QzXU(*w4CpYW+9K|rxWy}_Tl>g;_`?qHGeFQbZA4-m1hhS68g za74=Q>b?s&R|Rh}c_KWF8L0XJTGDBfwuH+-*3NR}ZO!Qn?mBuImfr~2H$`8cQ1jJh zME=J%ze1v_t<6WkLrJCH)U-a<9s##!9TTwWCEw_>u!gUVbSr0KF)QbPANmm6@m)inS#rH)>4F6M+bo+nuW>J z`SoJn-D_ktSWE6I`ywiyiBPg50M2pN^beG<-K)%33Z?o+X|=+v+w1aR?L>ew#;%%~ zSLvEvA+05oi#8mN=XP2twUme@bzaZE7{zTZGTu+oI)_8PiZq7@13w3*?(@&T8jJ7N z3{g!icv;I3AKpOO#h!8dj#cxGrbo9#@4sQC`d!oTHl_n5D+}4Obj8>oy^;Hy-7~J2^a>M+H~r({*BRa8xuTYfxkA1x;nY*oS>5B0a1ERWR}edN>CY ztLR-eS~Ua8eF<^MRqpU&M@2234*TwpL~PC1bZA_4ks@JKhDQuVnlIyzA3DRU&olt+ z4sd6o?nS$|)t^eGQX>YEPbcxBv!1hl9M+r?MJ*$oZkF%gjs+Ml9U{Ad{DfL+IaNCO zft7_1ll|kywpU2TA6ed^EDYs{DGGvnQJ354A)Af{ZqcjY7m+AoW*+Vk^i$#8=tqDS z(I`ZHLUp!+Qiol0A(KB?=OBXCn&=f8NQmTI1wx}{{9R6 zz(qz1sUbyU&>1C`r9xpEj6tc(ERD<1UxhG<^-6rhTi9l&PrLK-NFeD`k zagO(E1>MsJ(nqhp5~}5no`I77^*A9|aZhqhZ9XTnS#}vuKC^Y}v0&3!=%p)USw7Uf ze^GvPRJEW=-|(Z3V~;^1EPe~(*1EA$3OJ{jHa}`G@3Oo4Ha20{9HngN@~Wke#?aWg zAQSI&drk!dX9@_dl~-&H^Jf#xw>@)oo{dA-<~5C|!D3bJk$|-R?0``iLs&6Mui6#P8 zQLlpdyk|zFR_c0nLmmOSGP48^8}UY0%jI+1MHhaRia!TMsz*0I_C(xv22iMq;v0%C zY**j7=c}`KlJTsZvT$qU7p9}fC&W#1?crG1O@22|nE6*YYrD(KqaGqgihyV4Ni;kx zR~K|AMLZ-=en%F|b7JA?%C>#tm6A!M1ks|jexJ_2scu9mymxTe|?>pS|ODd9NIHBm8D`E6x00u`?) zB?x6hC|Ia&tn94jYBVbEpfR1+!ZefP*GmvH%wm=jsY0sT8T0&Uk3-IzugpBX+U<~x zcE2ImU1OA6!RD>jd`7)`l&8)01Zjm#Aeh#{8=i=+axZlrRSuI%UG2f25+c`CZY-)K zY@OF#)BW}9U1o;}P9>+?bh032PVI7yZ{Zk8lA{wh2sT*=i*{BdLKW7^29WH;JC2D# zky=`d?eZ~9nfXGo{4oJ!|B9CWDF?C-d2froZFsircS*ha=K|MX3T%>Jrl zlD(n*fO?%~#1F7r7rahqFux{fu(H4<&`-HC=Vy-DI&<-xBZ4|qC1Qrgmu&EBP_Ij- zDM0GpMGs@;X0kZ-^o-j#l-%C9Gq@ah#;Pg{AQ|1HzNM*ynsdLY=(X84)vYr+QI*(0 zizWBmDSPKVd4KZz|6}Q_`yw1+fKId~^_jSE*BAR#H)g2;k#Wryaub^mB@l-U#Oq_N4FQSoJ&9M6h&(y~KUKOd6*?b%@eca(m5R2nAR12Qw0Yib}V3 zT*sE=5ybARb8j<8xAS9FkPH(LEMG}g^;;Q}T;9DX>6z>ZIj;^q~t&ljX-6*6*tujO(lP|wqt~tKdYY<+8wK@Cd@<%=4cF^67 zkI&Xb1W(MYAURYLu}Z&DppL5WST{kgOxm0Aak284prN^R^zzwUcrjBT+1Aqz+y!U{ z$$&uaegum(pwQky@K#?kmA~otG3)?r^;GIXxU2U>y55@@wt%#ZL|lcU4n?jT=p~Og zUKD*H>D-p@Vj(BfDzmt6N33Ug>Xpy7NTZw++#C~l)mW~Y=zcrDReJv1uc=5~Es*hN zCm3L8;+D@iu7Rzwvfojt*S8W^O#ut1o6)($Lu2wbj5ovy!87)O?*to|SN_ke#+QNxylisw*UhwP5(42$b1%Ms}R55F@}!2AA!zYAxS57#U-O^Og}w;@o1x> zIjPGv+O+(++-Yt|yw(D(jj^|5r@<&w3V~^`bHMzEP z6aQ=ln;UYxa*$y#8sjIqve|;2UG%GVHy^Cnd+Uq}4IBuJ?{o7u2jAS=Rt=jr%_f)E zA7K$Vk#V;K>andqe*N;TZH-6tG;f2v2_9j$kQsMS7r)? ziFx4K8?4FrUx&1@CK(Y4=X9m~&xmpthn;$2K5SvtCd4h!UX87J9~2nIfQ4)xo$S^jNfvVhhR>A8GDbygurvN^JDNP6DoHw z>a(|vfmjwY)k84n>Zpuol)5q@=7Y1Qjj{Y%+l7ux;UJ=?ENZ=m6^oMpC*zQ2JHgHL zaPgiSK6&B%7FR8@_QV5Zzyeok-UHoaYI(RNGqGIDPpW8EGM1+4+VOB4zD@Z}5G_C; zwaUtc(=<9epAsBl=)s+&K2+!8xTPw7^TAOQJ@FnHwPvy>k_KK#TBlstRoynAr^vdQ z5oI{^fGOAoO00-SI967V-{E67y&mcgkXdQdi&7ZaVfK5mu$I^>FD{>@55fquq)Q;K zW8}{jZr%@Efe9N2S%tg;!>-y8-3{|e2<$h6e6MpruEfazUIEtakDnB`PGMM_Jr;Q7 zdg!XB?Q76+w-XuuPQhy~h8XzJ_Ql&-e&fgBFWqs}YEk+%Q|GAc+I*glpZ-THYZX(= z;iLuf$-UVj$m>&zKa$fUqBcz?OJ6o~QEODJjv#6B^bOaC%>J}i#;e2#>dlX)l)O1$ zykb7_(PhDAT@{AH0>Xvz^|I4DjR}(1+Od1zVN3r2!bZLR0gP-7il3Jbysw=N@OSy# z#0rs*E%$XGD9T&+ptaVSXI@Ad^liU-%>Lz4FT+y-yiUB6KW}O)g!`aM%D!gXFV}f% zaWUqP__0ygGq5oa^Q3y~wr6DVOl#P7$5tK!hRd-qnWtk*pDs{YTFD%!?F(reFX=FM zquPP(oRZw;pWKK2+UDXEB8adyQME_|mP2fWvXh*2Nl!zsINICzGxX4EOXaOgt#H`n zKAezY;X}N}=50ddi0%Wydm6KK_c*gHfyqLD)p7oyWT|Y1i*1msZB3LZ=iifkwqDre z8w9~ge5~sXe%1Y+d&~F|y+rsMlRtu0&lmS?w6~eaj`>8Hc2HMr-*^~Ea0 zDo?w(LFmrk&I^T(nqX_=!}2Df7VOnE%WkrelQGAm7^KA6MYK}Y$KQ_n3+A6-wcG^Y zCX1XmOirt^XfiI`Cm9!15WB$`-1cxo_QM5=AO5BeVu(@+!Q)nLV>6t56&w`Had&AkRoGUif_(N7Px4E)_H0LFqgA z9@1SZc6&S(e2PLWXfClS*3|zpDn);bDq`j$901`@7BQQf z(5MUASG7*qWnBgH7Kv?(3~IOrNZ0Hec_Mbj91QqRaLlrkkkMZ-+0m;PwOM)CwVOfp|kMyC%M5begbi-_%~>&yB=>)atfqC~Ch zY<}MFnPoYFrDVyO-ryb$*L#W$7)Qbtex^=O*|t^O)9vq@A)V zc%)rfL{6Aa$i%LBvZy{J>xSJHsnmNFR0Ueu}JG#{Sf+x=9)2kC>IZ5L&RD?3F4aUT! znALZ;W4Lz|gam49{l=I^A6x$IK(fz$_CL(i4e5^&-%^ z+q)bHW35HE{w;1JiZRKkqG*$A=PKH+OKF?9F1ty!LfnHmGi4bmmUWH+2rwTJTCDiv z5Fj_U&ow!ltn3+=c1dfO*!5SXSS$um}+YIfA8WA>MHY#_h~ z^UAU563ZK_7$LhybyUOsZdnwF($46iA$oszfK3TX&8OL5+2(tk#Xz!9gk}Co<1Q`q zl?3}A!N`n#%aX@j;}xbsX$?|glZI}GNK#*1tc)(s;@#+Q6j^^1U2j#v+lc0<+GO1V zy)7R=JOn83(M5j{i*JZJVDBAj7$ccyTObxr3`OgI`;mO@;z>(OC$>`H42FJ-3UaMB zZ4wt?e>7;*^3e-U-byC=s#}hQ0{x|qZN-9JpABih4PmrklxkRJ`u| zPwkvL!r0zAzvNI!Dl6<&p`*PeCt&0^J=;%sW?Jx?jB*jd>p&ifL{bEAk7dt7LXu{q zoP$}1P{pB*W{>T;@%JiY+Z75T1Gl!=yt^D5PN_DfxrHO0 z`m|5g3bBi&9+~Nu64g&Y4lU@QSQq|#qv&n~#!6Hby_rm&BC<`-3qn*ia8tg9=uH5y zK~GKMsp@eHX?Q7(Z8TUf%voq$zObdQItb2i7TTINn3gx7xW0;{QL-(>da)0ws2J9t zK>}CU_D!7ExwDUl)ZSCQEHM_u|1rbEZcg|&meTp~mH!z=4eCW^ZVr&=FR~ZAvPvO@ z79VubNF+`Q?%B46bIe<&Gf2tjh)-ouyQlvd#QVLcG5Dq1|M+&aEEg3_AHMMUpI5`l z08g4`XPPB{O$UJCKw!Ll*lP6D$Ez=;+7)isd=;3aa;Mzee2khh_&Zs-!_w!d@>wa8 zYxnkUiQnVnhNEY9{Irt_T9=?pm9YyKj`pshVAQoCa28ip9aVJPpn1Y!!$?Qxv7T>i z{h~Vj+T|(061-x+0U&en(S5%1^w7^FkV5TTSwML=LG`8wKBI1FVoM+q$0QKpq3cR%`{m~tJuuX+L1c` z&1S9cN=Uo}!v{Vt|v2Nb%E&fi@i?;%~m^y&VheoN>QBMZ|wr zHMjwb$dz13UE7p}v>Xz++CVe0vE9(z{Q6L?5bQ5p-0ZOu4yu=hZ~eJfzT1SgLXBZz z@xwMcUlKt8((s=;@^JjI2cfYTyW3fCa3apCt&<5fzPF7}M0*TY;elzK;h@1R*V>!E{JM`&CL$H}d;rDA7qapomaMzfC#;v`J z_eF)1KdZZxCY`1*w(Ychn$eFUYJ>y@I-7Kk$^K!A_E)N)@?Es(-a8uAQ$*3?$&yZA z1pz_`2_R6?kXy*%oKF65r?fO*H`IWx>((L5W^W??%$K8ZJ)Sp;-9CF!6b*l>#^~E?Rl%?s_UE#-5WwDKL2y32g0geq@vb>h~N-P zLbRCwh+{JR#fP6$ZIy4E8y?Ie$+ckUy!4*EV(y(i0UV_GCz^(AZ{cj z>D-OC1vM{rkl#cHMgK2XXtfdC&#f_72re#C^KBIzSWS$cUbsZ=OtIVE z;mD6Dz8PLZKQ(wP6A>EXAx$_-u8j1(Ju-={5ab<^m=Iu4;H$5np5forGQfJLskxGtp0!p-^mSVPZU?LMOM^A1LYONl&80?c(tm?o5mC z79@bLlbI22PPzN?Ya1)yh*fYA?rO4&*}$eg&Db)B zJtG~O49RCpkLJzEl4Fy`SKV99ER9a$;%*(^(P_<4(CRKTET^UieCLnPqH`u!gR6?e ze#SdxGi+`DmIqfg<|b#1H$2{gKgjlit3V`4RJ>t=n>ZW<7(2UbgGP4T3QKwFx~0y) ziRF*~Fk;NiWxp_Q_*nBFZj}l?`X5lL{5wXUo8szCx`ULte&z$Z znin{EmOCJ{98+Y}`&KA*!!4R7QnnJACugLs;5b0->1YHVv*Evh*yywWj`%M$w$JKc zJXM(%=0#?b4h+`?5)$FFMCYbcKRmPbF$^DGDaWD(@$!L_=HtDCLMaJuV!R!-U$cGL zxTq9yU!dU+?T6?leBUJ!ryXLWV{>Q^f=7QsEm>t}|4LnPY(O`USTS&o{zy6rdTJIz zg|d~#hKN3|LQ(xo4L!B5o?r6n!)2`{wdBeO0Jsak^yRntO{2kAhkb4?$|2CR_W~0; z4+A1%F(eV<4-RpeaL|HSUqibs)V>7!iM=98w3Y#kix9`2*c`ip!6VJ>`qpinW}m_7 z9zEpTQ0Z}O`NlbaXD=JySyk9anD^K_K~t(HihEPnsWJa^;ATQ2=qS2QQ!G5&!I0?h z(`1T%mSMcBmoJ_6-yC$DlLRuhhdfF8lx$V^FYI)>O3~`S=O<{UM}yv^F|XLD)^Hrb zPDc55;Pu};AqkZA1x^!-UTn=@$#7%A*kA3hF3MAt6Nem z!Q9(ptD|4n#f;}9Y(@kMoXA$~NH|oPin%~5(sE->MLSWgB5QYy3ax96^T`u;t)|A%yrx0FruB;}@@B$c|ArsrllV$&0O zeP+Cd5cZ#~uPKvo9vc~URd&B07C91(e-5O2E(sct7+eq=1Xx%Uv$2VnPUSaR7JODf zs+XWQ%G5q1UCPr}`eG~jw^vTwyC3zipR=b9Fb|3B$~oT9Xz#u`UA7nHG4k3XRr^{$ zu37!HD_^1`744rfsa`G-bodplCSc?aw#QA!X_Ri@^v+F<2HJUKH}-|xSSnb_{~RtB ze=MiHC4N*7Lj2+zn$%VVhyI5Y21|)vxlAppG(1it7PPGe$vv~|Q>^@ZXs97jiSfK- zi{sOyzBa>aL^MOJY7c86!l7L*wbdW-=TaQBY&2Vb#H|5K*#Y81Y_h|Rqo5tHEAO?o z6Q0!Pl zGxv~`Kbwjq`{6Yp0#WHFpm(ele`l4}hS8R*FdM19|NUp73Tq1i=;XrXb&`k0l#2`b z(DvzDre_xEA6hZYMoQNJrOhWliT|Wot_x3KsFU8OkXdt6v)M_^?>Oh7Hc}1EL=!>+ zw@GmUHj&J42cVsS|?|eulEg`b5QO*>2n1yMAui!)G_nZw;eP z1lIoTeuc#X@c0*9scve}Zb$E%6vn74RWX5Q+xhSh0Oe4Sz*|R#N!Mc5C7$|W^F~{> zJn98q&O^Z!cHyUjmJTlg2?5do_8Y(_v7%C(f(JU5H*au@Cu=y0#1 z6QVze2ZCSIb!``OYZ701<(?aR8Ty69^w8;45W*tl74FFau4&1LO#4FmK%)ZK+kN5B zOTnMk`wCzKx;Fd9V+n*d8b~IbzN&a46Agkc+2ju{o+T-LCREJwwmaGKNzdiHu-{s) zhH}(VnGubh5`V}Hrz>dh){%kpP(@*urhh}i*cb>&FjXqI-5uG-() z^6VT8hl5&BAUP?3p39Xfd&mQGV--~|BAu=8@Sbiecs>L7aP+1{I!1;0$mq~}_&`0h7 ztz>aoz9IY^aB{TcwW|^8>kT~qT)!u&W85-^y7Rv8r3IehxW#JB9w8m0$KY~aiZ*jF z$L#n!7pX9W)&^fb@{`tV5b0_R_+3sxjUrMAV#kC1@D)0mp<^7; zFw7Xdkr^%Wu+yb~Bf3Z`m1aj$$*47m8xlZxR+5mW-a(Xlv(Gn#W!EqqdGuAzZ(9pd zJWh5Riybo~G}^Fmcr&`Z6H@RN5!*qcadJvlJbO<%_4O5>Vdl?0+Gs|omgd)5cK1~#^H$E-C{F@ON4e^P&x4kd7Xq?8IbCBg~&}!EW6*G`z-{9_F*@eek z&ADh)tR3_VqQq{Kkl|~I0nk|)6|9>FIIRFP6~=YuR*L|X)#~cb zZtvlPG?T>!M+Mb9d@Lt)WqyE&+iwY8$C|=uAzGr6alt=h%I6LYpBwX~cAuRJWaz{# zdx6!E9Lg11N(j7s^2JQ86GCQ3Z<+NFvNE~6C21`E;r)TK~0+Vsb$n3 zRq$1#h?Qfo)WBL_QDO6gDnuZaS?t%6em2DzG9!lQU=@&u^oG^7t{cI)4X5QHh{$*! z0Qc@+&9$4p{4)g_5@mbE37t`r;p=yiF&BVGS}V^wvRw2suV`skES4sCf0g#wRD8PU zg$4bv6j}UUHUu#+jIFM#Pkd6fm>w1FO`6D6pBA-LM@u@GND&s_Fg*L%QTjFGapNtt zI9izdj{^l% zp~8myWRe29Zd#S%OjM@;AGdh*%aFnTq^JnnMRfK+_co%rS*dnfaHn-WDWG~mE+HVU zUQSwYhUq-TZoNtv^WWUYRL_ZS<>`r~byUqG1Y{}SPYl6{*IHzHrHt_cnw3JNO!R-7 z*D6dsw885Jxm<)gXb*hHwLD3X1-bLAgOAmuN#U|XoZFcSFCSi+$X9;ys8mZX0K0WL zwz&7v49cfw(JeCwE|e~5boh>r=}8P*;!CNWQ<-WfqUaSlN(rK+iu2%_H_Tlrn1KCj z^ede~jRhuoh|8n%)-A;&1}^n2!F-!J9^)Nyulc{z$Vd!@Pi%H)0>#psCa~~IEz;CJ zR_!Gqn~-*ttWL6A;)Eee`t;(HE354@MD;P6D&!LM;LwBN1-1J4Z{VUN&A%VazaLd| zu3q3@fkKgvk>hGqQ!n9pgw7Xl}im)3m^nAj=#cFd`}j8=G1vPp{9ZJ^e`hCJF#2mlnhQLu@N=xIhuR3@zd)F~Y ztW|%ud%na^pdXoEZ?PDc#V;8f@6E=^i2n*lW~7|nSISuE$LHn-J!H|I*?FF(B#5>&ZO5`}Orv5rR^y8!X55=?OZc4Qn5q zdTaP4f+2>8$*5j&NUoFV+0c@8$6ElI`i;eeQXK?y4@Lclp#OIcg(8stqk{h$@As-y zqZ1|rSbZ8DWKsiw)AYPO+{T|pFy|~YeWHLZcZ>S*3l*DM%K`>98}P#RZ(XWbCyo0( z(ksa|??QzZJ9i<5WU8@*`b*F|=6+L=2fYANk5kw<>g|HA{sMPAr76KCEI%r27U!&> zqAD%(sWEtlq^4(~HBokzSDT5qh#ahoh$j_2#D72D?le_*6P5@~^$%1>mY6TOw?UnPIM%IiA27m}Q zNp@&JkvuT!Ko*<)0!uVTT*PLdYw%nqdO%7DL9RP6V*pf06>J~V|vjZB{dmKW*61`Ffo+(b=Ws2L^pLh(t-62mv>NwBlyka znBhS)dW@p!nR+|h2(phUs&@5bvAq`W$A4S2t|Si7ZfSQ1NCr3uDS7tW zjZrnGoe=0yUp{?vN!9H9!%!9fi}?iL-uAKA4tr~Z2?oq$QLOfENLzZSENMSKV*yhE zDPp$dLB~s0b3(!>kr7V2D7%nWJXs)V+xn;D@DA zSbkm#+oJU?T&I3F>#-)sZC^^UV%~afGQ3N^E#)4{uWW-;n z^EhdwfhE&^yB_R9D3aEb&jg3yQlz`>XQmYJ7XNIiNm2V|ogSPCA29g`a0INe9H()6 zkB#h1?R9^N5FZrRjrZuH!{6^X04Il;%~72|H=DtWeC=o9SZ-OGTmaG*^Y*iYeP6GM zX?oqq9Z^@QKZ71e94`R!qdEu2M1OO)`huNFBpS3EV6Y7?qv`C8&GYvfn%ByQDhH6v z#wqAMS6v66!NU_%ZdzPCkRls8%e79oiL8lRp+X|J!B`tJPa$4IwWuK#-tT9Z3+LDR zE!zf!a#7Wmf6ppA2R`ONWuNVqhb@4kDJ2uRE>r-ReSx~M(c^kknz@`^6&T&mkD+|J z)y`t{Uwx;0k`{V-=?BAg0alrmj!1C5xDv-ri9hLTq|r1iWD zVD3AVDyY^w@$<)R+W0d~aRT@w0`bd5*uwJ)IzJ`-wcPu4k*uIepBJG+h{DI^Lpo-- z-t6qwE1Z%Cm3_8}5=tmvN%2CQIQnDuq>ZagIpyzx*B)%H_4mc<-lT@*u3NrozhHlm znxgD%1O|J66?~asC;!C5-O}xEY+4%47s#l-#&OsaW%Np3>Cj_XHzkGPEAD z;lL>dQGpVHb(IJfj%qn|zNlqkh|N$gZAEuOn9j=vm85-N_u&lQ$;uuQ;^MxdH4ro8 z6SFMYAk5de@)VT>hpsQUZ-2{tIzA8AMVxKpT3DOSRFl z#6@pr?jS|3nE&31eI6D)kiUgDsYh!Sz2*806Uj|L=S9sRj)J2e&N)ETMsC{%4I*a3 ztfo(jAk{JaIFg6b_o43n`clPkckd&c$eto+N2HihsN8ABWxvP*4%6~2UtgVaQss|2 zf6Bks>1BD}o6CX(cM<8Je15~ zX{ioC-o(w`ZUFzBlge<|dAvx~9xkB=a~PT5wWz@ei69YMDxB?mV-vz^z}z~jCz!kM zyjV2)>C(b-Zi1yH-glA9$;A8zYbJV-vVX9X%tMQk9-YaIu@y!MfE2pJheRREtcIwn z89v_u$ncB25`+f+Pw&hC2ixC&E7p)wQtow_Bq{&kGJ8v)>ZZoVi@FAlCQf(rYA6h>Xt7AGn)eO86-Ro$*uHvfNFgl|&ZRVWrsns}YlrszQg^U6 zs|HumhD-qxvxFwoSR|9F7pUpb+t|jxhvDKKpwq3L?^a{XwEEP~Kle*uO8zSO|F4@f z(^jd@=|1yU{?@79*j1OOwxQ{=e zpNow{AxR5_+Lm;)JBeO0djn>gpND&6B-GJMKuii=@Ua?*;0?61`;}B4ju`Ws^Ew{HpKY5I24wh5>Dw5@~CPTJQ(d96$b42bxk@*cF~T4xYv6T1k_ z^1d4`XFst{>`sWU6pTY`t}?ap6kHCxyb60@tvj9e6Q%CL<9_P}b|-)&KOCT%5+WV+ zemwGvDWA#`y4esur$O1D!We4sYJ2gW6FE=6Y_t0nCz7&FY*jrK1p6!OWA>7PiM(%C z0n(Ut&*A%sQc3L!eybpCFj@M&3+y}EfC2@{qJw;S<@E{wLHG1azqHM6=a+CF((r51 zb69a{Ql*-bv&}hG6(J``&9`aOZ~&s=q_VXee=;p0fA8H$7w5n~7rnYoX!uoD4qF4y zLAt;=q|IQkp^B0rVlZWG5HiJ)F*T^6G(bP@jq>ml+w{q(zW6c3Tg+`_OUs8X$3i+l zS6;fE{*6e=f_1p2o*3HD2fIrh8zUts+7#@>Z)?GA0i06g}*r7u*9|goCy1JDilEkq?6?x1g^Ql*u^@h#Vkvmg}XbrRNEr!jFoUMz}$9bP!k+lc&rUjqw9`cO*i9ON{^oN9h zKw?9pL*g=Fqqh_Z>AMH|?k;a?{9xKny7X#!_7^Mc?&V(ZtPXS+H-#K4g{|7#RAL;} ze3!YfhU{2L3MCtr^~@`{7H{2=r4Jd&+4hcaB3Oqwe!gCDQ60TP=?I4&-RJDPSq>5C zgEk#mIqNro(^H1^2Trly$BlVU0>BV)4LgAW<|hK}-!EpplaBi6{Q_IwBtNqx{ehVu zgqu?xU+z@>ic-Sy`s;w9>P9kEBGchFA$%QPC-Q<;t=62HbhqCc7zBwa>+#9zQq?e3 zrb`m3!njriw{otZX2<2hqhdu{Zfy>X6LE5UL!{UtjUDZ6cPMiU_u5J?wi9QcPM`jS z`u~zH=x>IrtoD+ni`X@;MdhO%X*rcWL1QS1*v-cd_tbE+6AAL56JXFTOGeG$H1T$CC(#gze#X?1EJ)(Nixw$>rP^$qgAbu%Lby zH-5M5&Jd#;bLEjHY0+-~Cw#phx#&1%h#<+>fe-S8I4iTk4enL%t};N^-iLoJz;F6k z*XNBLPfqdy0Hz+tBBALDu(lx_p{gy;em-I@j8UcKFta;k!#hal$4nm^Z^Is2^?)`% zCuH~)G|_QmY2bp4dq2d#O30im_s9C%RSw#pMxDlARrguC6pGWm<{`i#P+_M?e39+F z@jW`xseDmUy~G^a_{s=Y05?=nk%|B-tyD7CK?VH_mZ%qF^8V}1H?2hZ79zl*W)+RkyPk>9xD`o8#( zs&n@`{pKdbbJlfp(|p#W9A?oA`$VghW`N=3qq8zoulwkLL|&i{j7{6_}SQ3KvTrYl{V z^sW~YX06x8i+#+?naE{$x$t1B*(Cf#AGUq)?AA3-#xN*frra`aV#2Y`d4_vmj4K!3 z4^Q%^vruSorn*h9MS=riv1d+Y4!c`0vCE#YT-%x~d~bQ7!Kd*jn_rmhI*rWZQL#bT zOAC`*ZMPq8riWSzOxfLS^Jz(&m_DY1Gj{k}S>l%=v7STQOYA!i+2;0$em*6)N$g5s z9kGLxABnWv05SbU16{Jw2HvaO7u0D6clRgK&Z_#nI+W-3R@`Q_^M>vkHJgBjA7!!p z7)c-8)KRYcBff9=*qWui+*<3HJ?{q=K5qWa7bNKTiFQr5`qMuRXv?=vtgN+ILAv={ zYz1-vi(6uEofXK65c*)(WjWv_`~z!udjviM2FHkM@fgvSk=j#SKDX1p(@R zFjL@IQp^;3!_P;X8cb*-s8=ZXVI#yG-RtVt+K*?6`crI z`aJ(&TA8!ZvNMhElQ$oa9uZ6W(ntP|7U*8I0A;VKZt1@~iFI!Fc3+`tXy;HC!&pwV z>}*chq~61$p@#46Iaqj@78eJO)TOqQU(VBL+k^q{rw5!G`pBqpQz)Qex>Z@+P$j~X zgsKA-_wJV0irDb}!2dPZu-juNDr^t*Y^n!{_{I>6PCApWT-g3SoCo>?`nE+9p9U&L zHmY0ksHf{kDI+r`IKyg6tHt=R zNOQP$;-v(&21F|V;1<$$9`Ek+YFq8GLSO;nl;cP>wElUwwcNi4+FOoSiK5JH{!{7H<9jH1fdOY zKGtwZyaw4xkYEg0L!RT=KBxe-IFas1p2uOvWmbRTXzzjkdQf>ZF%mTPsQ+|#QckVW z7zn(z)6!?bJ&AE`*IYnCWeOVPeqPum-ivDXGphZFFZgcRnLCd;#X|hTx1RgP3?_}@ zB4aKN_F+$B9zEMik!srNlqvz>BxL4IG=QC#cvEfXM5KOqX(L(4DHqakQY5zoP%5BNll$553cUF3N9Qd?)_ZA~wp)8uunKY1_@Bj3ME5R2KDTzU|`Lu~u;l zf8xQrQPJlMB-{d&vc-geMiqL7=r&DoSYu)5<4a=R<3oqmk@3kRF5|DAq5ay69Gi?k zeLU$F#QQ(bnU}RF3AyK1K?*RBTWmPjKBn#eDDYPgBQ@sNaGy%;9kw%avEbaMj%t05 zh1Zv`KsdI7Q(Te15>EJvK>^ZkDCNFpSsVRPlAUQKr)24`dJm$?p>dy1r8ffn%8cp@ zk6h6DfPEq$ZqbF=Fw=p6`t~i)Q_G}HG$d(1_>`4CUb&e4^3b)-dRcVGxM5+DmEg6_ z@`r>(s#smk>6C(#Mor$v;6A@DbV%^z#iGx&uqau7a$Vaqp2Lb>bk`RmgMP_Xdpl*4 zr_l$!8sDKNW-3PZ!9iur{5Qm8)6H+M2k0d!N%F$JHY`1Dkpz7atM7Eqe=rSZ+l@21 z9C@`|8oB8rqa6|ypA(sCT4xJ1ncjs}k1w!raXlFB$6+ORe5Hp;ZmIpf8#pB4Z`_h- zdGWWWj#fed^i3G-nqpB~_zF8_S8pdqBBjNRz7|OyZV^l^pfkgb&-q8-i>6`Sso^&t zQpIE@iH+LVc%9gRjGggfD`63wcsi-Cg;XOQhvQ8 z$>S$*nsIo7>D8xob7EDuT~(sDPT*k2>N+0gpRv{Y{{Xmz88Ndq|D2kJ%kca~R(l!8 zuvRa8m4T~bBp>5*>+&?$Ul=AVMkB}`!OV2>Y)o-$EY%iTOpf1(iiTcCzly0`goH@b zQ0_1Zqt`sIgh7OD$dUIubV zZH&HP8YJwz*T2<8S%va#eR6j(=vCH10|j&rI*QoNsfc~GY%a7cc(Xq77DLN}Z6 zbG0L0wSNGgAe2yfaFY^dx|#=mxjOG$MeJ3vV+!m$Gkle$s3ssMI6de!?E!p0Cc|ZT z!W+}lU}dG?RSW~pJ>GI3BL;|cF5u6MXBgV4*c?ccaC>{k2%9I4;Y&rBK9Un>p0A$z zruN$k7;O!ns)Eb}U*OnU&DUD&*XK|okk5M8;Zq<@PC_v;F_{mpftB~A4mcnj!wOufFO#mx@3Et$RVivf%g^mleVARX2yty%7>?#px=6bKi=kn z-$pXIos^<;Md`#O-G<5~wyU6HYxR0#s(-7BJrJKy%RTXdNq5g`B6Hf89%-rSn{~Wh zFVLG<-Jm`Y?ULrTJj0o&)N5mHm zzt+$^iOahAGF>~SxhBX+OvCVX!#u_Id{lH`zIh0?wXtpCypDGvkKE#v z+|szQH`;HXdRn31J=HhZN*+NvMWC9(F-O{1F&xV3V^J(ew#C>W>+Smc_oPHT$1tQ4 zso7{`0^i{&qbAs4m5cSnj9gf~2TgcfXDHeb%MyWMF}TG7nBjL@oJD7{#`@;zT;2!% zQ67jgS9a7UHc?PjtjK=*WPOBZN6MX;ytkD6tAz!Mz&&NnHt*+`GeAL`U;5B3G06ARZ4e{9<{iYe{TEFq7frDm1e1)j7yX<_mnFNgCw-3quKY&7xHYdMg zk&qk}U66fv>=n@#OK^QJMdN`U&bLL$MyG!O+9BWwB6ih?W^?`>-V-W zwKh?rCz8jQI|C()88wmW19v5+yQgzYvd$m-n9epuwe4at5)xO;vqen*k6A9~_J)CT zW@6`TD$GCeDOfsl<@x0B>jKnaE%n3)f1UR0(YqzrcGos-Q=z|x`#qUzz297TMdoT4 zBYOodzt)~uKW21F#EMJEY%zaSuu)!OlH1Icn>2P-dfNV0P z!U|PF76jdzAZLs zINWtN@IbFRQ;K8e{gof;qj3h0UenK_iB!kZ$d{+YKIA~@ah}BEd6{n}1x=1Eidm$~ ze+oImHe^{*cR-6XrWA3`h53ZKjQC#kLW}D64NYY)zO7dBR)&*9pK+=c6Lcb}WzvsO zsr_HEcb;=vtRK=#nNL3K>|f3%8d<&jlSDsnN=d_5E4ju1d_npTpzc*i?be-bFkesU z4rf2i47S-YxW-&5R&dz*vzimEHm!^s7Avs%p_P}LZqfUX^UcJv2Zr;m`rA{Re>#rw z4K*S?97~l}64o^8*P?HdIb)T^di{5W+kMeKF(Im%BBLd%&0Dx#pVF66>%K$T=-qYA zQ$urHf^yEp@%)38qES4>5{I3F3hnnDN}s!1hbeT2|0FNOZiHKaaLLOKy*T4 zPbsNIN!>va)IUOq10r`EofmaGiXAUM%?neq)@_Itkk?W#aJ&JY51^D!N^i;;yw@Be zCSp(N)ta9ABE$-2Z22sTnJfXzVEklmNq^m74lC|YuUeMiqJms5mjciO{!jusZf!9Dnc)=VOOI8CqxNxp z=VI8ms*k*`>HWU#lti()%8ul0x1MFKG*3dkO?=sZP1`YCt9f-lHRHmuXkF7O$J!HL z)EdiGvKcFLATP5o*v`f+NbA&h5yaH*-g;?t-y%IM{;#I9j%(`sA3rIbf`GJuFiJrM zMu*hsfgmZ3#ORPlO1hg-!U!orwh;o--OcFkZiM%DetwVNpF8)SySux`3qN~*#LIkMzyQ=U4TGr7AcCa`%UiM7zUl~BBH`Zb5%f?)lu5lwlcQ-+n1A3O zQG#tyOH_+n!H1u;BFQZH^Fxq2#_`XWS&hKknu9g}Ig70vaFCU63aPm;a2du`0_w5b zSv=i*${JeUp(KV?&>v{^eA9Aiy$HE0tkab&q(a)36;W>c(FOTrIOgt3!q@50c*4Y? zcGT7u9E^d689&%^5E)e%7CE-96YKN1tdjLJ9 z`p9ClFnP~E_Q)9=(XYW~>%d;BqgVb&?;bY4P6*jVg&fN3;z#VLI0ol?GvKm+g~k*W z-8Noy{)bW3zof=iiNKIR??OattXWxlYd;)Da-)5tDtKI}JbwI*VDmEA_#5fNEaB!TUv%-HEu*-+8 zh-YQs!!t|vE|;2N-I6EX(s*J#8IEFPT3Y3m<^JOw`8s`^t29x>IL{7vdkL-h^GJQM zQx)kP&Uw9)b50Scr&?RJt4D>a7nAQNbt9kk=n4j}5TdEcs|L!TmhN946fV)F4EzUP zoOV0M@91#n!hj^_p_bQlB3Mn829lmRL19KCUs>%~6i>0xDL+XBJ=$!3D=vUjvpOk!=a{O_41GaegWTB1A zHk_P1F=R(FvvefxnesIWo)>tvag9}UvQ9K{WP^K?=ZaQqq$D7fqYI&bti7(0c7M7E zVB*5dc)A~Rc>I{X-2`5Kq5si`7UbyJ7n{at)rki3u;E$xGA~m%uD;TF7Ug-$Bime1 z-IYMqM);VcGFu&je9k{S+#Ghl1XS+ ze{BemW@W!Ke!rld7Kz+b83|BCQSY$4qsr%IHE~TKnHw4 zfhKfF7p2GNGi!*;*T6+@r4#zyG5z6y9G~g()NUyv_o%s_Z*F2z%E`pHbXPeGSUeBJ zoc6T`-qBy~imr8aq%$H~CZkhP?c8$x=P8aW6B@XodlY4G0145xsPpbS*75NZ9brFS4NEEZ{@11AN0d)qiCtQ>%Lm*Z zB3maSZj)7gPTD^xpTlSjyH471OMcc39*7@w$-zPRMZ?|gE4hKmwLpYH+3YaTfm_E)se6}0Ub*)ID0VTt*kx@nLm$sg`5m3hTF%Hl z=Etyad%C_SCKkteGFuZ`gdx7e5#WH?!0e=U_ratj55N8!Te#@nvbRHNH{l7aPWY5g zx1aV%p`U4&I>mU1r;9cZ$YB>CH_lOp=x}TbM;Fj#vc#3Jv7rvt`7}CtAUP#3`uul} z8)L{wICFrE0d;+2qE9%cPs(djNl=DSWZj>H-#A((3194Z9?0|VNBb(7+Y1voFH-8H z!X`IXrAoYT@UjI0n0D!8{)I{8>P7xGbJ)1GiWpp3DBdi z*XP6cZ4Q_Ik$W}{OLotlY7`Y3z7!-MH+->~G1Bji3vhR!By)7I8Becp<+&~t|{?d(3?Do4Z{z1z;S1-(n}T} zhayn;cz@Ew=Y9-qNrx4Zhghq;%~thAmC}nI((la>86EFxL9mGxdt>1!SM`4K%nUVy zOS0a)2Og=_Tn@<5S*_CXrC*&}85sv5rnl5EKBoxPudW=W1;j_26Q$%AVz(T@&x%&o zF{Q15e|Eg6y4U>UBr{fxPmEsFW!4@JXsbeDE))GAx8o#CyoO`-j`^YsEqPaTKt&$6 z^hR`yck%63N&ScmpKQjxbJ9=ex@V)hh1aL#l2dGZYRXUEt>KnslB!43z7So~0&t>f z1)zt7bZ+xb;0lUtD40<)s_6e6WwQ3cd$30(WajyBsM}-uhqqS*b6rpQS78 zZle`hA%2gSf|r?JyzLlCN)E29)PAW5zC7FHd)rE3KLSDS=8c!@b$SHrNJteIi+03* zaTIUAO%BR64P#+`Onpq{wY9%TRH{%a_nYGZ`;BI~&Q^}qs-qayW4#Ik%!ycwSCt`i z_>$kadM_6(E~`*!z>yEyuzyh_J)+eK!+FlnU#^vt2JO3ZKxX`c1X}*NMsI(U4NP+S z*DlwWMeRDkjq>cZn>}t{W^23tR<#sOa#fIM!#*Bw@!e_Gd5oK+sN&=7vSd=G=xaCm z{AihXB!Q0yHxUyTvxbdN<7L!0-F)Z^P9y&nKN7~ujj^7|ZgELXliJcWKb6+PwTEk9 zZ2kU=GCB^bC@f%tIa`7UIiG=xNa58wH>yme1MRJ@+Mm)Eo|R{x$KF{urp(mY-uQ_% zwxJ!{@6KkO`4A^MO0VW+P%-||;gU$wlg9JlkX#e(NA2fj-(iG zNlQ|O`ctG<=|%#P`|8baDBTZP>bti(ke0!zN^#c_q2&mdWm=KqH?N9^0^dhh6=@Gd z)P6IBe9wn@om*nI&cLkl%wfBmG-^f_^U#IdrhFtNa7;I#f1`-q>!1q=H zseiKTiUoB%MDMmxEpIvpc_*}*=Q=R;FoRM&aQ6l)g;IyC8np7zQnl@e6*P@#JfHOS zX=LCE&-uRT8|p1${MZS58y5Kk7&+_-qcWvfAFJ@EfKS^-5P88Cw8~a0;ZQTOls~_I>TA`KWOX6WB84rrPsuD7V{Gx(S?X?g4ARumj(}MR9 z6gzQXrGrzHr!g7f6t-5GmO*1G?Kxg1cB(W_rOS_jWy$DXc-8G~kov)_F%f;5@z}{{at3zu#V_B`=hvF!L8ftZvH`O&3tm`*EUP|P?PUAmppb>A&!(`PLThA_| z-szDHtd7(5iaYdvd1&RskZr-C3Dsl!n<+_mpu`xjdiP)}UmlF+YqFH+AjZY}r6qi$ zWNKXqO$r);j*%n|DV8*gdY|$!oDS8Wal9#7^@H_&T@{CkmS{c^^i6ouIjx8^?s**h9O+z|Yka}V)t4w+~BZm*uqI<%l)*NM07L9;eB7>DCg z1C_UilT1gYsiGSbBe)<}J?;cV>e`o}i_LBy^tv(i+2Hsw2)>xiDya&J)1rfuD{Nz2 z4@>w?xvWZ&7w+DchIt3bd2Yn7h_-=r`fHWxHYGniy^_p)S;DAR8oCqvopH@OwmwEMfOSG;loaAbh##i+l zWk|~J{#%_qO+^jR=Uv-p_*y8MFB9U@-a)qQp$!KWA?S_lZ%?3h(?~`YKCQxp3{<#z zT}r;iXzqU)ZpIwTWoW{jL}20D>E_kHO*mhU8u&%DPmB$MK7L_eFMe{{O^SNh`_|G? zW8#vYt*s&SJq%G3#v#2DOeV{HI(MKaCih~~T^n&7odHRYAk@6v+!HWr466kEun{J_ zE;e0=fl)WHW~?^ge#9v6QlR-;*m6_Zx(BJq!aMYH$0D+kA~=&oL3X#SQZ)A zN9%z$rxQ3Q_{2$9giKm`l_&a5r)QIv#(tZw$0KO3Nr~4erGB=109`Ru;g~eY($dW*~ zFpKi@Y%TnLy?$g;+VmKKt}B)&rz@Vr@#X$hsfTgG zAl0bhc8~7h<0xEz$#XzmV282JO^9Q|&OjP^Ib`>(%-iv^HM)W#E+vJdbeG|GR5wTi zt;7@CDnHr_z|V@@)n@W=WN%!ThjXT24qVWCEIVFKQFietzMZ^g*0xU=^#=X{%g_P4 zxIWfxHnyJ?7jI0W#Yl*D!}jpSt8ST#m)Un!_A}+3_YS-9jT3!_47(zmpl|}2?l&RO z2f!Wt$G6IAIW4>Wu0gI!N-L2yohq1k56(l4Qc6K-CE?FcRnMkME4ut0&J*KIQ^`ps z(8LEHjVkr_=C^yZE~IbaLTgl1srf5KaiL7zzsF?U94e)%>uq3*h&s!`+qd=W(d{58 z4r~EKm|<0s=^kPnfoLk^pFZ_)`4%b5#G6lz;g&0pi#ZzzQ=fUl=s|@U(W6tJ$iDcY&p;@GSN8OOR zm@fJp9L}g0XZSbYjUfo?egz|uVXgEpPA<;VrMxG!;=r)s)&+YWkxI!Jx+5~+N{&_y zDsN2slsxPKEMXK-?5`7I+eZKe64)@E=*xaNg^j(*G#M9ezQVhR<*Ie0LF=BlHi<`m zHG_CFKk7@2zMJbr8D8SXHbWohJ!6lL8`5x&aVDJT&{g0JT@6njz1EjZl>6YqflL|xZm!X4qX*R)I1oL z$w7G9vI#|@N%(|xIB|tAm1WZT_o|bw$7d|PDdjv^mI_H@T-pmAsa?#w()}$3K#3TD z*la52w?+$f6nbb0+@PhwAI8D0+h)P>R#+$;_z#2seq*9)z+)m=>&Ch;_ouGxocEtV zzyi9!6SDhL?TRA@_1%j1$I9BH^()Oa4cUVIq#qhy%FEe4gr6MbqeW6)$Q4l)()NOV zt{IUfF25DI&71ZI%*mU+XpgPG8VsJsR9|QGMFV4Je2I0(yfn2map4$iI$(`WEn1Wa zyJ?`_`8i}bBGDYf{(ACWX=13$_HLFkeA9BMpy1GU`|Tsme^rRptThh!&a4^mQB zp5q*p8Pe(Rt9)ZT|1q!XU_RJLcPZiIGx>b55`aKzu(||U>1y(hFA z#_VEea&Vu$8u~C0(5-fm=`>l(5F{9S-t)*!S+{}9=`66DsJY$ANqK}EU!VgkyBf&F zIeRC>bytaXX0nA5?xw)@g!;!G1NaWEu@Gs;mG z%2`D{oFsvj@>>sF$1!)$piE&Y-}vH9U&_4B@wb%ZL9jAvsrIo#Tj<(Mx$e8DxkRX< zflgYq|D;m4*B0l#erA5UZ#wCn2S;H?99l~wtefyO7_+~&GF)KB5iT-RJA-p*I^3!~ zJv(YKU&PAIy2+D4nb<;=97BBU0+W+5t( zH(mJB>PWtU-+&KX$a_OMXZv}JWu1jOvHt39s(VfE=%zzQ#=h8vaAx#!tDFyI-D3Qtx!n zC+*i-pkim}Fep#>$rX{2$iMG!b~VD4CN7Pm460?fzkm8XknF5Y7^mIqWhTds`Tj7W zBEW%R{Y}`dInFU*8*5oiW!UQ%iqe=xIIH6M?#vf=y7~@$3G>;Wkshh<8fu}pAH1`~ zi^3PKB-h1!$B)Tg{rL4=g<^bRHc~kSO~-=Ns&{USLE?ShyQ#~^7Iw}odcT5mwgpIW zoh1?AO{Z60+mct5#dA-WaYwYSimY4xtRol9?ex0X;}$>3X$HPx(3+_1w%&C@ge}Z% z%aP{w2TS85kVv2f)3=|s`Lrg;_R@*5KJgjZ%=ZVzwK^(IJ4{6?83Ku~gs9g<9Hm(suT4F7M}0M*Dq=?fznwE! zRQWPS6nCdI+S)tVnA?e{cp5yDWs;p#os<^yDO9wnnCflTK08oxU?q z4o#XMB@o#Y4d#84*TrP4ylp5s4o&1I^uI zr))3KOOSAxliO>NzN;#4L%1SN`F@rH2SWU0BhyKQEuPF(SwdZDme@M!_$dkE_$CVJwa0m5mdfjdgrwxZvI1WZxLf_CO~Tqy4rX+&VsDtNkOFZZ5aAt> z^p1jc*L%s@JV+A3>Cyn>`raHm@fIS|{7}A+bj#-y$)oFMm%uLF$2TqlkL-3j)g5*c zjrfKF8CD773LjF-Q@pe;mz^p-mKiqoI!>^`_RXf9>}=lGU1FF2-5Lm`_ISDY6tI@%`#)$2=H4Lw$0@ica4 zwX)*rx+swh`dThs4A)>?XQ`?RE4a%MM9SHhq{pMe{SVsgeSc_lBr>BmVSZ!e+B@+} zySH{;*T_lytEq?b1>W?pLJz5%g0R1Lw}ziB+@NULVDKl3!?b}Uv%y>>D<=S#Z=3c` z`wiG$s6qbP)h_y0;rQ1@iChlj5& ziR-}{)1T&XQ_oO8w=FSh(U0z}@AlkC3{!L)7&^kA_2&uZXwq-Xm~1HBK*D@sNUVPC zS!N{6Xt?k3yF)tKP#gD_D^r$ThF*{ZP;_uvX7Nm1cg<@0aqFa_S)oC>t*it`VWh1DTr9!Ds+#|=UHh{AN z>oom(N!{OM)zZ6Hq6i_=*aU#?;wu%e)rDWzQczc~tTX(eV=k!T64$j&ALxE#N2ttD zlz`R5T=KKW0QIBK9sGL_cviS*G%5I=M~p&ucT?#s<#=dU*)&{!&RJ{NQKBmvRun762Ptyy*((idq2FxQieV%+#VTvc zu`o_L2XbA$tWIm#YbQokcFnCm)!85BH1%$1yVD--ky4k9$^M?nzlc&gIor}CeN8>E z-#TH2EO*jqvvN>wQMS@R&9P9wtyKcj(n;n|zjEXCvMCH8!@PIT#Gx(*jc`XwWwLSh z2V|gN9M;iscG+7$5QCA=c#?}=(X-lqUMa=WPI}V8i9InxwORKbz7y@MX2cAx0*4!! zm0*SV$9;f49bL;Db<%qKp^XA++PFoTF6U@Nw39EjyKj(*e5IpWuOb)u9)AwJWDvO_ zvj1}zdHA7~=iTXEK)R3GnOhWYJs(S@;N+e+^=7}bTf^`|v%mDdk=-OX-TL+&z!oR8 zsAxUo!QOa@;obq?@pm1dflDQ#{T%IZ{$-pMa7;PD`Q~tJQ^{sLL#&%x5ue! zuHsI#5-N}>Ioc3c{9bR|ZBTn?P@;Re%J$cD#`a@(u8yP}TcZ{h@qX<5VPF5}pDv|F4&ME2X6n~#m%{BTr&P2DC{#8RqK zr1Tcmg^hPZd+*2IO-Q7=Dr0;nl>8)^e1kZ=aVze-aN&IW{kY_qO&Fwhu=p#!S0b)7?e^#T2DDgoFGv!{=cXQaE!Ii z_qrgLpxnZm`LcEfZFP>%TC(G=RHXQlaOaBUokfBtU85Y8Z z{K2lLEy}SvMet|1L|aBiOlnoWNiZTc{N$}B2~<8~Y!&E{6`f)Da!qu9@+spyajkyG zOi$mh-+{~{Zz#Uf(FvqBSz9de(jdW*NGqx4*AHy7JZ;3|<(NxM+S^Wr!x^Bu;L-aG zb>0*=n||rbKh(c2LON7XhP6vZ@s#_7x9h>}a=7d%0I^M=WT=D2F-WuC=8-yCa^E>_ zVtP_|Fav~X!UgQd2ck-}mNIm6o(~d*)p*edufhQ;xV%Tj4bm*f`z?~G%{#)%jR^*^ zeYZc;wiBJ|zhisPCXk{$Fjl6HV`kKI^h+zAS3TtemQ+%vw1e1+b%b(HWcHp8CdVxT zDw%Z_>&zE|MuJcvD*#FDtSq*F5+w!(aZZhX{cio}QxEc^GQrh;KB0L9@cNhiA?P+j zU_Qw~Fn_R#Z(!GBx*z{6R~u?3W%ih7XJ{|AdtUM-7Co=^@QUUfetp&tKEsf8#W+KQ z&=KqYyPO~Q%Y1(L+18kIM7Vt8vZ4guC8DtLuo=nZRPKCcY+GiT5Y6sp5fIKCX2BI1 zLN>R%>pSqvg2`zji6n`v{*|;foyl*OI9#m5`Fyqcz)>Y#DriU?9JtAmwEQ7XXu$Gh z=hvxvCtDMfJj`?dht^6mTS^QT8F%FMa3|R3GJCX&Uj}KI^`2k{Y!o-!3yxSQ3Itry z`|1%JhUYd9$shT4p{u7zYr!oJF|Oq-rbjLf)FY5=n<>#M1Ksd7!TU&WB!k|o@B)PF zFU0+wlS)&1(QhG3*=EJxh22Qi#AkrpuS{}0Tq8iHJy{IRE&xvoSFLLhQhru)}L1Gu|KAc;lmiO5|Qy9-X zLS>_9C5RA1&t#izI@$G^`@r)g9WhMEfO(6qEz6)Ziv4rEoN%2MU@8|<&UVYvfgN$3qwutY67+fF+UJ`D50@L5i=jjv6 z#q|!E+56L;)oXs^I=PYr=QMiJgAw0byU-GvP%#RrP8Yx_wqhm^Eskq$7$+N^L)5zU z_!QvH*uSw!-}_D{5#MRie)t<(DXnla6p_Odtk7G$NCyu*91g41o(@~{=; zy5sL&$>-qP4KH4&n{rq$>jix)XuB->=wkJP7_O-O!`6Bm?DV!tmd(UABuW9ufMwVe-D$_{cH=}&T+Wb8r zY*!A%aa0W2gD?vebM)tC7hsB8|Lme6G!ITHDraLkPLL2Tp;9UMY3I&AvZmxvOGOJf z#bh^=qdzA;Zgn{{mU?N6`r#;S*p@>ln3KaKNv|<*G>@DO62kV`7=1qWNMB{X{Po<< zj~zgk6z1T|z-*F|XEvorPcy6*y7IX+p`bVEwJ(N(6v%zormnjtX1y^9oYBDgJ|-!l zD6bd=_ImvJt=gkJ(^efeoNgoI+9i`2x?fz*MR&_(~t7@y~ueL(kAzorrd zA55@~hG=b!E5I5JEEX$YXCxi=FJpG`K-qDrO5<7DmP?Grp6$Zu(27QjP0q%OVPY}6f7B5^5O z>XLjV-3Y&EVsheic7n}Z^*qAd5iF^&$TQ5Kgn}|0DXi}SJRe+*oqo!H!e zZt2FvMl!YJHt(l3)HnG4u-sSiyPbKNla-b$J5Ho*3m~A+BaKQz4n84oo;%0w^MJR;!nm(n=(uz&5@#pX>+pGs&wl@eI_?l`LwYHYByCVx6Z zSya{&lIvJR#U7&jOT~y+*x$_AS2OP8t5iNhZm{f!ocQ$l?|D&6jQVC87P`C9QJjVw zt_talbXsPApd8+hNf`7OBKt!~x>=^)E~o*YnZ*cbkrCvx;@~zz-5Uu-oH zp4|6QX1jDkq089UbvKDkHrW_=xyB>ojZPjTy?%YIvwT-61_vkWwR`xW#gnu3>?6== z^0mZOiF|bM{EPiUD_+}K%}rz3!OVD}XS?7tvT%5?;nuLUI>vB&VY9vGL^_EwqmPf& zS86VN4_l`WQXWp{T$KNwO0E5S;|l&tEBt7Z-)!uzXGNwKCHY+@o0jiIg;#yGcNX=Y zu+dzNmB26#=d`9N*B|Ml`RYOwl>0$%X1yB|pnZ3H7Y}IyHSgHk1LRs*lyu3*NwKtZ z|DcCf$tGCen$gZMb-Uj-9$pHAZ%l|<6G`!I)g`_xgZ6PbkIQz7lB^XX#L6`sq;iGt!+#F^8gzqALeKNLgCHRiLyS*ZXRTc=ls8JmO* zLyu#&Q_Csu;e>}*Y0_B7^+kiHrTmZgnyyquq1Dx;`=F3d^wk{jh_0T5oH+zW0O zdmizMqeLGK6$@XG8H58^I0%m1;&R-wlHMMIE$KqGOK}b>kF6cp-z`ZX_TjwM$F*nmD0_;)rI*`-xYR?CFe%>eKaA=0OuOYy z{I6c0cflaHkWIZk;o{1R)}j|JL(!SNouN0D8%30OMo;0NHmiJc{1pxQ|8(cJ@Kfny zY0J4+astbk=Gyn|E(Y~ry^b#?Q!4u?se7^F&tekeK_L4O8lC}#L!pB~caeD`2w#4s zWo~{*kicOcT*!miaIp*!x>2t~|)$<86U4y=S;KJ%9eq@0(D1$461kE^C_cgt-% z%jyUQV-BzPlPxor79B|_wkl4Ss$DHSzytYlbT`O#5OUF&Ul;}R3qx9GqqwVo{GFlWXsp{`xO-!i-Oo&Q4nW-|IsEE4GWehVq%QR+y;_ALau!)|(;R0L&O zFE}>vV5Y04xe%v^lyZBWi}rDt`e4s$V@17Dw1%$5eIM_V0=yQXbj@QF-|)Yi&Yq$4 zAh|l&Y}-$+5QV3Pua_3oIo(QM9JctMt7UObWDaxe=^301P5p_nAdr3f5XQ9|@cPq5 z$HJIsM7>f_r;meg{@EDQEB3>-^WUbNHHo5x7t%?(i#ZI8o_kobji z#+S_3Icz6ou{~7tmlFn1RFdVo4Y+tz)IJlgPd zf*B^KqZ&^*4x`H7;3{g=r)mG zG}eP>%GvpqHv#4*CSL*K7a@vf@ri9X`$jmBzqS=1-y7Dsm& z|1^%CE)qy@LOrb1*0+-YM0EQ&snL&6JLwT{Xc?)sh|KXJ621qnu{&LLz2Pb-O@#P; znVi;J?+axW5D1?P?Ni~*K#$al!u-w^4iaL=A1C~Lrk|VK-qb0_G`#Xlp9KZ~ni1(z zG>>`?YV26k*4=lVui`UEx_jEoY)-2JZFWi0ct|!lFmm$6x4tK!)Q8lP|CI)x>M8&m zQu`I&jA9>aL2Tf!4C)uy)?cPI&y{r5_nN`Fvy38{KtN`JTZ%0S8S#Y<0`?X6V+}yM zgs|Hrc>QOaM|240bdil|1*NcLeb1o|Q*qw6;n#C-<*Mc`Ev1;c@53gQWr698XkxwV z*Hm2c+ILx+k5rWtps|v&OQCBF zi~Ly2f*J+w!Ag>avu2oR8qocd*vX_;tXw2Z1t)=$!DvB0p^oRG@tbAfko&+|?M)<| zFikQ11wGKj6SKl5nJq*7uq{^Uuk8S{Nt**I0~9@aQv{#g-OH+vFd$L?C1`ZUQmNn{t`d zaom&R9urTJ0e(hJM})`P9%qzF45d^$jtP=?fuC00Vs$(jK6e-hBVl@KWhFCgl!U3P z^al?c5`_e8|7m>roc$(};`Wc>XHfW332GP2 z5Nlhwx*LLpMK4Rgz{=H4hAdmI1bhGfSl`n2{>x#vbKZ64wp2lt(r?X{*HDplu>XPr z*@3*%(C4EogaKiHmz`LLJn~tkQL{AAZzJq-_#RynY z5Pw%F-g(h?QQVz5UAePM071^i9c*KffBO*p+bQ$`bN55!FwgQdoT_oa^a5nqt1%(9 zuZDu(t@^p}?z-<6QJwjh50+8J|B)R^18g>pD3q03mA*pM6d*rCdwl0o(tx` zKZ=Yu)p=p_SgQ)*0ZyiJ?qlPoloneTHWVlp>-$|r7z$u5-=RJh5Y6|M(_zxUtoHTb@%QeD@oW)Rv;$ zH5HQ29vu;{PPhHJXowc%tYZ&q#T;tHpG?wsXF0xCF85I>#X(%F-Z?xGxc(7udXv*6 z%ONbLqu(or#%_#%t8yD8I|TO-e7P+?^0KV*>zu1;EF5%atwFIWT3qZO8f_%cRxMNN zX4WrNz6s@-m*XY7w%3#AEOsRo^93tDy$NU3hj+$}zu=H=)PYuh0rm*FgdjowcHRzl zJa$HL`%bUd&D2Ea2xSu#WRrZz-4Fn5#aZ{mV&b%;)78c7BtmZ(D=D!fRo-hchIQ&M zsMJjbuyUcCfg!}tWZco^@I*|%6&2aSmIU|_bEavQA#k1-XV57+L$?&zy1a1jcieE`J&13r2D zPZUG!gg&BXBy^Kjze`VL&p6Gwrb*l3&?_6{k#<_^62nV6$FlBl(e6L6M(Ecx60?9q zu1^7B9cx}zTznSoT+|9i9&_W#;P?9k2wJQ&qK z;X!^|#od@Z#=mx^Wdrm@v70TL(MLkicYwL{Qw)t=@I`|qgYw7ZF+ZX;40IKQT=Lko@7GMPbu)fx&H$VgG`wK literal 0 HcmV?d00001 diff --git a/images/patterns.jpg b/images/patterns.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e68220011632e87166c68338b740c44da50f4500 GIT binary patch literal 36382 zcmb5VWmH?;6E_+hLeb(*(LkY3ao1wOLI_ZT6pFhQC|cZ#lK{mncyPD2xI=MCDNfPi z1q$uue?Q!{?s`AHbM~jb&&-)Md!0S9fAeqY-#UOCtg4|3z`_CmupS@4zg2)T00$fU zf8&vG9|s;G9v&_(9*}?lpYRFr$rEBAF);}#B{>Nx1t~ExISo0*Qz~j|>L+BhbTm|S zlvLDI{}Y0R^T>mXM}&t*L`6bOLiK-5|Goey2(h5puW_)R0kA2sa44|;^#hpyrzS4W zqo)5KVd3Eu5aQqxVLvWI$N^Y5IM}$jxYz{vIJl4VSlBqY06YqOb^^+miiA(~EUBJ( zgbJk)QFACk3+riw-+F%iMjMt&$F*_FsjOer(Adi@`U+X7*7c)&ETM|EK@W zQv9E@D}X1DdSO%GPypls3-GMCGzAoBUtxj(u8+cwMuQZN_4E@e6v|QB04SmW2Ac^5 zzbi8(?E z=ux<^-{ln(*indMMQRDuONCO97DTOC8x#*w(RF0-DR_tCBf*`6U6$HY6_y_>icIkF za)*6-WSbQ!utlH%Gl&A;auhE&f|~g!r~ad6D3Btx?9UHU5RgvOBR3u|)r8bcsNO

p?-=T^f}ntkJ$?=Z{s6~%)RVc=0sJTu|IuW;4l`k);16Jk z^h~j+?9XRqRQtwGeRg=3XJaPXO{Riw@Xc3Y?Qz}_cml8Cl$64C_hv!mFL_a}JAb|! zJ-O7^L;dg$+R4s#lCsO)G1k?VZO(+{SR)*)`gUC`T35r(?7kscvn9=nq827NbXq>q?COm z1Vl;(p%jzti3(SA)buu#&OM)TY_`Whgb2;!`y-QPz zn`V2!Gn)FwYZM>fUG9>B75x1l;HeyC6L;7~#K)5yN0o*R=IgYK67<3XvYIW7I>#zo zk_B0EQ7G+_erXp%D$)>Xu}w*4g@L%cfq{=(1mTl1{P|x0w5s?fwy!Fl zB|tkjD+i|@#eW*+3H9_c{sVb0ycxTQ3p_?EbR;%%CJv_1` z(S++u?KiKSoxjcg%^Tv8+A9&dN7mTbl7R!%8B*SRySr0s;iEFpZlQmGdSBp75iM#P zq4L)BLs*B%!aEQyTxd?s8X$i`r<|GT0fC($^h^qK^mfs7yPaeM%V)`cb z#bdNYMKO`Aha4(w=A;{GPN$qVc5EL5I-e5br0${$nn97PErgHufv;wvHvt>RcMS9& zRE^aYcQqUt8057IaN;gH2R@J;-HUHtR++^SwVEu_m-Ry$N(x_*`)P2lZ^;HsTlQR5 zl&yq<4b4ycdiizf+hVS=Zn;fOoXB%hoBN>9hJuoaGA_>GGg)cK+oyWw=|BGg@R**{ zM9*`76?4lf-tKfyKVTso#OehqjErFHP1oX#UV$|5A2rtXV5%J3A$J%{pI zut-waU)ona>i7e4d(;u08G@ch!_7IY$vhbn%(~hSiQE_K(UG1f4NPL43GJv2^;eP~ z&xguy+hcw_?pfBn=+C1!c=s=&{aPCt2E5ww_{Y6Rpc&ijL>%m|cSd9ZtTQ#*`s+U; zjf{$4{Q1wKB?MYKKh?G@`BQyl>aPgP8-29Dr2DB;Z>P1JkYIMFnGML2Wawr{Nkewd z6QF*e<9M09!5Vokvh|-)@dg2TM5Ih_`f4Qy{cUgK=k|j0u374Rryi2`$`qv14_R$; zkknc(p>4`)(s1CRkzoAk*S@wD?|#i`!$GEG6t6IEpfZ}HK*Qrz&X!kry{x?q`VhRD zPcO&vu8jE7CF=@RU2DPw+hVXJlQ^^n;g;NwN_>}}Y^H6-E<>q0s^BzBW$7=3hJzE^ zei>_qVK}VJ&wvwF-NQaOo+Iz-BL2>6^LXlfAmp@KtVQ7qqWu2>N+hhxE{~{QwJfbG z#my*Nh|*($hbjhj=N9<4)WUNG1{kaT&Ws+i^W=<}LwY4yb#K1a?fwIh|JidS6a7zP z^}HyQ0N3KqsJiRF^{CtpWQ}O$H&?6NlD`XH6I{Ao0Z4xOAF~BDEW$SdvrMM-nXSA< zB2DDRZK_JpKWc{wvj=I-1PFkgWw)m^^Ma&i0=f=8B&9y#|Dl)lNtRQ%$NZ_*-=+?^ z>dGz2riw4pxX0&7W#(6@SH7gxE3-NyjcXQ-avVQ-;misrQYBX!%rO|q`z^CH=giEV z3yRZ0`EG%#jEh1385V{csc&7vJ(2-Vn{Jf)`O(@#DGo`WEhw>!oU^8$7&H11DgOjd zZ@TM^N_y(oWIskyZ-6Wm;z1ne0*@!Xm!ijt7O?BitO1#H)Y;hUi|nPrX;sk#3-V`yj`L*l$# z6KSqP95nPdO3D|7%h25>S&MQ%jG@WSh8nRjY>SL<7i>+$n`*#yTSv)Osw*eVpGqh? zZIL~l2VTOe3_AEqdwNM_?gc<#q5~|xk)cxQX@8CPGc*Doc3Q*{s)9bhC7JYH0VOYR zLGsfKgTdT4oj%jQ_61nWfUmm_Dn045_pE0c^yvDWLeVI98pqE8vf~3{+sSM{}r3OtA4tJ zzg3rtYAXszL-OX=+}2DSj5#jc`K$D7Fn`Rg^fIjxsiRqqk-x=dAm)frfB$nepmpSP z^^2LzSDzA}wtIY?2$9Ch0f?>Cd<@QSw3qN*?c(=xmgcN<9#FOSOG5J=c-$o76Z{n& zu=sWz#4z%TTc)|NNpx4Dp&7wO9)aXdRTvqXn|EFGjSNiOEK+v_lQ+!f!Nb}OrAoF6 zSrByMeJ$;69#V7HO>+&9jGA*Z?H-^mABUQAGC9^kaafUmenEBLQ*~p^T*ASN`J>=w zmq;~NV`sluhgX!z{_DwnxsDq(Su?W{F=8#x_{TjKdI?5nF!we@Kl5B~hCaa2t5?iD z+TT{;Tn%noh4P)zSd~% ziXp0`U&M9-NYXfvEYZH%C#lkT>wHqCifR>4ryecqj=I#t2ukvtD{1EP|9-@LOuLJY zVK7pzDEOZ1zH7E8w;61N;{npi<1;`Ze|5Y|F1Xs6I1_)^deDbLCr7 zPTQ6v)ayz|{-*(6lGI9@cHJ*2!?7j(qt@~`NaDj?&5SC$-Dv!1k&vQwKZABANSFaHS2V<{5 z#NACfDUR}mKU|?>x|&+qLa-ztdN{Mv(@HNq=GIx@Gx=Lk9eJrruKo&{k8x*OVdvB7q6dC9`fPu978iy48=>`XpU>$ z>9YB1KS9NGT-iN+l_Mu2G{^3(XCZvdDvZ z5QAE5ov2t@*Z#axhI8l2Eo@6@Wrs|pCb6kN9qLtoLZ;VNb3AKQDp%|}MuWNES4m@E zgaRB8djQa@E9rHB4?c=?@Wbl}5fjsvV#y`<$dNC3V#Qt$Y-+@J z>R%y&BcCZkGyeDa!0gKASn0{ar)1=Npq6GBgqB_>J|6@o0O9S3mfMi>gQfXB##@Gt!NaJb z=;#HTTN96gj!< zglw5J;dqA?rYc(w@@h8QWywzQQOz-#YG#H$byMz}G=G|>*dm#vtM$7^ht@;z$AB3v zBjig0H;@Us;=|eRDb*LF2c9lGAgqei&vX?^Z-pRmFR0V%=qd#ha|YIah&d~0spacA ztU;510!Tgb3XUh!CGhVT| zq6+)54#0%iVYS#YPH?ju&rd>Hv|QZalm^0KrjQ5WiN!MTuqoMm%LCV-8PU5=4GK$X zfJnPwK7tn+qVW&#-&eV-cn)1Hd&AoSRtox3T5KnfOqGJ3IT?m2{NSY#P3p7}iHE0Z zqUD^x5O-nKyNv5?0zR)F2##HjMdk^Ds)g*d2(K2L>S}8WL*J)2tEd~uv*CH#S0-uf zD$hm8%b3iik`ZCqkXn0vigev!-}9;1#;(9`#9CkzT8#@d zWqJ6+%Roo&w84(*@*~YPR}ue_#;Xt}+@^DPXz7A15axU2ZZFy9I~XK6@FyVc-9*2p zCh?93P(wK(4>KYi{|_M5Q{&s`+APZlPoYmY5tRB9O{AnSq9@ne+}FDp^`{ocyc<(X zx$N)Kk80Uc(bIQnNmUP}7ig{8JcxTPrExgu9U88)%@3!Y)C4pAiBnz#``W&axDeU`JHepC}rZ+X1bH~QC z;>(USd2*3g%X-%8$LGGr4o^hSn`V^!lIjW;UN8mPMcRIwX;RklYptp@(MNNX_+a-o z6dx4jVC|<}O+Vjk4Hyy!*y!9Oa+sK1P}8TcwyAPdV(n;F+*xDGiXSwgH#lStJMVc`|SzfJ>78YRTZY zeMGjkZ85FkdA5@Nu2ePpHQk8pgw>?5yJ8-X`MZI?GFvV^`}=Z%Msq*1R%X=slx9Es(Gf~tu=z1>ElNw^{BMuUg{2tygfvGK74oM$CG1I zft1P`50qTG>-oeWm{Uro6h;wAb%ztC6V%P0_R2mM4u6LGrz(R7GWMKeWTy$oBBZ34 z2X`-MO3^63y#A!+_hwf_C-e1~lOhJhX%T*1Fks5J>D1*6JG}RaXO@U1PH+Ve?^Pa4PXpXp}!xl`CxLASUk5S~`k$#Y(G2}#*Z0{JB$m}kf$S6vR%h*4# z!X|OIsjNqYCCGGMoisF-7XNUrVk6j6S*-y(_xP#Gdu5j9AKQ{x*TrEX$JiS} z3m^N_xh8G-dT|n4)W)gjWxfCkq<+ncrm*7|@ir0e^2qv6TfTVe%X^n_NI=S{;iHX_ z6@w7`DIaGC1Eqg^jSbxzmS?wN<&G(;_A&f>gL z*>&7^w(m7T|K(B%QY6-$=R{e3=kyibJAc+~bt6&BD)})$KR%k^mqZCA!FQQB`YSmE zRgem2Fk1QOyoI}?bb-J?Z057wJ%Zpq44u_+qL+MCsL6r6MxW(D)fC5T^jj;KO3`2f zM`$scpd$E*KH$Co%n#y`ap;+E2K|N1&Yoz_0-}N-b3I_8?uK#do}fj7lqNeqYN!MJ z4{#=0pOHb69Zab{8p7#s=TB-a$des`#tUAW@J+?iApjS4y2WFZN=mpCCHI(}x_TE} z?uX70WK&d>7vC6tk*;hnrc=yXz`$Vb0m$(;I)65gxyX(}ibH&2^B%2D^D?&FdIVaV``(8A60pAX!i` z>XEKDeh{YC?cSe3ab;OX*?8oH@=*8L9X0#=3pd2ywueh(70N*q0vIWeTdSgXRK3|@B8 zW;^dV46kOAX8T{)lqSrciuBf;38c)@2&WS7q+f##OA?()mj{DO@~);(M|Fh*$%ih0 zqcX(BWbRyZ+T1??m~=3!)8P*yJKWF@vWDmj6=t;5xFByxN&1iE>kbb6v1vvpD(^|= zk&a)?e{Yf*{T<9W>b3O{nIBPf-{8?pF1oqN1jao})Q%tacDrH=HtNu9c&}`Sm z!)$J5uWi`0ZsxUS9Orp)4dAD^^xg5k;$m#YyGH+1(K^kTiQ{WTDMx37%$C)eCr+4} z*49jw?<%rq7I(N^W|iVA*e~a&hJ-p!J7)-<34Bjuxt0`erxg>;WnwFKufd3NksOzz zILx7#-mPq{at=WDF4zdMoYR=Q@bMs?Kfy_ih|jSNOY>jIlGlF8Vzvu&a<=J;T1k2N zSpci=MolP+GH8X$+oeOv_8mP5qNA^3RQ(@d z%Kv9?v$7Fcp^0*^OPi{zgo`5|&>oQUbS~b1`c;Ba=>YmLJ4Dh|8SLjN#|&04AA(xLv5zioB_Mge zRv`6&&u8V3!2K7^EYcrh7t<+Iev3MvR%D|(m0v$Qpbk_yYt!b2hmPnMa%ArH+^*HV zNC+aUxJqd(`r(@5S;v(TeypN|*!BZ>iS6LpeighgB#=kzia)V^CdH?k zU0TYM1P@D2JkTF+G^SKM+RkA)Q5Xzz-L$dw>bl-=0C-pngRCTOa)@qxkxOOyxGQx7{m1DqP zO&phwN*)cqwV0#W=)S7Un0o+ytu#k(S=gD>flgg{`u){Ez-L7RO=;*;>e>Xj5hoiS z;t7w$51hl-n*FW7W}2o2Cb_v5+sGNY+}u6&ri$rhcWSoVSRUs@^QdsnHGxGl6%?1) z>_N*|a&NdR+jc{d*khRi74);hh=ii|Phf(#Pkj5rPro+PxONjG!C+4v@j>BVS-H<2peb;%f&PYM_!Rce@)?bggvTxK@uRGpBSEAF`9k`XgA^h<>WC zO8ZnAbiQ)5?{ZKrbiFGs&1tE8o(&#h>z)KMsqg0w^VEp_YDRr=WpHAlTXtD-)eFlG z>R#sBx7UO3ayjz3IIwnj20QPS;(Mj!v9pOy(~s7-UllaH$WaM9sZ&jAd9K;bUii#H zbwB3YJ(pbra#>b=WPf3G)0fPfyS&}O*oXh_=v*K}4AqmY%W)JQ*-{|2!+GRWiNge% z_cw|4?>MtXQY?5=4L8J(L~43U=|!|HzVH~E7zuB2_cwHIZG!V%7>sYo{()G2rrxQV z4391$KTsLH{|Dd@NSGg;(Ht|BkMQJtx|(B@H~NVSiu;KOYb;{-2Q+G*(7A#b*WV&J zD8a2Zc1h6waK0>-{mT~3Pq&p^2MnU|3>Q~M_P-uj)ji)e8b9Vu#C&t?2vn2A)Ev<` zO-aamcPW1X1nLw85__6ikrbI!+E@f%IOm9cct-GU)p$4s*27 ztz>Y^Bl6zHxine0zY=_zUc z#PVTqs{ls9bARH%)6HI>W~#k~@N|RbjC3kA@_>lo)`(MGTa6%%HBTG5fW(w`Av>Db z8$&2Pa;s8lWi(KU-U4_sU&^-=e)0)zv~tNe;jmow(O~*CcXfc>8k~<=`JtJq$pu3^ z4Fri@1ofD0y{PFZ&UUJq`un{ zCw6KjqUC1r&6C=Bj_zkyKBcAv*C~GG>5&HBHag5oSszB+>VW-~OT?D-2yN=?!1_4> zX_#q4few$DR=X2Nzh$BCN+!D=eqeHGF4?DP?I%7{iiv(LC6fG`kn^R0jATUBTjAUx zaPx7N!-RA#tr+v5M(dM)=N-^2gb2tHEL2?Z!$1N~M z;3~AxBsg1!OT+B2ycKh*hj!IMJ-v-)1@~yRPoqq&U>uzN{qmaJKt2Dg`U%FViRZr5m(eyk^t9NKV4P0{Hnw@&# zZ+$o4N=!|IH!~|N$3oPJ;kSO8Z9hqUhqR_)xviqZ#y5K=in@Y0*`Y@sS$ng0{y)kD zVxwJc1k^_KzH({m9iVtmP%PWOV8i}=GxM2>F!u|tT7d!S3xV-?%Hp{+%>2`c4*9<< z@ESvILFkFtwGC(~HgivEORRtlLFEe~keEW_i=2J7Y#m*vKHkee&-=(8tHqIxrQ;V$ z;Pwt}#y{S1DL&X;(dmm-HcBe}40hPZ@4c63Fov|m`3Bli$61|AGxwWTb$o<*0JjsP znS+{yi2T4T(sIsJoTU=kk60(Ydnj#V3-RiE-F|cbtGm;#xU^mmUyg?DEq)gPMBVEA)cDLZ7zYKmjb7)gCUXnJH*!?r;LV+h8hGsI>he zO#iohO3gUv!WrFAp8CXTGTUgdKmFS%lY^!PuC;$T#Q_l&OZSQalU!2{0pY6Xn5@?A zamjj>{Ae{P@d16-RJ4@Om7mpyps;1j#l(6UkfWTSKsNX)R>s=tSfix)H5%@(Ri*Fy z_S9en1Sp&Xl!)kv0hw0ANY_3UEVFIf?uo(V-n>yZTm4v#3LjS_IE`EQj31~x7pS%| zcJ8gT2pVocGRIDq0h2?LJ#{^aIONvaA4^F6hGlI(YpBg?7R1&(^%aRs>c8m6i=c4P zb76#BY-zL2_$sMeEKr}Ra%vbQ9n7Ms9M{VK+h>A6^@ZO(9FEMA0qtX-SDvvu!A7jB zo7P{eW7#!C1;26WT-Y`<*fvHf!fVL_)J=GsiIw|@*Y|$T^>>wEU3s0i z%Y>}ZqMU%mRD%xrl4o1xpL})uEV|YZen5Rxb%Z1*zja$sS{T;vOSIs=g7gfM>gh~w zg-+BXhDZvc=9mv>oy_H>DnHbk*44QzrxpP&Phrs>x_pey=o7upN_n=tJpi5>-yEB` zuef>)uj=bj%Tp#-H zg82Q9>6nW8)tZ)R9B#|tTxiYfXOt0~-?N6O{h0+;Xq0K|B|Gr7^Lk)Jx z<#$}vj?MjLePEwzdrChly}O33FtLK{@x=C)5)#~!Vs1T;OqFNEJJXy#XugVlS(=>G zn;jn07x>U)eZj?3&l!AWFGG|UK!X75rfd4rlcJRWvY z>8)<2N3a@iDe~X#qVq~?sv=Y|jeg<0aDgNB?PKH%*>AUwZ2Pi!`L``S}Jdo|-( zSdPF99oLf%$~AwgJ?gb^LKWvQ4IWQjFWB}(wy>OI8Jl-3qC}#m92V zBqWeX1oV!)bu=<-An{ac1(j*m)aLfJc*Gr!b^~w6v zaijyiKhF5T(?yYGMbVDywo3=O;I(({V{LR_sDHo9*>Y{^qr0HZopL-Vg0;D}?xFdW z=AI(rEwX?mZ4RkJ>8kU4zxQ#0PWmfW=)gl|bqG%_Xpiw>#_5BgaQy48eR{p6Ag8IM z83IyG|C0%Uor~xV6enp2N^?;DK!l+XY>Rk2d9YJ^7-L3=bfs%5)Rnv*uQ2T%NSv6R zUxdB(7j;;=f>CMbdX&x`$+{_u5Sky|t8>+>=~7aZ#nyV}5f zeN&1;%yG?#Gk%Gxex&miIGGHV*ZxELxOJ9iOU=ynSCO;Hs!O1?-G3RGbjs!Ok*`O- zE`0B+J~8>mO(WTawZp9>}K)Gncjm@!@{k zDBYL$n((A0fb8A%v3B^&s*C|uDAmfh(Jf5^uiO~O{tst=1C}4s5EIdmXi?|!Mh*Ll zjMO|eV|Oc%;SGdCdfkfoqSf_``%DUivGBxkK1?E!34sITX?*<@tjZRu8v+VRMDCQ%#e>3w=w#m@UTm^DKM<%Rk|?g zN@rb@G84{bsb?%;+rH$G)RtI41(2>cRmk0w^yM!j{PJwi8{VT8P3_U-O}#zY4!D{D zFXgO>7kQfI9d?rAyL_!9zjq6W=IYpKdk` z>}`Yw3(=;}oU#fjeU8p#e&Cfh*PdwU#r_2Eb*E9Jmn$)%LC&T=_psr6w}Y_*drf%n zaB{J&0}!0=)NwFZYaMk0w9)8tVb@XN^!T>6pI{TEq|-1d(&dq>N?+pbgYq-MmhZ;g zvc0B~d=z#=>WV2$Ay8Ba0Q>7M0V5O6S~%U!^h|?mAYhKG6#|dA5DtU=r=S_aOV$uu z`*OlxdyB1w;>84^?W-0JVdjJ|;|r*q)jU{&^O_!~MK@b+j$)>aY7jIN`o@Lb z$(zWTa)XVNr$V-Bv$I9{<2X~YymP|T9Kz?6~@Dd6p# zl>h2u+_u-BBq%jWH6GPFU0W_~P}s}tx%l+bAYZ>%X|u>nW(So!T>yrq9flE3z=Jv8 zFY-qi{;_g5b~L*_Xk`rhB=a={f+e%`gNF?Zlh;HK{ZWR#Mhu1$MOi94TRu@!P@oE{8533CfS<2Avip3yb)SCI2(2bVBIJ57FYfE|&wc zn+n&Es|JS4+=<<9bRtM1kkb4xm4*Z#fh66Lhe?&IgG%j7QYx<}DT>0+)AxPxzXuDG zb}f#tEG$8@d2TfJ^Bs3vRV>EL)wwu!@S6_%Kg2y4^$D`otd?ylnRPIJja4;q)KBaj zMyBM%^B^6*vRpNqip<3>b|{UMLsm9}>*6rUD~UgM)g}JEW5U?XTzBjDVZTeK$JDlIwblfkXYIL;TL|_5 z@v+VC({@nM=KtEM{`7JeUW9~i3$yYlP^41v?eqvoeU#vHP0E|Us2+R9b0XImtF z{~JqXh?&XNcjoz|NranB;MEL!=UUNv_?_RmXVuWG74us2s6v?lkr?5_QOHtMvEpe^ zbK#)ua~{2f=bW1Rg;x(8y%febbB0s5i zaaHhnd-5$qoqr(Gsa8dq?SxkL*Fy??H$mkq@LBH;VY1x0rV7$$(pKZ|QPN~y zp`lr%pf3xyEen&DXmvmY6?Lhc483Ih{gfd zaSo6)pg6wYT5bcZEwB-k_E_$i{|PBMKJ|6QZ%!UKwT+CrapJy zOA)&4L2D1`5b>5%Gj-<;YlDQQ>dDiJ+TY!r!2pOW= zhTpo?&yCW(SDImP_3xqV&rEZz?W#W?_GpPCpO!skf(~EnqWt~=NNz4NoA9_mnqmGk zu>YXbKwhJzz4L;S$7*o|vY~es*P*zfUovwh5KUJjzf&9keBmYO*%xi_<#kLW9%GFmSDRy6 zz&n*M&&>Y9NM(^4{j8fk(yma>5&1yHZ)qO%q`}m!1n!q!FB=9Tqy7Qn%N#44&Q0C&TX=cniO3nxaq#*RQcF^1Iv3iioCRE+o6F0$ zn%$-S)v^j!`QPD$m4wFq>dlIEiR3A}EGX^ho8D>La=DFl-wttCtWoz^3pUC-km=$< z@JQAex565JBcjH}7?V?Gde$+s>?(M8&50F8a8mvpHR9u*X*~PMp*GRQ^Aa78x-D}k zc|mbns`_KzvL550J|s0?r~X59A?ZEW9BKhJCHAF?aoP)GN&nIhzu#4^+eukoUAWTV zV0g7+$FGU!giP+SPo(ukt6$Zkt>|Uk#nxqn;Hv~XtKYs12h{D&eOfG3AwxS!u3pTnxvXc(lm^Vo<@v9-ixgyUtJY94o&kc#b7y zKiV3zZE7n`vZMtHHX-_8g9c4aC4Nb(lU}#$4m7FJS>4e};N}>=bw!Gt>IyL{wc=7t zCwwJfmL(MZK68gkP{JUhm}0qw#@zn<*_-@fj){rkfX6VnzGE^P8@SC)8SbY3ddF>? zgA2qN*#E?i@yS+=JJWMbR#q8w|j{rc*I6EVl zM5O7Q(S~(PEMeT`qG3tPoX=CF09XQNnshragaH_|aQ{baPfPRNrv{IxC8F&S9>zya zMlNfdv}>&V%}Tlb-g>Tb&9yv20zBQx+=b)+80q{9?pir^8@Cun9TlAc6JX-Yt~OQRmD=x5+8v!bhru z9sY_RtD^<=Yf`eq_u6pZj~EHfEa@8L<@Bf39pMEjbep-lXQyYseM_J}8D(8dinW>v zP>@^Q?7+w^CLL^ee{^q>x9%|E^dqbLH69rzl)-3DAO(}&dP{h$7}I65va5Cary|pp z4@0r`Rvj+R^4gp0)twd3d9Bdl+#BkSUYH#IVG%}9J+$O2nQUv8DV)40<)>uK$bsT< z`}rXi#adZm`Q-mv9>9vw5FK#r5@~yR%{*F>Rw7OUAQR?wMMeE9%KueZ29s{TmbHSH zbTrE_XVrb|&d3!jykft~I$(aK|Eo#p$7+P}{aT#XI|N?U{pbiw-$I4EDMg7%j@P!! z$Ic3G$0d&J4EoETA2`gu%gc^2ru#08=J*f!2%%JUf9L7(%6#!QY$QVqdVrRB!x4| z+3M=#S~1OVp@Lyⅈ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 literal 0 HcmV?d00001 diff --git a/include/MaxHeap.hpp b/include/MaxHeap.hpp new file mode 100644 index 0000000..7e44eb8 --- /dev/null +++ b/include/MaxHeap.hpp @@ -0,0 +1,45 @@ +#pragma once + +#include +#include + +template +class MaxHeap { + size_t size; + std::vector *arr; +public: + MaxHeap(size_t size, std::vector *arr) { + this->size = size; + this->arr = arr; + build(); + } + void heapify(size_t i) { + while (i < size / 2) { + size_t left = 2 * i + 1; + size_t right = 2 * i + 2; + size_t root = (*arr)[i] < (*arr)[left] ? left : i; + if (right < size) + root = (*arr)[root] < (*arr)[right] ? right : root; + if (root != i) { + std::swap((*arr)[root], (*arr)[i]); + i = root; + } + else { + return; + } + } + } + void build() { + if (size >= 2) { + for (size_t i = (size_t)(size / 2) - 1;; i--) { + heapify(i); + if (i == 0) return; + } + } + } + void update(DataType d) { + if ((*arr)[0] < d) return; + (*arr)[0] = d; + heapify(0); + } +}; \ No newline at end of file diff --git a/include/Mesh.h b/include/Mesh.h new file mode 100644 index 0000000..d00f4eb --- /dev/null +++ b/include/Mesh.h @@ -0,0 +1,45 @@ +#pragma once +#include +#include +#include +#include +#include +#include + +#ifndef SLICE_PRECISION +#define SLICE_PRECISION 1e-8 +#endif + +class TFace; +class TVertex; + +struct TUsedTypes : public vcg::UsedTypes< vcg::Use::AsVertexType, vcg::Use::AsFaceType > {}; + +// The main vertex class +// Some attributes are optional (ocf) and must enabled before use. See more at vcg::vecter::vector_ocf +// Each vertex either needs 32 bytes (ILP32) or 36 byte (LP64) +// see more: https://en.cppreference.com/w/cpp/language/types +class TVertex : public vcg::Vertex< TUsedTypes, + vcg::vertex::InfoOcf, // 4 byte + vcg::vertex::Coord3d, // 12 byte + vcg::vertex::BitFlags, // 4 byte + vcg::vertex::Normal3d, // 12 byte + vcg::vertex::QualityfOcf, // 0 byte + vcg::vertex::VFAdjOcf, // 0 byte + vcg::vertex::Mark // 0 byte +> {}; + +// Each face needs 32 bytes (ILP32) or 36 byte (LP64) +class TFace : public vcg::Face {}; + +/* the mesh is a container of vertices and a container of faces */ +class TMesh : public vcg::tri::TriMesh< vcg::vertex::vector_ocf, vcg::face::vector_ocf > {}; \ No newline at end of file diff --git a/include/MeshOperation.h b/include/MeshOperation.h new file mode 100644 index 0000000..565cd0a --- /dev/null +++ b/include/MeshOperation.h @@ -0,0 +1,296 @@ +#pragma once +#include "Mesh.h" + +inline void clean_mesh(TMesh& mesh, double minimum_diameter, uint16_t smooth_step, bool verbose = true) { + if (verbose) std::cout << "[libVCG Cleaning] "; + vcg::tri::Clean::RemoveDuplicateVertex(mesh); + vcg::tri::Clean::RemoveDuplicateFace(mesh); + vcg::tri::UpdateTopology::FaceFace(mesh); + vcg::tri::Clean::RemoveZeroAreaFace(mesh); + vcg::tri::Clean::RemoveUnreferencedVertex(mesh); + vcg::tri::UpdateTopology::FaceFace(mesh); + vcg::tri::UpdateBounding::Box(mesh); + vcg::tri::Allocator::CompactEveryVector(mesh); + vcg::tri::Clean::RemoveSmallConnectedComponentsDiameter(mesh, minimum_diameter * mesh.bbox.Diag()); + vcg::tri::Clean::RemoveUnreferencedVertex(mesh); + vcg::tri::UpdateTopology::FaceFace(mesh); + vcg::tri::UpdateBounding::Box(mesh); + if (verbose) std::cout << "OK" << std::endl; + if (smooth_step > 0) { + if (verbose) std::cout << "[Laplacian smoothing] "; + vcg::tri::Smooth::VertexCoordLaplacian(mesh, smooth_step, false, true); + if (verbose) std::cout << "OK" << std::endl; + } + vcg::tri::Clean::MergeCloseVertex(mesh, SLICE_PRECISION * 1000); + vcg::tri::Clean::RemoveUnreferencedVertex(mesh); + vcg::tri::Allocator::CompactEveryVector(mesh); + vcg::tri::UpdateTopology::FaceFace(mesh); +} + +inline bool fix_non_manifold_edges(TMesh& mesh, double minimum_diameter, uint16_t max_iteration = 5, bool verbose = false) { + size_t nf = 1; + int maxSize = mesh.bbox.SquaredDiag(); + + int edgeNum = 0, edgeBorderNum = 0, edgeNonManifoldNum = 0; + vcg::tri::Clean::CountEdgeNum(mesh, edgeNum, edgeBorderNum, edgeNonManifoldNum); + vcg::tri::UpdateTopology::FaceFace(mesh); + + for (uint16_t iteration = 0; (edgeBorderNum > 0 || edgeNonManifoldNum > 0) && iteration < max_iteration; iteration++) { + if (verbose) std::cout << "[Fix non-manifold edges]" << std::endl << " -- Iteration " << iteration + 1 << std::endl; + if (verbose) std::cout << " -- non-manifold edges: " << edgeNonManifoldNum << std::endl; + if (edgeNonManifoldNum > 0) { + // fix floating faces + vcg::tri::Clean::RemoveSmallConnectedComponentsDiameter(mesh, minimum_diameter * mesh.bbox.Diag()); + vcg::tri::Clean::RemoveUnreferencedVertex(mesh); + if (verbose) std::cout << " -- Remove small components " << minimum_diameter * mesh.bbox.Diag() << " [OK]" << std::endl; + // fix non-manifold edges + vcg::tri::Clean::RemoveNonManifoldFace(mesh); + vcg::tri::UpdateTopology::FaceFace(mesh); + if (verbose) std::cout << " -- Remove non-manifold edges [OK]" << std::endl; + // fix holes + if (vcg::tri::Clean::CountNonManifoldEdgeFF(mesh) > 0) { + std::cout << "[Warning]: closing holes failed: Mesh has some not 2-manifold edges" << std::endl; + } + else { + vcg::tri::Hole::EarCuttingIntersectionFill>(mesh, maxSize, false); + if (verbose) std::cout << " -- Close holes [OK]" << std::endl; + } + } + if (verbose) std::cout << " -- border edges: " << edgeBorderNum << std::endl; + if (edgeBorderNum > 0) { + // select border vertices and faces + vcg::tri::UpdateFlags::FaceBorderFromNone(mesh); + vcg::tri::UpdateFlags::VertexBorderFromFaceBorder(mesh); + vcg::tri::UpdateSelection::FaceFromBorderFlag(mesh); + vcg::tri::UpdateSelection::VertexFromBorderFlag(mesh); + // Dilate selection + vcg::tri::UpdateSelection::VertexFromFaceLoose(mesh); + vcg::tri::UpdateSelection::FaceFromVertexLoose(mesh); + vcg::tri::UpdateSelection::VertexClear(mesh); + vcg::tri::UpdateSelection::VertexFromFaceStrict(mesh); + // delete all selected + for (TMesh::FaceIterator it = mesh.face.begin(); it != mesh.face.end(); ++it) { + if (!it->IsD() && it->IsS()) + vcg::tri::Allocator::DeleteFace(mesh, *it); + } + for (TMesh::VertexIterator it = mesh.vert.begin(); it != mesh.vert.end(); ++it) { + if (!it->IsD() && it->IsS()) + vcg::tri::Allocator::DeleteVertex(mesh, *it); + } + + vcg::tri::UpdateTopology::FaceFace(mesh); + if (verbose) std::cout << " -- Remove Border faces [OK]" << std::endl; + // fix floating faces + vcg::tri::Clean::RemoveSmallConnectedComponentsDiameter(mesh, minimum_diameter * mesh.bbox.Diag()); + vcg::tri::Clean::RemoveUnreferencedVertex(mesh); + if (verbose) std::cout << " -- Remove small components " << minimum_diameter * mesh.bbox.Diag() << " [OK]" << std::endl; + // fix non-manifold edges + vcg::tri::Clean::RemoveNonManifoldFace(mesh); + vcg::tri::UpdateTopology::FaceFace(mesh); + if (verbose) std::cout << " -- Remove non-manifold edges [OK]" << std::endl; + // fix holes + if (vcg::tri::Clean::CountNonManifoldEdgeFF(mesh) > 0) { + std::cout << "[Warning]: closing holes failed: Mesh has some not 2-manifold edges" << std::endl; + break; + } + vcg::tri::Hole::EarCuttingIntersectionFill>(mesh, maxSize, false); + if (verbose) std::cout << " -- Close holes [OK]" << std::endl; + } + + vcg::tri::Clean::CountEdgeNum(mesh, edgeNum, edgeBorderNum, edgeNonManifoldNum); + } + + if (edgeBorderNum > 0 || edgeNonManifoldNum > 0) return false; + return true; +} + +inline bool fix_non_manifold_vertices(TMesh& mesh, double minimum_diameter, uint16_t max_iteration = 5, bool verbose = false) { + vcg::tri::UpdateTopology::FaceFace(mesh); + + int maxSize = mesh.bbox.SquaredDiag(); + int vertManifNum = vcg::tri::Clean::CountNonManifoldVertexFF(mesh, true); + + for (uint16_t iteration = 0; (vertManifNum > 0) && iteration < max_iteration; iteration++) { + if (verbose) std::cout << "[Fix non-manifold vertices]" << std::endl << " -- Iteration " << iteration + 1 << std::endl; + if (verbose) std::cout << " -- non-manifold vertices: " << vertManifNum << std::endl; + vcg::tri::Clean::SplitNonManifoldVertex(mesh, 0.5*(iteration+1)); + vcg::tri::UpdateTopology::FaceFace(mesh); + if (verbose) std::cout << " -- Remove Border faces [OK]" << std::endl; + // fix floating faces + vcg::tri::Clean::RemoveSmallConnectedComponentsDiameter(mesh, minimum_diameter * mesh.bbox.Diag()); + vcg::tri::Clean::RemoveUnreferencedVertex(mesh); + if (verbose) std::cout << " -- Remove small components " << minimum_diameter * mesh.bbox.Diag() << " [OK]" << std::endl; + // fix non-manifold edges + vcg::tri::Clean::RemoveNonManifoldFace(mesh); + vcg::tri::UpdateTopology::FaceFace(mesh); + if (verbose) std::cout << " -- Remove non-manifold edges [OK]" << std::endl; + // fix holes + if (vcg::tri::Clean::CountNonManifoldEdgeFF(mesh) > 0) { + std::cout << "[Warning]: closing holes failed: Mesh has some not 2-manifold edges" << std::endl; + break; + } + vcg::tri::Hole::EarCuttingIntersectionFill>(mesh, maxSize, false); + if (verbose) std::cout << " -- Close holes [OK]" << std::endl; + vertManifNum = vcg::tri::Clean::CountNonManifoldVertexFF(mesh, true); + } + if (vertManifNum > 0) return false; + return true; +} + +inline bool fix_self_intersect_mesh(TMesh& mesh, double minimum_diameter, uint16_t max_iteration = 10, bool verbose = false) { + + mesh.face.EnableVFAdjacency(); + + std::vector faces; + std::vector::iterator fc; + vcg::tri::Clean::SelfIntersections(mesh, faces); + + uint16_t iteration = 0; + int maxSize = mesh.bbox.SquaredDiag(); + size_t nf = 0, nv = 0; + while (iteration < max_iteration && (faces.size() > 0 || nf > 0)) { + + if (verbose) std::cout << "[Fix Self-intersect face]" << std::endl << " -- Iteration " << iteration + 1 << std::endl; + vcg::tri::UpdateSelection::FaceClear(mesh); + if (faces.size() > 0) { + // Select self-intersect faces + for (fc = faces.begin(); fc != faces.end(); fc++) { + (*fc)->SetS(); + } + // Dilate the faces and vertices + for (uint16_t dilate_step = 0; dilate_step < iteration + 1; dilate_step++) { + vcg::tri::UpdateSelection::VertexFromFaceLoose(mesh); + vcg::tri::UpdateSelection::FaceFromVertexLoose(mesh); + } + // Select vertices from current faces and remove all selected + vcg::tri::UpdateSelection::VertexClear(mesh); + vcg::tri::UpdateSelection::VertexFromFaceStrict(mesh); + for (TMesh::FaceIterator it = mesh.face.begin(); it != mesh.face.end(); ++it) { + if (!it->IsD() && it->IsS()) + vcg::tri::Allocator::DeleteFace(mesh, *it); + } + for (TMesh::VertexIterator it = mesh.vert.begin(); it != mesh.vert.end(); ++it) { + if (!it->IsD() && it->IsS()) + vcg::tri::Allocator::DeleteVertex(mesh, *it); + } + if (verbose) std::cout << " -- self-intersect faces: " << faces.size() << std::endl; + vcg::tri::Clean::RemoveUnreferencedVertex(mesh); + vcg::tri::Allocator::CompactEveryVector(mesh); + vcg::tri::UpdateTopology::FaceFace(mesh); + if (verbose) std::cout << " -- Remove faces [OK]" << std::endl; + vcg::tri::Clean::RemoveSmallConnectedComponentsDiameter(mesh, minimum_diameter * mesh.bbox.Diag()); + vcg::tri::Clean::RemoveUnreferencedVertex(mesh); + if (verbose) std::cout << " -- Remove small components " << minimum_diameter * mesh.bbox.Diag() << " [OK]" << std::endl; + vcg::tri::Clean::RemoveNonManifoldFace(mesh); + vcg::tri::UpdateTopology::FaceFace(mesh); + if (verbose) std::cout << " -- Remove non-manifold edges [OK]" << std::endl; + if (vcg::tri::Clean::CountNonManifoldEdgeFF(mesh) > 0) { + std::cout << "[Warning]: closing holes failed: Mesh has some not 2-manifold edges" << std::endl; + return false; + } + vcg::tri::Hole::EarCuttingIntersectionFill>(mesh, maxSize, false); + if (verbose) std::cout << " -- Close holes [OK]" << std::endl; + vcg::tri::UpdateSelection::FaceClear(mesh); + vcg::tri::UpdateSelection::VertexClear(mesh); + } + + vcg::tri::UpdateSelection::FaceClear(mesh); + vcg::tri::UpdateSelection::VertexClear(mesh); + vcg::tri::UpdateFlags::FaceBorderFromNone(mesh); + vcg::tri::UpdateFlags::VertexBorderFromFaceBorder(mesh); + vcg::tri::UpdateSelection::FaceFromBorderFlag(mesh); + vcg::tri::UpdateSelection::VertexFromBorderFlag(mesh); + nf = vcg::tri::UpdateSelection::FaceCount(mesh); + nv = vcg::tri::UpdateSelection::VertexCount(mesh); + if (verbose) std::cout << " -- Count border edge v:" << nv << " f:" << nf << std::endl; + if (nf > 0) { + // Dilate the faces and vertices + //for (uint16_t dilate_step = 0; dilate_step < iteration + 1; dilate_step++) { + vcg::tri::UpdateSelection::VertexFromFaceLoose(mesh); + vcg::tri::UpdateSelection::FaceFromVertexLoose(mesh); + //} + vcg::tri::UpdateSelection::VertexClear(mesh); + vcg::tri::UpdateSelection::VertexFromFaceStrict(mesh); + for (TMesh::FaceIterator it = mesh.face.begin(); it != mesh.face.end(); ++it) { + if (!it->IsD() && it->IsS()) + vcg::tri::Allocator::DeleteFace(mesh, *it); + } + for (TMesh::VertexIterator it = mesh.vert.begin(); it != mesh.vert.end(); ++it) { + if (!it->IsD() && it->IsS()) + vcg::tri::Allocator::DeleteVertex(mesh, *it); + } + vcg::tri::Allocator::CompactEveryVector(mesh); + vcg::tri::UpdateTopology::FaceFace(mesh); + if (verbose) std::cout << " -- Remove Border faces [OK]" << std::endl; + vcg::tri::Clean::RemoveSmallConnectedComponentsDiameter(mesh, minimum_diameter * mesh.bbox.Diag()); + vcg::tri::Clean::RemoveUnreferencedVertex(mesh); + if (verbose) std::cout << " -- Remove small components " << minimum_diameter * mesh.bbox.Diag() << " [OK]" << std::endl; + vcg::tri::Clean::RemoveNonManifoldFace(mesh); + vcg::tri::UpdateTopology::FaceFace(mesh); + if (verbose) std::cout << " -- Remove non-manifold edges [OK]" << std::endl; + if (vcg::tri::Clean::CountNonManifoldEdgeFF(mesh) > 0) { + std::cout << "[Warning]: closing holes failed: Mesh has some not 2-manifold edges" << std::endl; + return false; + } + vcg::tri::Hole::EarCuttingIntersectionFill>(mesh, maxSize, false); + if (verbose) std::cout << " -- Close holes [OK]" << std::endl; + + vcg::tri::UpdateSelection::FaceClear(mesh); + vcg::tri::UpdateFlags::FaceBorderFromNone(mesh); + vcg::tri::UpdateFlags::VertexBorderFromFaceBorder(mesh); + vcg::tri::UpdateSelection::FaceFromBorderFlag(mesh); + vcg::tri::UpdateSelection::VertexFromBorderFlag(mesh); + nf = vcg::tri::UpdateSelection::FaceCount(mesh); + } + + vcg::tri::Clean::SelfIntersections(mesh, faces); + iteration++; + } + + vcg::tri::Allocator::CompactEveryVector(mesh); + + mesh.face.DisableVFAdjacency(); + + if (faces.size() > 0 && nf > 0) return false; + return true; +} + +inline void report_mesh(TMesh& mesh) { + vcg::tri::UpdateTopology::FaceFace(mesh); + int connectedComponentsNum = vcg::tri::Clean::CountConnectedComponents(mesh); + std::cout + << "[Topology Measurement] " << std::endl + << "-- Mesh is composed by " << connectedComponentsNum << " connected component(s)" << std::endl; + + int edgeNum = 0, edgeBorderNum = 0, edgeNonManifoldNum = 0; + vcg::tri::Clean::CountEdgeNum(mesh, edgeNum, edgeBorderNum, edgeNonManifoldNum); + + int vertManifNum = vcg::tri::Clean::CountNonManifoldVertexFF(mesh, false); + + std::vector faces; + vcg::tri::Clean::SelfIntersections(mesh, faces); + + std::cout + << "-- border edge: " << edgeBorderNum << std::endl + << "-- non-manifold edge: " << edgeNonManifoldNum << std::endl + << "-- non-manifold vertex: " << vertManifNum << std::endl + << "-- self-intersect faces: " << faces.size() << std::endl; + + if (edgeNonManifoldNum == 0 && vertManifNum == 0) { + int holeNum = vcg::tri::Clean::CountHoles(mesh); + int genus = vcg::tri::Clean::MeshGenus(mesh.vn, edgeNum, mesh.fn, holeNum, connectedComponentsNum); + + std::cout + << "-- Mesh is two-manifold " << std::endl + << "-- Mesh has " << holeNum << " holes" << std::endl + << "-- Genus is " << genus << std::endl; + } + +} + +inline bool is_mesh_manifold(TMesh& mesh) { + int edgeNum = 0, edgeBorderNum = 0, edgeNonManifoldNum = 0; + vcg::tri::Clean::CountEdgeNum(mesh, edgeNum, edgeBorderNum, edgeNonManifoldNum); + int vertManifNum = vcg::tri::Clean::CountNonManifoldVertexFF(mesh, false); + return (edgeNonManifoldNum == 0 && vertManifNum == 0); +} \ No newline at end of file diff --git a/include/OptimalSlice.hpp b/include/OptimalSlice.hpp new file mode 100644 index 0000000..4bf9560 --- /dev/null +++ b/include/OptimalSlice.hpp @@ -0,0 +1,1302 @@ +/* + * This is the implement slice algorithm from Rodrigo, 2017 (An Optimal Algorithm for 3D Triangle Mesh Slicing) + * which is claimed to be faster than slic3r and CGAL method. + */ +#pragma once +#ifndef SLICE_PRECISION +// There's a problem of double hashing with the precision less than 1e-8 (e.g. 1e-10) +// when performed contour constructing +#define SLICE_PRECISION 1e-8 +#endif +#define DOUBLE_EQ(x,y) (abs(x - y) < SLICE_PRECISION) +#define DOUBLE_GT(x,y) ((x - y) > SLICE_PRECISION) +#define DOUBLE_LT(x,y) ((y - x) > SLICE_PRECISION) +#define DOUBLE_GT_EQ(x,y) (DOUBLE_EQ(x,y) || DOUBLE_GT(x,y)) +#define DOUBLE_LT_EQ(x,y) (DOUBLE_EQ(x,y) || DOUBLE_LT(x,y)) +#define USE_PARALLEL + +#include "Mesh.h" +#include "MaxHeap.hpp" +#include "tbb/tbb.h" + +namespace slice { + + enum Direction {X = 0,Y,Z}; + enum PolygonSide {OUTSIDE = 0, INSIDE = 1, PAIRED = 2, UNDEFINED = 3}; + + struct Point2d { + double x; + double y; + bool operator==(const Point2d& ls) const { + return (DOUBLE_EQ(x, ls.x) && DOUBLE_EQ(y, ls.y)); + } + bool operator< (const Point2d& ls) const { + if (x < ls.x) return true; + return y < ls.y; + } + Point2d operator-(const Point2d &rh) const { + return {x - rh.x, y - rh.y}; + } + Point2d operator+(const Point2d& rh) const { + return { x + rh.x, y + rh.y }; + } + Point2d operator-() const { + return { -x, -y }; + } + }; + + struct Point3d { + double x; + double y; + double z; + bool operator==(const Point3d& ls) const { + return (DOUBLE_EQ(x, ls.x) && DOUBLE_EQ(y, ls.y) && DOUBLE_EQ(z, ls.z)); + } + bool operator< (const Point3d& ls) const { + if (x < ls.x) return true; + else if DOUBLE_EQ(x, ls.x) + if (y < ls.y) return true; + else if (DOUBLE_EQ(y, ls.y)) { + return (z < ls.z); + } + return false; + } + }; + + Point2d make_point2d(Point3d p, int direction = Direction::Z) { + if (direction == Direction::X) return Point2d{ p.y, p.z }; + else if (direction == Direction::Y) return Point2d{ p.x, p.z }; + return Point2d{ p.x, p.y }; + } + + class Triangle { + public: + Point3d v[3]; + double min[3], max[3]; + Triangle(TMesh::FaceType face) { + assert(face.VN() == 3); + vcg::Point3d point = face.P(0); + v[0].x = point[0]; + v[0].y = point[1]; + v[0].z = point[2]; + point = face.P(1); + v[1].x = point[0]; + v[1].y = point[1]; + v[1].z = point[2]; + point = face.P(2); + v[2].x = point[0]; + v[2].y = point[1]; + v[2].z = point[2]; + updateMinMax(); + } + void updateMinMax() { + min[0] = std::min({ v[0].x, v[1].x, v[2].x }); + min[1] = std::min({ v[0].y, v[1].y, v[2].y }); + min[2] = std::min({ v[0].z, v[1].z, v[2].z }); + max[0] = std::max({ v[0].x, v[1].x, v[2].x }); + max[1] = std::max({ v[0].y, v[1].y, v[2].y }); + max[2] = std::max({ v[0].z, v[1].z, v[2].z }); + } + + double minOf(int i, int j, int direction) { + assert(direction >= 0 && direction <= 2); + assert(i >= 0 && i <= 2); + assert(j >= 0 && j <= 2); + if (direction == 0) + return v[i].x < v[j].x ? v[i].x : v[j].x; + else if (direction == 1) + return v[i].y < v[j].y ? v[i].y : v[j].y; + else + return v[i].z < v[j].z ? v[i].z : v[j].z; + } + double maxOf(int i, int j, int direction) { + assert(direction >= 0 && direction <= 2); + assert(i >= 0 && i <= 2); + assert(j >= 0 && j <= 2); + if (direction == 0) + return v[i].x < v[j].x ? v[j].x : v[i].x; + else if (direction == 1) + return v[i].y < v[j].y ? v[j].y : v[i].y; + else + return v[i].z < v[j].z ? v[j].z : v[i].z; + } + + bool operator== (const Triangle& ls) const { + return (v[0] == ls.v[0]) && (v[1] == ls.v[1]) && (v[2] == ls.v[2]); + } + + bool operator< (const Triangle& ls) const { + if (v[0] < ls.v[0]) return true; + else if (v[0] == ls.v[0]) + if (v[1] < ls.v[1]) return true; + else if (v[1] == ls.v[1]) { + return (v[2] < ls.v[2]); + } + return false; + } + }; + + class Line { + public: + Point3d v[2]; + + Line() { + v[0] = Point3d{ 0, 0, 0 }; + v[1] = Point3d{ 0, 0, 0 }; + } + + Line(Point3d v0, Point3d v1, size_t index) { + v[0] = v0; + v[1] = v1; + sort(); + } + + void sort() { + if (v[1] < v[0]) std::swap(v[0], v[1]); + } + + bool operator== (const Line& ls) const { + return (v[0] == ls.v[0]) && (v[1] == ls.v[1]); + } + + bool operator< (const Line& ls) const { + if (v[0] < ls.v[0]) return true; + else if (v[0] == ls.v[0]) return (v[1] < ls.v[1]); + return false; + } + }; + + class SupportLine2d { + public: + double x, y; + double m, theta, c; + bool is_vertical = false; + SupportLine2d(double _x, double _y, double _theta) : x(_x), y(_y), theta(_theta) { + update(); + } + SupportLine2d(Point2d p, double _theta) : x(p.x), y(p.y), theta(_theta) { + update(); + } + void update() { + // adjust theta to be in range [-90, 90] + if (theta > 90) theta -= 180; + else if (theta < -90) theta += 180; + if (DOUBLE_EQ(abs(theta), 90)) { + is_vertical = true; + m = 0; + c = x; + } + else { + is_vertical = false; + m = tan(theta * M_PI / 180); + c = y - m * x; + } + } + void update(double _x, double _y) { + x = _x; + y = _y; + update(); + } + + void update(const Point2d& p) { + update(p.x, p.y); + } + + SupportLine2d& operator+= (const double theta) { + this->theta += theta; + update(); + return *this; + } + }; + + double two_line_distance(const SupportLine2d& line1, const SupportLine2d &line2) { + if (DOUBLE_EQ(line1.theta, line2.theta) && DOUBLE_EQ(line1.m, line2.m) && line1.is_vertical == line2.is_vertical) { + return abs(line2.c - line1.c) / sqrt(line1.m * line1.m + 1); + } + return 0; + } + + // return the 0 < angle <= 90 degree between line and vertex P + double line_point_angle(const SupportLine2d& line, const Point2d& p) { + double angle; + if (DOUBLE_EQ(p.x, line.x)) angle = 90; + else if (DOUBLE_EQ(p.y, line.y)) angle = 0; + else angle = atan((p.y - line.y) / (p.x - line.x)) * 180 / M_PI; + // calculate delta + if (DOUBLE_GT_EQ(line.theta, angle)) angle = line.theta - angle; + else angle = 180 + line.theta - angle; + // normalized angle + if (angle > 90) angle -= 180; + return angle; + } + + // return the 0 <= angle < 180 degree between line and vertex P + double line_edge_angle(const SupportLine2d& line, const Point2d& delta, bool direction_ccw = false) { + double angle; + if (DOUBLE_EQ(delta.x, 0)) angle = 90; + else if (DOUBLE_EQ(delta.y, 0)) angle = 0; + else angle = atan(delta.y / delta.x) * 180 / M_PI; + // calculate delta + if (DOUBLE_GT_EQ(line.theta, angle)) angle = line.theta - angle; + else angle = 180 + line.theta - angle; + if (direction_ccw && angle > 0) angle = 180 - angle; + // normalized angle + if (angle < 0) angle += 180; + return angle; + } + + class FeretDiameter { + public: + bool empty = true; + double min; + double max; + double perpendicularMax; + double perimeter; + double angleMin; + double angleMax; + FeretDiameter() : min(0), max(0), perpendicularMax(0), perimeter(0), angleMin(0), angleMax(0) {} + FeretDiameter(const SupportLine2d (&lines)[4]) { + double d1 = two_line_distance(lines[0], lines[2]); + double d2 = two_line_distance(lines[1], lines[3]); + if (d1 > d2) { + min = d2; angleMin = lines[1].theta; + max = d1; angleMax = lines[0].theta; + } + else { + min = d1; angleMin = lines[0].theta; + max = d2; angleMax = lines[1].theta; + } + perimeter = 0; + perpendicularMax = max; + empty = false; + } + void update(const SupportLine2d (&lines)[4]) { + double d1 = two_line_distance(lines[0], lines[2]); + double d2 = two_line_distance(lines[1], lines[3]); + double angle1 = lines[0].theta; + double angle2 = lines[1].theta; + if (d1 > d2) { + std::swap(d1, d2); + std::swap(angle1, angle2); + } + if (min > d1) { + min = d1; + angleMin = angle1; + perpendicularMax = d2; + } + if (max < d1) { + max = d1; + angleMax = angle1; + } + if (max < d2) { + max = d2; + angleMax = angle2; + } + } + }; + + typedef std::vector Plane; + typedef std::vector Triangles; + typedef std::vector Layer; + typedef std::vector Lines; + typedef std::vector Slice; + typedef std::vector Polygon; + typedef std::vector Polygons; + typedef std::vector ContourSlice; + typedef std::pair PairPoint2d; + typedef std::unordered_map ContourHash; + typedef std::vector PolygonSides; + typedef std::pair DistanceIndexPair; + + std::ostream& operator<< (std::ostream& out, Point2d const& data) { + out << "[" << data.x << "," << data.y << "]"; + return out; + } + + std::ostream& operator<< (std::ostream& out, PairPoint2d const& data) { + out << "(" << data.first << " " << data.second << ")"; + return out; + } + + std::ostream& operator<< (std::ostream& out, Point3d const& data) { + out << "[" << data.x << "," << data.y << "," << data.z << "]"; + return out; + } + + std::ostream& operator<< (std::ostream& out, Triangle const& data) { + out << "slice::Triangle(" << data.v[0] << " " << data.v[1] << " " << data.v[2] << ")"; + return out; + } + + std::ostream& operator<< (std::ostream& out, Line const& data) { + out << "slice::Line(" << data.v[0] << " " << data.v[1] << ")"; + return out; + } + + std::ostream& operator<< (std::ostream& out, FeretDiameter const& data) { + out << "slice::FeretDiameter(min:" << data.min << ", max:" << data.max << ", pmax:" << data.perpendicularMax << ")"; + return out; + } + + std::ostream& operator<< (std::ostream& out, SupportLine2d const& data) { + out << "slice::SupportLine2d(x:" << data.x << ", y:" << data.y << ", m:" << data.m << ", c:" << data.c << ", theta:" << data.theta << "[" << data.is_vertical <<"])"; + return out; + } + + std::ostream& operator<< (std::ostream& out, Polygon const& data) { + out << "slice::Polygon{" << std::endl; + for (auto d = data.begin(); d != data.end(); ++d) { + out << " -- " << (*d) << std::endl; + } + out << "}" << std::endl; + return out; + } +} + +namespace std { + template<> struct hash { + size_t operator()(const slice::Point2d& p) const noexcept { + size_t x = hash()(llround(p.x/SLICE_PRECISION)); + size_t y = hash()(llround(p.y/SLICE_PRECISION)); + return x ^ (y << 1); + } + }; +} + +namespace slice { + + inline void build_triangle_list(TMesh& mesh, size_t grid_size, Plane& P, Layer& L, int direction = Direction::Z) { + // Uniform slicing with delta > 0 + // in this case, grid_size = k from Rodrigo paper + assert(grid_size > 1 && direction <= 2 && direction >= 0); + vcg::tri::UpdateBounding::Box(mesh); + vcg::Box3d bbox = mesh.bbox; + double minBBox = bbox.min[direction]; + double maxBBox = bbox.max[direction]; + vcg::Point3d dim = bbox.Dim(); + double delta = dim[direction] / (grid_size - 1); + // build Plane vector P[0...k+1] + P.resize(grid_size + 2); + P[0] = minBBox - 10 * delta; + P[1] = minBBox; + P[grid_size + 1] = maxBBox + 10 * delta; + for (size_t i = 2; i <= grid_size; i++) + P[i] = P[i - 1] + delta; + // initialize layer L[0...k+1] + L.resize(grid_size + 2); + for (size_t i = 0; i <= grid_size + 1; i++) L[i].clear(); + // foreach triangle in mesh +#ifndef USE_PARALLEL + for (TMesh::FaceIterator it = mesh.face.begin(); it != mesh.face.end(); it++) { + if (!it->IsD()) + { + Triangle triangle(*it); + size_t i = 0; + i = size_t(ceil((triangle.min[direction] - P[1]) / delta) + 1); + assert(i > 0 && i <= grid_size + 1); + L[i].push_back(triangle); + } + } +#else + tbb::spin_mutex writeMutex; + static tbb::affinity_partitioner ap; + tbb::parallel_for( + tbb::blocked_range(0, mesh.face.size()), + [&](const tbb::blocked_range r) { + // Prepare local_L + Layer _L(grid_size + 2); + _L.resize(grid_size + 2); + for (size_t i = 0; i <= grid_size + 1; i++) _L[i].clear(); + for (size_t i = r.begin(); i < r.end(); i++) { + if (!mesh.face[i].IsD()) { + Triangle triangle(mesh.face[i]); + size_t level = size_t(ceil((triangle.min[direction] - P[1]) / delta) + 1); + assert(level > 0 && level <= grid_size + 1); + _L[level].push_back(triangle); + } + } + { + tbb::spin_mutex::scoped_lock lock(writeMutex); + for (size_t i = 0; i <= grid_size + 1; i++) { + L[i].reserve(L[i].size() + _L[i].size()); + L[i].insert(L[i].end(), _L[i].begin(), _L[i].end()); + } + } + }, ap + ); +#endif + } + + inline Point3d compute_point_at_plane(Point3d v0, Point3d v1, double position, int direction = Direction::Z) { + double dx = v1.x - v0.x; + double dy = v1.y - v0.y; + double dz = v1.z - v0.z; + if (direction == 2) { + assert(dz != 0); + double frac = (position - v0.z) / dz; + double x = frac * dx + v0.x; + double y = frac * dy + v0.y; + return Point3d{ x, y, position }; + } + else if (direction == 1) { + assert(dy != 0); + double frac = (position - v0.y) / dy; + double x = frac * dx + v0.x; + double z = frac * dz + v0.z; + return Point3d{ x, position, z }; + } + else { + assert(dx != 0); + double frac = (position - v0.x) / dx; + double y = frac * dy + v0.y; + double z = frac * dz + v0.z; + return Point3d{ position, y, z }; + } + } + + // Modified version of Rodrigo (2017) and Adnan's slicing algorithm (2018) + // (Real-time slicing algorithm for Stereolithography (STL) CAD model applied in additive manufacturing industry) + // The ill-conditioned case will be cured + bool compute_intersection(Triangle t, double position, Line& L, int direction = Direction::Z) { + assert(direction >= 0 && direction <= 2); + assert(t.min[direction] <= position && t.max[direction] >= position); + int np = 0; // number of endpoints on the plane + std::vector found_indexs; + found_indexs.reserve(3); + for (int i = 0; i < 3; i++) { + if ((direction == Direction::X && DOUBLE_EQ(t.v[i].x, position)) || + (direction == Direction::Y && DOUBLE_EQ(t.v[i].y, position)) || + (direction == Direction::Z && DOUBLE_EQ(t.v[i].z, position))) { + np++; + found_indexs.push_back(i); + } + } + if (np == 0) { + int k = 0; + for (int i = 0; i < 3; i++) { + int next_i = (i == 2) ? 0 : i + 1; + double min = t.minOf(i, next_i, direction); + double max = t.maxOf(i, next_i, direction); + if (min <= position && max >= position) { + assert(k < 2); + L.v[k] = compute_point_at_plane(t.v[i], t.v[next_i], position, direction); + k++; + } + } + assert(k == 2); + L.sort(); + return true; + } + else if (np == 1 && DOUBLE_GT(t.max[direction], position) && DOUBLE_LT(t.min[direction], position)) { + assert(found_indexs.size() == 1); + int i = (found_indexs[0] + 1) % 3; + int next_i = (i + 1) % 3; + L.v[0] = t.v[found_indexs[0]]; + L.v[1] = compute_point_at_plane(t.v[i], t.v[next_i], position, direction); + L.sort(); + return true; + } + else if (np == 2) { + assert(found_indexs.size() == 2); + L.v[0] = t.v[found_indexs[0]]; + L.v[1] = t.v[found_indexs[1]]; + L.sort(); + return true; + } + return false; + } + + Slice incremental_slicing(TMesh& mesh, size_t grid_size, int direction = Direction::Z) { + slice::Plane P; + slice::Layer L; + slice::build_triangle_list(mesh, grid_size, P, L, direction); + Slice S(grid_size); + + Triangles A; + for (size_t i = 1; i <= grid_size; i++) { + if (L[i].size() > 0) { + A.reserve(A.size() + L[i].size()); + A.insert(A.end(), L[i].begin(), L[i].end()); + } + S[i - 1].clear(); +#ifndef USE_PARALLEL + for (Triangles::iterator t = A.begin(); t != A.end();) { + if (t->max[direction] < P[i]) { + t = A.erase(t); + } + else { + Line line; + if (t->max[direction] >= P[i] && t->min[direction] <= P[i]) { + if (compute_intersection(*t, P[i], line, direction)) { + S[i - 1].push_back(line); + } + } + t++; + } + } +#else + tbb::spin_mutex concatMutex; + tbb::spin_mutex writeMutex; + static tbb::affinity_partitioner ap; + std::vector deleteIndex; + deleteIndex.clear(); + + Triangles new_A; + new_A.reserve(A.size()); + tbb::parallel_for( + tbb::blocked_range(0, A.size()), + [&](const tbb::blocked_range &r) + { + Triangles local_A; + Lines lines; + lines.reserve(local_A.size()); + std::copy(A.begin() + r.begin(), A.begin() + r.end(), std::back_inserter(local_A)); + for (Triangles::iterator t = local_A.begin(); t != local_A.end();) { + if (t->max[direction] < P[i]) { + t = local_A.erase(t); + } + else { + Line line; + if (t->max[direction] >= P[i] && t->min[direction] <= P[i]) { + if (compute_intersection(*t, P[i], line, direction)) { + //tbb::spin_mutex::scoped_lock lock(writeMutex); + //S[i - 1].push_back(line); + lines.push_back(line); + } + } + t++; + } + } + if (local_A.size() > 0) { + tbb::spin_mutex::scoped_lock lock(concatMutex); + new_A.insert(new_A.end(), local_A.begin(), local_A.end()); + } + if (lines.size() > 0) { + tbb::spin_mutex::scoped_lock lock(writeMutex); + S[i - 1].reserve(S[i - 1].size() + lines.size()); + S[i - 1].insert(S[i - 1].end(), lines.begin(), lines.end()); + } + }, + ap + ); + A = new_A; +#endif + } + return S; + } + +#ifndef USE_PARALLEL + ContourSlice contour_construct(Slice const& S, int direction = Direction::Z) { + ContourSlice CS(S.size()); + ContourHash hash; + for (size_t i = 0, len = S.size(); i < len; i++) { + CS[i].clear(); + hash.clear(); + hash.reserve(S[i].size() + 1); + for (Lines::const_iterator l = S[i].begin(); l != S[i].end(); l++) { + Point2d u = make_point2d(l->v[0], direction); + Point2d v = make_point2d(l->v[1], direction); + ContourHash::iterator item = hash.find(u); + if (item == hash.end()) + hash.emplace(u, make_pair(v, v)); + else { + Point2d w = item->second.first; + Point2d target = item->second.second; + if (w == target) { + item->second = make_pair(w, v); + } + } + item = hash.find(v); + if (item == hash.end()) + hash.emplace(v, make_pair(u, u)); + else { + Point2d w = item->second.first; + Point2d target = item->second.second; + if (w == target) { + item->second = make_pair(w, u); + } + } + } + while (!hash.empty()) { + ContourHash::const_iterator item = hash.begin(); + assert(item != hash.end()); + Polygon C; + C.push_back(item->first); + C.push_back(item->second.first); + Point2d last = item->second.second; + hash.erase(item); + for (size_t j = 1;; j++) { + item = hash.find(C[j]); + //if (item == hash.end()) break; + assert(item != hash.end()); + if (!(C[j] == last)) { + if (item->second.first == C[j - 1]) + C.push_back(item->second.second); + else + C.push_back(item->second.first); + } + hash.erase(item); + if (C[j] == last) break; + } + CS[i].push_back(C); + } + } + return CS; + } +#else + ContourSlice contour_construct(Slice const& S, int direction = Direction::Z) { + ContourSlice CS(S.size()); + static tbb::affinity_partitioner ap; + tbb::spin_mutex printMutex; + tbb::parallel_for( + tbb::blocked_range(0, S.size()), + [&](const tbb::blocked_range& r) { + for (size_t i = r.begin(); i < r.end(); i++) { + CS[i].clear(); + ContourHash hash; + hash.clear(); + hash.reserve(S[i].size() + 1); + for (Lines::const_iterator l = S[i].begin(); l != S[i].end(); l++) { + Point2d u = make_point2d(l->v[0], direction); + Point2d v = make_point2d(l->v[1], direction); + ContourHash::iterator item = hash.find(u); + if (item == hash.end()) + hash.emplace(u, make_pair(v, v)); + else { + Point2d w = item->second.first; + Point2d target = item->second.second; + if (w == target) { + item->second = make_pair(w, v); + } + } + item = hash.find(v); + if (item == hash.end()) + hash.emplace(v, make_pair(u, u)); + else { + Point2d w = item->second.first; + Point2d target = item->second.second; + if (w == target) { + item->second = make_pair(w, u); + } + } + } + while (!hash.empty()) { + ContourHash::const_iterator item = hash.begin(); + assert(item != hash.end()); + Polygon C; + C.push_back(item->first); + C.push_back(item->second.first); + Point2d last = item->second.second; + hash.erase(item); + for (size_t j = 1;; j++) { + item = hash.find(C[j]); + // TODO: fixed this bugs + if (item == hash.end()) { + CS[i].clear(); + break; + } + assert(item != hash.end()); + if (!(C[j] == last)) { + if (item->second.first == C[j - 1]) + C.push_back(item->second.second); + else + C.push_back(item->second.first); + } + hash.erase(item); + if (C[j] == last) break; + } + CS[i].push_back(C); + } + } + }, + ap + ); + return CS; + } +#endif + // Finley DR (2007) Point-in-polygon algorithm + // determining whether a point is inside a complex polygon. + // Available at: http://alienryderflex.com/polygon/. + // return true = inside, false = outside + bool is_point_inside_polygon(const Point2d &p, const Polygon &C) { + bool oddNodes = 0; + size_t size = C.size(); + for (size_t i = 0; i < size; i++) { + size_t j = (i == size - 1) ? 0 : i + 1; + if (((C[i].y < p.y && C[j].y >= p.y) || (C[j].y < p.y && C[i].y >= p.y)) && (C[i].x <= p.x || C[j].x <= p.x)) { + oddNodes ^= (((p.y - C[i].y) / (C[j].y - C[i].y) * (C[j].x - C[i].x) + C[i].x) < p.x); + } + } + return oddNodes; + } + + PolygonSides contour_inside_test(const Polygons& C, std::vector>& pairList) { + PolygonSides position(C.size()); + pairList.clear(); + std::vector countInside(C.size(), 0); + std::map> pairMapping; + for (size_t i = 0, size = C.size(); i < size; i++) { + std::vector insidePairedList; + for (size_t j = 0, size = C.size(); j < size; j++) { + if (i == j) continue; + if (is_point_inside_polygon(C[i][0], C[j])) { + countInside[i]++; + insidePairedList.push_back(j); + } + } + // Fill the outside polygon + if (countInside[i] == 0) position[i] = PolygonSide::OUTSIDE; + else if ((countInside[i] % 2) == 0) { + position[i] = PolygonSide::PAIRED; + pairMapping.emplace(i, insidePairedList); + } + } + // Match a pair polygon + for (auto it = pairMapping.begin(); it != pairMapping.end(); ++it) { + auto target = countInside[it->first] - 1; + assert(target > 0); + for (auto list_it = it->second.begin(); list_it != it->second.end(); ++list_it) { + if (target == countInside[*list_it]) { + position[*list_it] = PolygonSide::PAIRED; + pairList.push_back(make_pair(it->first, *list_it)); + } + } + } + // Finally, fill the inside polygon + for (size_t i = 0, len = position.size(); i < len; i++) { + if ((countInside[i] % 2) == 1 && position[i] == PolygonSide::UNDEFINED) + position[i] = PolygonSide::INSIDE; + } + return position; + } + + PolygonSides contour_inside_test(const Polygons& C) { + std::vector> pairs; + return contour_inside_test(C, pairs); + } + + // This formular came from the cross product of two vectors that is the area of PARALLELOGRAM + // Then the area of polygon is 1/2 * sum of all parallelogram + // ref: http://geomalgorithms.com/a01-_area.html + double measure_polygon_area(const Polygon& C) { + double A2 = 0; + for (size_t i = 0, s = C.size(); i < s; i++) { + size_t i_prev = (i == 0) ? s - 1 : i - 1; + size_t i_next = (i == s - 1) ? 0 : i + 1; + A2 += C[i].x * (C[i_next].y - C[i_prev].y); + } + return abs(A2) * 0.5; + } + + double measure_point_square_distance(const Point2d& p1, const Point2d& p2) { + return (p1.x - p2.x) * (p1.x - p2.x) + (p1.y - p2.y) * (p1.y - p2.y); + } + + double measure_point_distance(const Point2d& p1, const Point2d& p2) { + return sqrt((p1.x - p2.x) * (p1.x - p2.x) + (p1.y - p2.y) * (p1.y - p2.y)); + } + + double measure_point_magnitude(const Point2d& p) { + return sqrt(p.x * p.x + p.y * p.y); + } + + // To find orientation of ordered triplet (p, q, r). + // The function returns following values + // 0 --> p, q and r are colinear + // 1 --> Clockwise (Turn Right) + // -1 --> Counterclockwise (Turn Left) + int orientation(const Point2d& p, const Point2d& q, const Point2d& r) { + double val = (q.y - p.y) * (r.x - q.x) - (q.x - p.x) * (r.y - q.y); + if (DOUBLE_EQ(val, 0)) return 0; + if (val > 0) return 1; + else if (val < 0) return -1; + return 0; + } + + struct CompareOrientation { + Point2d origin_; + CompareOrientation(const Point2d& origin) : origin_(origin) {} + bool operator() (const Point2d& q, const Point2d& r) { + int o = orientation(origin_, q, r); + if (o == 0) + return (measure_point_square_distance(origin_, r) >= measure_point_square_distance(origin_, q)); + return (o < 0); + } + }; + + // Graham scan algorithm for constructing convex hull + // the direction is counterclockwise + // https://www.geeksforgeeks.org/convex-hull-set-2-graham-scan/ + Polygon convexhull(Polygon points) { + if (points.size() < 3) return points; + double ymin = points[0].y; + size_t index = 0; + // Find the bottom-left point + for (size_t i = 1, size = points.size(); i < size; i++) { + if (points[i].y < ymin || (DOUBLE_EQ(points[i].y, ymin) && points[i].x < points[index].x)) { + ymin = points[i].y; + index = i; + } + } + assert(points.size() > 3 && index >= 0 && index < points.size()); + std::swap(points[0], points[index]); + Point2d origin = points.front(); + std::sort(points.begin() + 1, points.end(), CompareOrientation(origin)); + // delete the colinear points + Polygon p; + for (Polygon::iterator p = points.begin() + 1; p != points.end();) { + Polygon::iterator next_p = (p + 1); + if (next_p != points.end()) { + if (orientation(points.front(), *p, *next_p) == 0) { + p = points.erase(p); + } + else { + p++; + } + } + else { + break; + } + } + if (points.size() < 3) return points; + // Create convexhull polygon with points[0..2] + p.reserve(points.size()); + p.push_back(points[0]); + p.push_back(points[1]); + p.push_back(points[2]); + for (size_t i = 3, size = points.size(); i < size; i++) { + Point2d prev = *(p.end() - 2); + Point2d current = p.back(); + Point2d next = points[i]; + // check if clockwise (Turn right), then remove current point from convexhull polygon + while (slice::orientation(prev, current, next) > 0) { + p.pop_back(); + if (p.size() < 2) break; + current = p.back(); + prev = *(p.end() - 2); + } + p.push_back(next); + } + return p; + } + + // Support function generate the farthest point in direction d + // Complexity O(m+n) + Point2d gjk_support(const Polygon& P, const Polygon& Q, const Point2d& d) { + assert(P.size() > 0 && Q.size() > 0); + Point2d p = P[0]; + double max_dot = p.x * d.x + p.y * d.y; + for (auto it = P.begin(); it != P.end(); ++it) { + double dot = it->x * d.x + it->y * d.y; + if (dot > max_dot) { + max_dot = dot; + p = (*it); + } + } + Point2d q = Q[0]; + max_dot = q.x * -d.x + q.y * -d.y; + for (auto it = Q.begin(); it != Q.end(); ++it) { + double dot = it->x * -d.x + it->y * -d.y; + if (dot > max_dot) { + max_dot = dot; + q = (*it); + } + } + return p - q; + } + + // return the origin-closest point on simplex A,B + // complexity: O(1) + Point2d gjk_closest_point_to_origin(const Point2d& A, const Point2d& B) { + Point2d AB = B - A; + double norm_AB = (AB.x * AB.x + AB.y * AB.y); + if (DOUBLE_EQ(norm_AB, 0)) return A; + double lambda_2 = (-AB.x * A.x + -AB.y * A.y) / norm_AB; + if (lambda_2 > 1) return B; + else if (lambda_2 < -1) return A; + double lambda_1 = 1 - lambda_2; + return { lambda_1 * A.x + lambda_2 * B.x, lambda_1 * A.y + lambda_2 * B.y }; + } + + Point2d center_point_distance(const Polygon& P, const Polygon& Q) { + Point2d d; + double x = 0, y = 0; + for (auto it = P.begin(); it != P.end(); ++it) { + x += it->x; + y += it->y; + } + d.x = x / P.size(); + d.y = y / P.size(); + x = 0; + y = 0; + for (auto it = Q.begin(); it != Q.end(); ++it) { + x += it->x; + y += it->y; + } + d.x -= x / Q.size(); + d.y -= y / Q.size(); + return d; + } + + // Minkowski difference of two convex: P and Q + // return S = P - Q, Complexity O(m+n) + Polygon minkwoski_difference(const Polygon& P, Polygon Q) { + Polygon S; + if (P.size() >= 3 && Q.size() >= 3) { + // compute -Q and find the bottom-left vertex of Q + size_t index_q = 0; + double min_y = -Q[0].y; + for (Polygon::iterator it = Q.begin(); it != Q.end(); it++) { + it->x *= -1; + it->y *= -1; + if (min_y > it->y || (DOUBLE_EQ(min_y, it->y) && it->x < Q[index_q].x )) { + min_y = it->y; + index_q = (size_t)(it - Q.begin()); + } + } + size_t index_p = 0; + min_y = P[0].y; + // find the bottom-left vertex of P + for (Polygon::const_iterator it = P.begin(); it != P.end(); it++) { + if (min_y > it->y || (DOUBLE_EQ(min_y, it->y) && it->x < P[index_p].x)) { + min_y = it->y; + index_p = (size_t)(it - P.begin()); + } + } + SupportLine2d line(P[index_p] + Q[index_q], 0); + S.push_back({ line.x, line.y }); + size_t last_p = index_p; + size_t _count = 0; + Point2d first_point = S.front(); + //std::cout << line << std::endl; + do { + size_t next_p = (index_p == P.size() - 1) ? 0 : index_p + 1; + size_t next_q = (index_q == Q.size() - 1) ? 0 : index_q + 1; + Point2d edge_q = Q[next_q] - Q[index_q]; + Point2d edge_p = P[next_p] - P[index_p]; + double angle_p = line_edge_angle(line, edge_p, true); + double angle_q = line_edge_angle(line, edge_q, true); + //std::cout << "Edge P:" << edge_p << " " << angle_p << " Q:" << edge_q << " " << angle_q << std::endl; + if (angle_p > angle_q) { + // insert edge q into S and update index_q + line.update(line.x + edge_q.x, line.y + edge_q.y); + line += angle_q; + index_q = next_q; + } + else { + line.update(line.x + edge_p.x, line.y + edge_p.y); + line += angle_p; + index_p = next_p; + _count++; + } + //std::cout << line << std::endl; + if (index_p != last_p || _count == 0) { + S.push_back({ line.x,line.y }); + } + } while (index_p != last_p || _count == 0); + } + return S; + } + + // Implement from JAVA lib + // ref: http://www.dyn4j.org/2010/04/gjk-distance-closest-points/#gjk-closest + // return -1 if the polygon is intersecting + double gjk_minimal_distance(const Polygon& P, const Polygon& Q, double tolerance = 1e-2) { + if (P.size() < 3 || Q.size() < 3) return -1; + Polygon P1 = convexhull(P); + Polygon Q1 = convexhull(Q); + if (P1.size() < 3 || Q1.size() < 3) return -1; + //Polygon Diff = minkwoski_difference(P1, Q1); + //assert(Diff.size() > 3); + //std::copy(Diff.begin(), Diff.end(), std::ostream_iterator(std::cout, " ")); + Point2d d = center_point_distance(P1, Q1); + Point2d simplex_a = gjk_support(P1, Q1, d); + Point2d simplex_b = gjk_support(P1, Q1, -d); + d = -gjk_closest_point_to_origin(simplex_a, simplex_b); + //std::cout << std::endl << "Pre: a:" << simplex_a << " b:" << simplex_b << " d:" << d << std::endl; + size_t _count = P1.size() + Q1.size(); + while (_count > 0) { + if (DOUBLE_EQ(d.x * d.x + d.y * d.y, 0)) return 0; + Point2d c = gjk_support(P1, Q1, d); + //std::cout << " -- c:" << c << std::endl; + // check new point c is better than simplex a, b + if (( d.x * c.x + d.y * c.y ) - (simplex_a.x * d.x + simplex_a.y * d.y) < tolerance) { + return measure_point_magnitude(d); + } + Point2d p1 = gjk_closest_point_to_origin(simplex_a, c); + Point2d p2 = gjk_closest_point_to_origin(c, simplex_b); + if (measure_point_magnitude(p1) < measure_point_magnitude(p2)) { + simplex_b = c; + d = -p1; + } + else { + simplex_a = c; + d = -p2; + } + //std::cout << " -- a:" << simplex_a << " b:" << simplex_b << " d:" << d << std::endl; + _count--; + } + //[Warning] GJK Iteration timeout: Found the intersect polygon + return measure_point_magnitude(d); + } + + FeretDiameter measure_polygon_feret_diameter(const Polygon& p) { + // find max min in convexhull + Polygon convex = convexhull(p); + if (convex.size() < 3) return FeretDiameter(); + size_t size = size = convex.size(); + size_t index_point[4] = { 0,0,0,0 }; // store top-most, right-most, bottom-most, left-most index + double minX = convex[0].x, minY = convex[0].y, maxX = convex[0].x, maxY = convex[0].y; + double perimeter = 0; + for (size_t i = 0; i < size; i++) { + double x = convex[i].x, y = convex[i].y; + size_t next_i = (i == size - 1) ? 0 : i + 1; + perimeter += sqrt(measure_point_square_distance(convex[next_i], convex[i])); + if (maxY < y || (DOUBLE_EQ(maxY, y) && x < convex[index_point[0]].x)) { + maxY = y; + index_point[0] = i; + } + if (maxX < x || (DOUBLE_EQ(maxX, x) && y > convex[index_point[1]].y)) { + maxX = x; + index_point[1] = i; + } + if (minY > y || (DOUBLE_EQ(minY, y) && x > convex[index_point[2]].x)) { + minY = y; + index_point[2] = i; + } + if (minX > x || (DOUBLE_EQ(minX, x) && y < convex[index_point[3]].y)) { + minX = x; + index_point[3] = i; + } + } + SupportLine2d line[4] = { + SupportLine2d(convex[index_point[0]], 0), + SupportLine2d(convex[index_point[1]], 90), + SupportLine2d(convex[index_point[2]], 0), + SupportLine2d(convex[index_point[3]], 90) + }; + // rotating caliber with perpendicular set of 4 supporting lines + // the conhexhull's direction is counterclockwise and this procedure direction will be clockwise + // the loop will finish when the initial upper-most point reached the bottom-most point + size_t last_index = index_point[1] == 0 ? size - 1 : index_point[1] - 1; + size_t _count = 0; + FeretDiameter feret(line); + feret.perimeter = perimeter; + while (index_point[0] != last_index && _count < size) { + // measure the next minimal angle (min_theta) of 4 lines + double min_angle = 359; + size_t min_index = 0; + double angle[4]; + for (int i = 0; i < 4; i++) { + size_t next_index = index_point[i] == 0 ? size - 1 : (index_point[i] - 1); + angle[i] = line_point_angle(line[i], convex[next_index]); + if (min_angle > angle[i]) { + min_angle = angle[i]; + min_index = i; + } + } + //std::cout << " -- min_angle: " << min_angle << " min_index:" << min_index << std::endl; + //for (int i = 0; i < 4; i++) std::cout << angle[i] << " "; + //std::cout << std::endl; + for (int i = 0; i < 4; i++) { + if (DOUBLE_EQ(min_angle, angle[i])) { + size_t next_index = index_point[i] == 0 ? size - 1 : (index_point[i] - 1); + line[i].update(convex[next_index]); + index_point[i] = next_index; + } + assert(DOUBLE_GT_EQ(min_angle, -1) && DOUBLE_LT_EQ(min_angle, 180)); + if (min_angle < 90) line[i] += -min_angle; + else line[i] += 180 - min_angle; + } + //for (int i = 0; i < 4; i++) std::cout << " " << line[i] << std::endl; + feret.update(line); + //std::cout << feret << std::endl; + _count++; + } + return feret; + } +} + +namespace slice { + // Export the contour to SVG + bool write_svg(std::string filename, const Polygons &C, const int width, const int height, const int min_x, const int min_y, bool show_convexhull = false) { + if (C.empty()) return false; + PolygonSides p = contour_inside_test(C); + assert(p.size() == C.size()); + std::ofstream svg(filename, std::ofstream::out); + svg << "" << std::endl; + svg << "" << std::endl; + svg << "" << std::endl; + for (Polygons::const_iterator t = C.begin(); t != C.end(); t++) { + if (t->size() < 2) continue; + size_t index = (size_t)(t - C.begin()); + std:string color = "eee"; + if (p[index] == PolygonSide::INSIDE) color = "f00"; + else if (p[index] == PolygonSide::OUTSIDE) color = "000"; + else if (p[index] == PolygonSide::PAIRED) color = "00f"; + // Write origin polygon + Polygon::const_iterator c = t->begin(); + svg << "x << "," << c->y << " L"; + c++; + for (; c != t->end(); c++) { + svg << c->x << "," << c->y << " "; + } + svg << "z\" fill=\"transparent\" stroke=\"#" << color << "\" stroke-width=\"0.1\" stroke-linejoin=\"round\"/>" << std::endl; + // Write a convex-hull polygon + if (show_convexhull) { + Polygon convex = convexhull(*t); + if (convex.size() >= 3) { + c = convex.begin(); + svg << "x << "," << c->y << " L"; + c++; + for (; c != convex.end(); c++) { + svg << c->x << "," << c->y << " "; + } + svg << "z\" stroke-dasharray=\"1,1\" fill=\"transparent\" stroke=\"#d33\" stroke-width=\"0.1\" stroke-linejoin=\"round\"/>" << std::endl; + } + } + } + svg << "" << std::endl; + svg.close(); + return true; + } + + // Measure feret diameter + void measure_feret_and_shape(const slice::ContourSlice& CS, std::vector& minFeret, std::vector& maxFeret, std::vector (&shapes)[5]) { + // Foreach slice in 3D +#ifndef USE_PARALLEL + for (size_t cs_index = 0, cs_size = CS.size(); cs_index < cs_size; cs_index++) { +#else + static tbb::affinity_partitioner ap; + tbb::spin_mutex feretMutex, shapeMutex; + tbb::parallel_for(tbb::blocked_range(0, CS.size()), [&](tbb::blocked_range r) { + for (size_t cs_index = r.begin(); cs_index < r.end(); cs_index++) { +#endif + if (CS[cs_index].empty()) continue; + // Inside or outside testing + std::vector> pairPolygons; + slice::PolygonSides p = contour_inside_test(CS[cs_index], pairPolygons); + + assert(p.size() == CS[cs_index].size()); + // For each contour in slice, calculate min max of contour + std::vector< Point2d > polygonCenters; + std::vector< std::pair > centerPolygonMap; + size_t n_outside = 0, n_inside = 0; + for (slice::Polygons::const_iterator c = CS[cs_index].begin(); c != CS[cs_index].end(); c++) { + // the polygon must have more-than 2 lines + if (c->size() <= 2) { + polygonCenters.push_back({ 0, 0 }); + continue; + } + size_t index_polygon = (size_t)(c - CS[cs_index].begin()); + + // Loop for each point to find max min in polygon + slice::Polygon::const_iterator l = c->begin(); + double _x = 0, _y = 0; + for (l++; l != c->end(); l++) { + _x += l->x; + _y += l->y; + } + _x /= c->size(); + _y /= c->size(); + polygonCenters.push_back({ _x, _y }); + + if (p[index_polygon] == slice::PolygonSide::OUTSIDE) { + // the polygon is the boundary of solid; + // count the outside polygon + n_outside++; + centerPolygonMap.push_back(std::make_pair(Point2d{ _x, _y }, index_polygon)); + } + // 1. Rotating caliber for INSIDE polygon + else if (p[index_polygon] == slice::PolygonSide::INSIDE) { + n_inside++; + double area = measure_polygon_area(*c); + FeretDiameter feret = measure_polygon_feret_diameter(*c); + if (!feret.empty && feret.min > 0) { + double w = feret.min, h = feret.perpendicularMax; + assert(w > 0 && h > 0 && area > 0); + // calculate Podczeck's shape description + // from: https://diplib.github.io/diplib-docs/features.html#shape_features_PodczeckShapes + // the polygon is the boundary of a hole; push the feret diameter to the result + { +#ifdef USE_PARALLEL + tbb::spin_mutex::scoped_lock lock(feretMutex); +#endif + minFeret.push_back(feret.min); + maxFeret.push_back(feret.perpendicularMax); + } + { +#ifdef USE_PARALLEL + tbb::spin_mutex::scoped_lock lock(shapeMutex); +#endif + shapes[0].push_back(area / (w * h)); + shapes[1].push_back(4 * area / (M_PI * h * h)); + shapes[2].push_back(2 * area / (w * h)); + shapes[3].push_back(4 * area / (M_PI * w * h)); + shapes[4].push_back(feret.perimeter / feret.max); + } + } + } + } + + // 2. GJK distance for OUTSIDE polygon + if (n_outside > 1 && centerPolygonMap.size() > 1) { + // For each Polygon in slice layer, find 4 minimum-distance polygon + for (slice::Polygons::const_iterator c = CS[cs_index].begin(); c != CS[cs_index].end(); c++) { + if (c->size() <= 2) continue; + size_t index = (size_t)(c - CS[cs_index].begin()); + std::vector feret; + double _feret; + std::vector pairs; + int k = std::min(centerPolygonMap.size() - 1, (size_t)4); + if (p[index] == slice::PolygonSide::OUTSIDE) { + size_t i = 0; + // Take the first k in centerPolygonMap for initializing the MaxHeap + for (size_t j = 0; j < k; i++) { + if (i != index && i < centerPolygonMap.size()) { + double distance = measure_point_distance(polygonCenters[index], centerPolygonMap[i].first); + pairs.push_back(std::make_pair(distance, centerPolygonMap[i].second)); + j++; + } + } + MaxHeap minimum(k, &pairs); + // Process the other centerPolygonMap + for (; i < centerPolygonMap.size(); i++) { + double distance = measure_point_distance(polygonCenters[index], centerPolygonMap[i].first); + minimum.update(std::make_pair(distance, centerPolygonMap[i].second)); + } + // Use the k closest centerPolygonMap for calculating the pore size + for (i = 0; i < k; i++) { + size_t next_index = pairs[i].second; + if (next_index < CS[cs_index].size()) { + _feret = gjk_minimal_distance(*c, CS[cs_index].at(next_index)); + if (_feret > 0) feret.push_back(_feret); + } + } + } + if (feret.size() > 0) { + std::sort(feret.begin(), feret.end()); +#ifdef USE_PARALLEL + tbb::spin_mutex::scoped_lock lock(feretMutex); +#endif + minFeret.push_back(feret.front()); + maxFeret.push_back(feret.back()); + } + } + } + + // 3. GJK distance for PAIRED polygon + for (auto it = pairPolygons.begin(); it != pairPolygons.end(); ++it) { + double feret = gjk_minimal_distance(CS[cs_index].at(it->first), CS[cs_index].at(it->second)); +#ifdef USE_PARALLEL + tbb::spin_mutex::scoped_lock lock(feretMutex); +#endif + minFeret.push_back(feret); + maxFeret.push_back(feret); + } + } +#ifdef USE_PARALLEL + }, ap); +#endif + } +} \ No newline at end of file diff --git a/include/ProgressBar.hpp b/include/ProgressBar.hpp new file mode 100644 index 0000000..2e4d9a6 --- /dev/null +++ b/include/ProgressBar.hpp @@ -0,0 +1,63 @@ +#ifndef PROGRESSBAR_PROGRESSBAR_HPP +#define PROGRESSBAR_PROGRESSBAR_HPP + +#include +#include + +class ProgressBar { +private: + bool is_done = false; + unsigned int ticks = 0; + + const unsigned int total_ticks; + const unsigned int bar_width; + const char complete_char = '='; + const char incomplete_char = ' '; + std::chrono::steady_clock::time_point start_time = std::chrono::steady_clock::now(); + +public: + ProgressBar(unsigned int total, unsigned int width, char complete, char incomplete) : + total_ticks{ total }, bar_width{ width }, complete_char{ complete }, incomplete_char{ incomplete } {} + + ProgressBar(unsigned int total, unsigned int width) : total_ticks{ total }, bar_width{ width } {} + + unsigned int operator++() { return ++ticks; } + ProgressBar& operator+=(const unsigned int tick) { ticks += tick; return *this; } + + void update(const unsigned int tick) { ticks = tick; } + void reset() { + start_time = std::chrono::steady_clock::now(); + ticks = 0; + is_done = false; + } + + void display() const + { + float progress = (float)ticks / total_ticks; + int pos = (int)(bar_width * progress); + + std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now(); + auto time_elapsed = std::chrono::duration_cast(now - start_time).count(); + + std::cout << "["; + + for (int i = 0; i < bar_width; ++i) { + if (i < pos) std::cout << complete_char; + else if (i == pos) std::cout << ">"; + else std::cout << incomplete_char; + } + std::cout << "] " << int(progress * 100.0) << "% " + << float(time_elapsed) / 1000.0 << "s\r"; + std::cout.flush(); + } + + void done() + { + if (is_done) return; + is_done = true; + display(); + std::cout << std::endl; + } +}; + +#endif //PROGRESSBAR_PROGRESSBAR_HPP \ No newline at end of file diff --git a/include/QuadricSimplification.h b/include/QuadricSimplification.h new file mode 100644 index 0000000..046f8e4 --- /dev/null +++ b/include/QuadricSimplification.h @@ -0,0 +1,119 @@ +/**************************************************************************** + * MeshLab o o * + * A versatile mesh processing toolbox o o * + * _ O _ * + * Copyright(C) 2005 \/)\/ * + * Visual Computing Lab /\/| * + * ISTI - Italian National Research Council | * + * \ * + * All rights reserved. * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License (http://www.gnu.org/licenses/gpl.txt) * + * for more details. * + * * + ****************************************************************************/ +#pragma once +#include +#include +#include +#include "Mesh.h" + +namespace vcg { +namespace tri { + + typedef SimpleTempData > QuadricTemp; + + class QHelper + { + public: + QHelper() {} + static void Init() {} + static math::Quadric& Qd(TVertex& v) { return TD()[v]; } + static math::Quadric& Qd(TVertex* v) { return TD()[*v]; } + static TVertex::ScalarType W(TVertex*) { return 1.0; } + static TVertex::ScalarType W(TVertex&) { return 1.0; } + static void Merge(TVertex&, TVertex const&) {} + static QuadricTemp*& TDp() { static QuadricTemp* td; return td; } + static QuadricTemp& TD() { return *TDp(); } + }; + + typedef BasicVertexPair VertexPair; + + class MyTriEdgeCollapse : public vcg::tri::TriEdgeCollapseQuadric< TMesh, VertexPair, MyTriEdgeCollapse, QHelper > { + public: + typedef vcg::tri::TriEdgeCollapseQuadric< TMesh, VertexPair, MyTriEdgeCollapse, QHelper> TECQ; + inline MyTriEdgeCollapse(const VertexPair& p, int i, BaseParameterClass* pp) :TECQ(p, i, pp) {} + }; + + void QuadricSimplification(TMesh& m, int TargetFaceNum, vcg::tri::TriEdgeCollapseQuadricParameter& pp, vcg::CallBackPos* cb) { + vcg::math::Quadric QZero; + QZero.SetZero(); + vcg::tri::QuadricTemp TD(m.vert, QZero); + vcg::tri::QHelper::TDp() = &TD; + + if (pp.NormalCheck) pp.NormalThrRad = M_PI / 4.0; + + vcg::LocalOptimization DeciSession(m, &pp); + cb(1, "Initializing simplification"); + DeciSession.Init(); + + DeciSession.SetTargetSimplices(TargetFaceNum); + DeciSession.SetTimeBudget(0.1f); // this allows updating the progress bar 10 time for sec... + // if(TargetError< numeric_limits::max() ) DeciSession.SetTargetMetric(TargetError); + //int startFn=m.fn; + int faceToDel = m.fn - TargetFaceNum; + cb(2, "Are you ready... 3 2 1"); + while (DeciSession.DoOptimization() && m.fn > TargetFaceNum) + { + cb(100 - (98 * (m.fn - TargetFaceNum) / (faceToDel)), "Simplifying..."); + }; + + DeciSession.Finalize(); + + vcg::tri::QHelper::TDp() = nullptr; + } + + void mesh_quad_simplification(TMesh& mesh, double percent, vcg::CallBackPos* cb) { + if (percent >= mesh.FN()) return; + mesh.vert.EnableVFAdjacency(); + mesh.face.EnableQuality(); + mesh.face.EnableVFAdjacency(); + //vcg::tri::UpdateNormal::PerFace(mesh); + vcg::tri::UpdateTopology::VertexFace(mesh); + vcg::tri::UpdateFlags::FaceBorderFromVF(mesh); + size_t TargetFaceNum = (percent > 0 && percent < 1) ? mesh.FN() * percent : percent; + vcg::tri::TriEdgeCollapseQuadricParameter pp; + pp.QualityThr = 0.3; + pp.FastPreserveBoundary = true; + pp.PreserveBoundary = false; + pp.BoundaryQuadricWeight = 1.25; + pp.PreserveTopology = true; + pp.NormalCheck = false; + pp.OptimalPlacement = true; + pp.QualityWeight = false; + pp.QualityQuadric = true; + pp.QualityQuadricWeight = 0.001f; + + vcg::tri::QuadricSimplification(mesh, TargetFaceNum, pp, cb); + + if (true) + { + vcg::tri::Clean::RemoveFaceOutOfRangeArea(mesh, 0); + vcg::tri::Clean::RemoveDuplicateVertex(mesh); + vcg::tri::Clean::RemoveUnreferencedVertex(mesh); + vcg::tri::Allocator::CompactVertexVector(mesh); + vcg::tri::Allocator::CompactFaceVector(mesh); + } + mesh.vert.DisableVFAdjacency(); + mesh.face.DisableQuality(); + mesh.face.DisableVFAdjacency(); + } +} // end namespace tri +} // end namespace vcg diff --git a/include/Scaffolder_2.h b/include/Scaffolder_2.h new file mode 100644 index 0000000..61c6697 --- /dev/null +++ b/include/Scaffolder_2.h @@ -0,0 +1,148 @@ +#pragma once +#include +#include +#include +#include + +#include +#include + +#include "cxxopts.hpp" +#include "diplib.h" +#include "diplib/file_io.h" +#include "diplib/regions.h" +#include "diplib/measurement.h" +#include "dualmc/dualmc.h" + +#include "ProgressBar.hpp" +#include "OptimalSlice.hpp" +#include "implicit_function.h" +#include "MeshOperation.h" +#include "utils.h" +//#include "QuadricSimplification.h" + +#define VERSION "v1.3" +#define PROGRESS_BAR_COLUMN 40 + +#define METHOD_IMAGE_PROCESS 0 +#define METHOD_SLICE_CONTOUR 1 + +typedef struct index_type { + size_t x; size_t y; size_t z; +} index_type; +typedef std::map Queue_t; + +// Flatten between 1D and 3D +// https://stackoverflow.com/questions/7367770/how-to-flatten-or-index-3d-array-in-1d-array +inline size_t indexFromIJK(size_t i, size_t j, size_t k, Eigen::RowVector3i grid_size) { + return i + grid_size(0) * (j + grid_size(1) * k); +} + +inline void indexToIJK(size_t index, Eigen::RowVector3i grid_size, index_type& r) { + r.z = index / (grid_size(0) * grid_size(1)); + index -= r.z * grid_size(0) * grid_size(1); + r.y = index / grid_size(0); + r.x = index % grid_size(0); +} + +inline bool MarkAndSweepNeighbor(Eigen::VectorXd& W, index_type& index, Queue_t& queue, Eigen::RowVector3i grid_size, double value = 0.0, bool findAbove = false) { + bool isBorder = false; + for (int8_t di = -1; di <= 1; di++) { + for (int8_t dj = -1; dj <= 1; dj++) { + for (int8_t dk = -1; dk <= 1; dk++) { + if (di == 0 && dj == 0 && dk == 0) continue; + const size_t id = indexFromIJK(index.x + di, index.y + dj, index.z + dk, grid_size); + //std::cout << value << " " << W(id) << std::endl; + if ((findAbove && W(id) >= value - eps2) || (!findAbove && W(id) <= value + eps2)) { + isBorder = true; + break; + } + } + if (isBorder) break; + } + if (isBorder) break; + } + if (isBorder) { + for (int8_t di = -1; di <= 1; di++) { + for (int8_t dj = -1; dj <= 1; dj++) { + for (int8_t dk = -1; dk <= 1; dk++) { + if (di == 0 && dj == 0 && dk == 0) continue; + const size_t id = indexFromIJK(index.x + di, index.y + dj, index.z + dk, grid_size); + if (W(id) >= 0.5 && W(id) < 1.1) { + queue.insert({ id, true }); + } + } + } + } + } + return isBorder; +} + +inline void marching_cube(TMesh &mesh, Eigen::MatrixXd &Fxyz, Eigen::RowVector3i grid_size, Eigen::RowVector3d &Vmin, double delta, bool verbose = true, bool dirty = false) { + { + if (verbose) std::cout << "[Marching Cube] " << std::endl; + dualmc::DualMC builder; + std::vector mc_vertices; + std::vector mc_quads; + builder.build((double const*)Fxyz.data(), grid_size(0), grid_size(1), grid_size(2), 0, true, false, mc_vertices, mc_quads); + TMesh::VertexIterator vi = vcg::tri::Allocator::AddVertices(mesh, mc_vertices.size()); + TMesh::FaceIterator fi = vcg::tri::Allocator::AddFaces(mesh, mc_quads.size() * 2); + std::vector vp(mc_vertices.size()); + for (size_t i = 0, len = mc_vertices.size(); i < len; i++, ++vi) { + vp[i] = &(*vi); + vi->P() = TMesh::CoordType( + Vmin(0) + mc_vertices[i].x * delta, + Vmin(1) + mc_vertices[i].y * delta, + Vmin(2) + mc_vertices[i].z * delta + ); + } + for (size_t i = 0, len = mc_quads.size(); i < len; i++, ++fi) { + fi->V(0) = vp[mc_quads[i].i0]; + fi->V(1) = vp[mc_quads[i].i1]; + fi->V(2) = vp[mc_quads[i].i2]; + ++fi; + fi->V(0) = vp[mc_quads[i].i2]; + fi->V(1) = vp[mc_quads[i].i3]; + fi->V(2) = vp[mc_quads[i].i0]; + } + if (!dirty) { + vcg::tri::Clean::RemoveDuplicateFace(mesh); + vcg::tri::Clean::RemoveDuplicateVertex(mesh); + vcg::tri::Clean::RemoveUnreferencedVertex(mesh); + } + } +} + +inline void mesh_to_eigen_vector(TMesh& mesh, Eigen::MatrixXd& V, Eigen::MatrixXi& F) { + V.resize(mesh.VN(), 3); + size_t i = 0; + std::vector vertexId(mesh.vert.size()); + for (TMesh::VertexIterator it = mesh.vert.begin(); it != mesh.vert.end(); ++it) if (!it->IsD()) { + vertexId[it - mesh.vert.begin()] = i; + vcg::Point3d point = it->P(); + V(i, 0) = point[0]; + V(i, 1) = point[1]; + V(i, 2) = point[2]; + i++; + } + // Faces to Eigen matrixXi F1 + i = 0; + F.resize(mesh.FN(), mesh.face.begin()->VN()); + for (TMesh::FaceIterator it = mesh.face.begin(); it != mesh.face.end(); ++it) if (!it->IsD()) { + for (int k = 0; k < it->VN(); k++) { + F(i, k) = vertexId[vcg::tri::Index(mesh, it->V(k))]; + } + i++; + } +} + +ProgressBar qsim_progress(100, 40); +bool qsim_callback(int pos, const char* str) { + if (pos >= 0 && pos <= 100) { + qsim_progress.update(pos); + qsim_progress.display(); + } + if (pos >= 100) + qsim_progress.done(); + return true; +} \ No newline at end of file diff --git a/include/cxxopts.hpp b/include/cxxopts.hpp new file mode 100644 index 0000000..4c21c1b --- /dev/null +++ b/include/cxxopts.hpp @@ -0,0 +1,2216 @@ +/* + +Copyright (c) 2014, 2015, 2016, 2017 Jarryd Beck + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +*/ + +#ifndef CXXOPTS_HPP_INCLUDED +#define CXXOPTS_HPP_INCLUDED + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef __cpp_lib_optional +#include +#define CXXOPTS_HAS_OPTIONAL +#endif + +#ifndef CXXOPTS_VECTOR_DELIMITER +#define CXXOPTS_VECTOR_DELIMITER ',' +#endif + +#define CXXOPTS__VERSION_MAJOR 2 +#define CXXOPTS__VERSION_MINOR 2 +#define CXXOPTS__VERSION_PATCH 0 + +namespace cxxopts +{ + static constexpr struct { + uint8_t major, minor, patch; + } version = { + CXXOPTS__VERSION_MAJOR, + CXXOPTS__VERSION_MINOR, + CXXOPTS__VERSION_PATCH + }; +} + +//when we ask cxxopts to use Unicode, help strings are processed using ICU, +//which results in the correct lengths being computed for strings when they +//are formatted for the help output +//it is necessary to make sure that can be found by the +//compiler, and that icu-uc is linked in to the binary. + +#ifdef CXXOPTS_USE_UNICODE +#include + +namespace cxxopts +{ + typedef icu::UnicodeString String; + + inline + String + toLocalString(std::string s) + { + return icu::UnicodeString::fromUTF8(std::move(s)); + } + + class UnicodeStringIterator : public + std::iterator + { + public: + + UnicodeStringIterator(const icu::UnicodeString* string, int32_t pos) + : s(string) + , i(pos) + { + } + + value_type + operator*() const + { + return s->char32At(i); + } + + bool + operator==(const UnicodeStringIterator& rhs) const + { + return s == rhs.s && i == rhs.i; + } + + bool + operator!=(const UnicodeStringIterator& rhs) const + { + return !(*this == rhs); + } + + UnicodeStringIterator& + operator++() + { + ++i; + return *this; + } + + UnicodeStringIterator + operator+(int32_t v) + { + return UnicodeStringIterator(s, i + v); + } + + private: + const icu::UnicodeString* s; + int32_t i; + }; + + inline + String& + stringAppend(String& s, String a) + { + return s.append(std::move(a)); + } + + inline + String& + stringAppend(String& s, int n, UChar32 c) + { + for (int i = 0; i != n; ++i) + { + s.append(c); + } + + return s; + } + + template + String& + stringAppend(String& s, Iterator begin, Iterator end) + { + while (begin != end) + { + s.append(*begin); + ++begin; + } + + return s; + } + + inline + size_t + stringLength(const String& s) + { + return s.length(); + } + + inline + std::string + toUTF8String(const String& s) + { + std::string result; + s.toUTF8String(result); + + return result; + } + + inline + bool + empty(const String& s) + { + return s.isEmpty(); + } +} + +namespace std +{ + inline + cxxopts::UnicodeStringIterator + begin(const icu::UnicodeString& s) + { + return cxxopts::UnicodeStringIterator(&s, 0); + } + + inline + cxxopts::UnicodeStringIterator + end(const icu::UnicodeString& s) + { + return cxxopts::UnicodeStringIterator(&s, s.length()); + } +} + +//ifdef CXXOPTS_USE_UNICODE +#else + +namespace cxxopts +{ + typedef std::string String; + + template + T + toLocalString(T&& t) + { + return std::forward(t); + } + + inline + size_t + stringLength(const String& s) + { + return s.length(); + } + + inline + String& + stringAppend(String& s, String a) + { + return s.append(std::move(a)); + } + + inline + String& + stringAppend(String& s, size_t n, char c) + { + return s.append(n, c); + } + + template + String& + stringAppend(String& s, Iterator begin, Iterator end) + { + return s.append(begin, end); + } + + template + std::string + toUTF8String(T&& t) + { + return std::forward(t); + } + + inline + bool + empty(const std::string& s) + { + return s.empty(); + } +} + +//ifdef CXXOPTS_USE_UNICODE +#endif + +namespace cxxopts +{ + namespace + { +#ifdef _WIN32 + const std::string LQUOTE("\'"); + const std::string RQUOTE("\'"); +#else + const std::string LQUOTE("‘"); + const std::string RQUOTE("’"); +#endif + } + + class Value : public std::enable_shared_from_this + { + public: + + virtual ~Value() = default; + + virtual + std::shared_ptr + clone() const = 0; + + virtual void + parse(const std::string& text) const = 0; + + virtual void + parse() const = 0; + + virtual bool + has_default() const = 0; + + virtual bool + is_container() const = 0; + + virtual bool + has_implicit() const = 0; + + virtual std::string + get_default_value() const = 0; + + virtual std::string + get_implicit_value() const = 0; + + virtual std::shared_ptr + default_value(const std::string& value) = 0; + + virtual std::shared_ptr + implicit_value(const std::string& value) = 0; + + virtual std::shared_ptr + no_implicit_value() = 0; + + virtual bool + is_boolean() const = 0; + }; + + class OptionException : public std::exception + { + public: + OptionException(const std::string& message) + : m_message(message) + { + } + + virtual const char* + what() const noexcept + { + return m_message.c_str(); + } + + private: + std::string m_message; + }; + + class OptionSpecException : public OptionException + { + public: + + OptionSpecException(const std::string& message) + : OptionException(message) + { + } + }; + + class OptionParseException : public OptionException + { + public: + OptionParseException(const std::string& message) + : OptionException(message) + { + } + }; + + class option_exists_error : public OptionSpecException + { + public: + option_exists_error(const std::string& option) + : OptionSpecException("Option " + LQUOTE + option + RQUOTE + " already exists") + { + } + }; + + class invalid_option_format_error : public OptionSpecException + { + public: + invalid_option_format_error(const std::string& format) + : OptionSpecException("Invalid option format " + LQUOTE + format + RQUOTE) + { + } + }; + + class option_syntax_exception : public OptionParseException { + public: + option_syntax_exception(const std::string& text) + : OptionParseException("Argument " + LQUOTE + text + RQUOTE + + " starts with a - but has incorrect syntax") + { + } + }; + + class option_not_exists_exception : public OptionParseException + { + public: + option_not_exists_exception(const std::string& option) + : OptionParseException("Option " + LQUOTE + option + RQUOTE + " does not exist") + { + } + }; + + class missing_argument_exception : public OptionParseException + { + public: + missing_argument_exception(const std::string& option) + : OptionParseException( + "Option " + LQUOTE + option + RQUOTE + " is missing an argument" + ) + { + } + }; + + class option_requires_argument_exception : public OptionParseException + { + public: + option_requires_argument_exception(const std::string& option) + : OptionParseException( + "Option " + LQUOTE + option + RQUOTE + " requires an argument" + ) + { + } + }; + + class option_not_has_argument_exception : public OptionParseException + { + public: + option_not_has_argument_exception + ( + const std::string& option, + const std::string& arg + ) + : OptionParseException( + "Option " + LQUOTE + option + RQUOTE + + " does not take an argument, but argument " + + LQUOTE + arg + RQUOTE + " given" + ) + { + } + }; + + class option_not_present_exception : public OptionParseException + { + public: + option_not_present_exception(const std::string& option) + : OptionParseException("Option " + LQUOTE + option + RQUOTE + " not present") + { + } + }; + + class argument_incorrect_type : public OptionParseException + { + public: + argument_incorrect_type + ( + const std::string& arg + ) + : OptionParseException( + "Argument " + LQUOTE + arg + RQUOTE + " failed to parse" + ) + { + } + }; + + class option_required_exception : public OptionParseException + { + public: + option_required_exception(const std::string& option) + : OptionParseException( + "Option " + LQUOTE + option + RQUOTE + " is required but not present" + ) + { + } + }; + + template + void throw_or_mimic(const std::string& text) + { + static_assert(std::is_base_of::value, + "throw_or_mimic only works on std::exception and " + "deriving classes"); + +#ifndef CXXOPTS_NO_EXCEPTIONS + // If CXXOPTS_NO_EXCEPTIONS is not defined, just throw + throw T{ text }; +#else + // Otherwise manually instantiate the exception, print what() to stderr, + // and abort + T exception{ text }; + std::cerr << exception.what() << std::endl; + std::cerr << "Aborting (exceptions disabled)..." << std::endl; + std::abort(); +#endif + } + + namespace values + { + namespace + { + std::basic_regex integer_pattern + ("(-)?(0x)?([0-9a-zA-Z]+)|((0x)?0)"); + std::basic_regex truthy_pattern + ("(t|T)(rue)?|1"); + std::basic_regex falsy_pattern + ("(f|F)(alse)?|0"); + } + + namespace detail + { + template + struct SignedCheck; + + template + struct SignedCheck + { + template + void + operator()(bool negative, U u, const std::string& text) + { + if (negative) + { + if (u > static_cast((std::numeric_limits::min)())) + { + throw_or_mimic(text); + } + } + else + { + if (u > static_cast((std::numeric_limits::max)())) + { + throw_or_mimic(text); + } + } + } + }; + + template + struct SignedCheck + { + template + void + operator()(bool, U, const std::string&) {} + }; + + template + void + check_signed_range(bool negative, U value, const std::string& text) + { + SignedCheck::is_signed>()(negative, value, text); + } + } + + template + R + checked_negate(T&& t, const std::string&, std::true_type) + { + // if we got to here, then `t` is a positive number that fits into + // `R`. So to avoid MSVC C4146, we first cast it to `R`. + // See https://github.com/jarro2783/cxxopts/issues/62 for more details. + return static_cast(-static_cast(t - 1) - 1); + } + + template + T + checked_negate(T&& t, const std::string& text, std::false_type) + { + throw_or_mimic(text); + return t; + } + + template + void + integer_parser(const std::string& text, T& value) + { + std::smatch match; + std::regex_match(text, match, integer_pattern); + + if (match.length() == 0) + { + throw_or_mimic(text); + } + + if (match.length(4) > 0) + { + value = 0; + return; + } + + using US = typename std::make_unsigned::type; + + constexpr bool is_signed = std::numeric_limits::is_signed; + const bool negative = match.length(1) > 0; + const uint8_t base = match.length(2) > 0 ? 16 : 10; + + auto value_match = match[3]; + + US result = 0; + + for (auto iter = value_match.first; iter != value_match.second; ++iter) + { + US digit = 0; + + if (*iter >= '0' && *iter <= '9') + { + digit = static_cast(*iter - '0'); + } + else if (base == 16 && *iter >= 'a' && *iter <= 'f') + { + digit = static_cast(*iter - 'a' + 10); + } + else if (base == 16 && *iter >= 'A' && *iter <= 'F') + { + digit = static_cast(*iter - 'A' + 10); + } + else + { + throw_or_mimic(text); + } + + const US next = static_cast(result * base + digit); + if (result > next) + { + throw_or_mimic(text); + } + + result = next; + } + + detail::check_signed_range(negative, result, text); + + if (negative) + { + value = checked_negate(result, + text, + std::integral_constant()); + } + else + { + value = static_cast(result); + } + } + + template + void stringstream_parser(const std::string& text, T& value) + { + std::stringstream in(text); + in >> value; + if (!in) { + throw_or_mimic(text); + } + } + + inline + void + parse_value(const std::string& text, uint8_t& value) + { + integer_parser(text, value); + } + + inline + void + parse_value(const std::string& text, int8_t& value) + { + integer_parser(text, value); + } + + inline + void + parse_value(const std::string& text, uint16_t& value) + { + integer_parser(text, value); + } + + inline + void + parse_value(const std::string& text, int16_t& value) + { + integer_parser(text, value); + } + + inline + void + parse_value(const std::string& text, uint32_t& value) + { + integer_parser(text, value); + } + + inline + void + parse_value(const std::string& text, int32_t& value) + { + integer_parser(text, value); + } + + inline + void + parse_value(const std::string& text, uint64_t& value) + { + integer_parser(text, value); + } + + inline + void + parse_value(const std::string& text, int64_t& value) + { + integer_parser(text, value); + } + + inline + void + parse_value(const std::string& text, bool& value) + { + std::smatch result; + std::regex_match(text, result, truthy_pattern); + + if (!result.empty()) + { + value = true; + return; + } + + std::regex_match(text, result, falsy_pattern); + if (!result.empty()) + { + value = false; + return; + } + + throw_or_mimic(text); + } + + inline + void + parse_value(const std::string& text, std::string& value) + { + value = text; + } + + // The fallback parser. It uses the stringstream parser to parse all types + // that have not been overloaded explicitly. It has to be placed in the + // source code before all other more specialized templates. + template + void + parse_value(const std::string& text, T& value) { + stringstream_parser(text, value); + } + + template + void + parse_value(const std::string& text, std::vector& value) + { + std::stringstream in(text); + std::string token; + while (in.eof() == false && std::getline(in, token, CXXOPTS_VECTOR_DELIMITER)) { + T v; + parse_value(token, v); + value.emplace_back(std::move(v)); + } + } + +#ifdef CXXOPTS_HAS_OPTIONAL + template + void + parse_value(const std::string& text, std::optional& value) + { + T result; + parse_value(text, result); + value = std::move(result); + } +#endif + + inline + void parse_value(const std::string& text, char& c) + { + if (text.length() != 1) + { + throw_or_mimic(text); + } + + c = text[0]; + } + + template + struct type_is_container + { + static constexpr bool value = false; + }; + + template + struct type_is_container> + { + static constexpr bool value = true; + }; + + template + class abstract_value : public Value + { + using Self = abstract_value; + + public: + abstract_value() + : m_result(std::make_shared()) + , m_store(m_result.get()) + { + } + + abstract_value(T* t) + : m_store(t) + { + } + + virtual ~abstract_value() = default; + + abstract_value(const abstract_value& rhs) + { + if (rhs.m_result) + { + m_result = std::make_shared(); + m_store = m_result.get(); + } + else + { + m_store = rhs.m_store; + } + + m_default = rhs.m_default; + m_implicit = rhs.m_implicit; + m_default_value = rhs.m_default_value; + m_implicit_value = rhs.m_implicit_value; + } + + void + parse(const std::string& text) const + { + parse_value(text, *m_store); + } + + bool + is_container() const + { + return type_is_container::value; + } + + void + parse() const + { + parse_value(m_default_value, *m_store); + } + + bool + has_default() const + { + return m_default; + } + + bool + has_implicit() const + { + return m_implicit; + } + + std::shared_ptr + default_value(const std::string& value) + { + m_default = true; + m_default_value = value; + return shared_from_this(); + } + + std::shared_ptr + implicit_value(const std::string& value) + { + m_implicit = true; + m_implicit_value = value; + return shared_from_this(); + } + + std::shared_ptr + no_implicit_value() + { + m_implicit = false; + return shared_from_this(); + } + + std::string + get_default_value() const + { + return m_default_value; + } + + std::string + get_implicit_value() const + { + return m_implicit_value; + } + + bool + is_boolean() const + { + return std::is_same::value; + } + + const T& + get() const + { + if (m_store == nullptr) + { + return *m_result; + } + else + { + return *m_store; + } + } + + protected: + std::shared_ptr m_result; + T* m_store; + + bool m_default = false; + bool m_implicit = false; + + std::string m_default_value; + std::string m_implicit_value; + }; + + template + class standard_value : public abstract_value + { + public: + using abstract_value::abstract_value; + + std::shared_ptr + clone() const + { + return std::make_shared>(*this); + } + }; + + template <> + class standard_value : public abstract_value + { + public: + ~standard_value() = default; + + standard_value() + { + set_default_and_implicit(); + } + + standard_value(bool* b) + : abstract_value(b) + { + set_default_and_implicit(); + } + + std::shared_ptr + clone() const + { + return std::make_shared>(*this); + } + + private: + + void + set_default_and_implicit() + { + m_default = true; + m_default_value = "false"; + m_implicit = true; + m_implicit_value = "true"; + } + }; + } + + template + std::shared_ptr + value() + { + return std::make_shared>(); + } + + template + std::shared_ptr + value(T& t) + { + return std::make_shared>(&t); + } + + class OptionAdder; + + class OptionDetails + { + public: + OptionDetails + ( + const std::string& short_, + const std::string& long_, + const String& desc, + std::shared_ptr val + ) + : m_short(short_) + , m_long(long_) + , m_desc(desc) + , m_value(val) + , m_count(0) + { + } + + OptionDetails(const OptionDetails& rhs) + : m_desc(rhs.m_desc) + , m_count(rhs.m_count) + { + m_value = rhs.m_value->clone(); + } + + OptionDetails(OptionDetails&& rhs) = default; + + const String& + description() const + { + return m_desc; + } + + const Value& value() const { + return *m_value; + } + + std::shared_ptr + make_storage() const + { + return m_value->clone(); + } + + const std::string& + short_name() const + { + return m_short; + } + + const std::string& + long_name() const + { + return m_long; + } + + private: + std::string m_short; + std::string m_long; + String m_desc; + std::shared_ptr m_value; + int m_count; + }; + + struct HelpOptionDetails + { + std::string s; + std::string l; + String desc; + bool has_default; + std::string default_value; + bool has_implicit; + std::string implicit_value; + std::string arg_help; + bool is_container; + bool is_boolean; + }; + + struct HelpGroupDetails + { + std::string name; + std::string description; + std::vector options; + }; + + class OptionValue + { + public: + void + parse + ( + std::shared_ptr details, + const std::string& text + ) + { + ensure_value(details); + ++m_count; + m_value->parse(text); + } + + void + parse_default(std::shared_ptr details) + { + ensure_value(details); + m_default = true; + m_value->parse(); + } + + size_t + count() const noexcept + { + return m_count; + } + + // TODO: maybe default options should count towards the number of arguments + bool + has_default() const noexcept + { + return m_default; + } + + template + const T& + as() const + { + if (m_value == nullptr) { + throw_or_mimic("No value"); + } + +#ifdef CXXOPTS_NO_RTTI + return static_cast&>(*m_value).get(); +#else + return dynamic_cast&>(*m_value).get(); +#endif + } + + private: + void + ensure_value(std::shared_ptr details) + { + if (m_value == nullptr) + { + m_value = details->make_storage(); + } + } + + std::shared_ptr m_value; + size_t m_count = 0; + bool m_default = false; + }; + + class KeyValue + { + public: + KeyValue(std::string key_, std::string value_) + : m_key(std::move(key_)) + , m_value(std::move(value_)) + { + } + + const + std::string& + key() const + { + return m_key; + } + + const + std::string& + value() const + { + return m_value; + } + + template + T + as() const + { + T result; + values::parse_value(m_value, result); + return result; + } + + private: + std::string m_key; + std::string m_value; + }; + + class ParseResult + { + public: + + ParseResult( + const std::shared_ptr< + std::unordered_map> + >, + std::vector, + bool allow_unrecognised, + int&, char**&); + + size_t + count(const std::string& o) const + { + auto iter = m_options->find(o); + if (iter == m_options->end()) + { + return 0; + } + + auto riter = m_results.find(iter->second); + + return riter->second.count(); + } + + const OptionValue& + operator[](const std::string& option) const + { + auto iter = m_options->find(option); + + if (iter == m_options->end()) + { + throw_or_mimic(option); + } + + auto riter = m_results.find(iter->second); + + return riter->second; + } + + const std::vector& + arguments() const + { + return m_sequential; + } + + private: + + void + parse(int& argc, char**& argv); + + void + add_to_option(const std::string& option, const std::string& arg); + + bool + consume_positional(std::string a); + + void + parse_option + ( + std::shared_ptr value, + const std::string& name, + const std::string& arg = "" + ); + + void + parse_default(std::shared_ptr details); + + void + checked_parse_arg + ( + int argc, + char* argv[], + int& current, + std::shared_ptr value, + const std::string& name + ); + + const std::shared_ptr< + std::unordered_map> + > m_options; + std::vector m_positional; + std::vector::iterator m_next_positional; + std::unordered_set m_positional_set; + std::unordered_map, OptionValue> m_results; + + bool m_allow_unrecognised; + + std::vector m_sequential; + }; + + struct Option + { + Option + ( + const std::string& opts, + const std::string& desc, + const std::shared_ptr& value = ::cxxopts::value(), + const std::string& arg_help = "" + ) + : opts_(opts) + , desc_(desc) + , value_(value) + , arg_help_(arg_help) + { + } + + std::string opts_; + std::string desc_; + std::shared_ptr value_; + std::string arg_help_; + }; + + class Options + { + typedef std::unordered_map> + OptionMap; + public: + + Options(std::string program, std::string help_string = "") + : m_program(std::move(program)) + , m_help_string(toLocalString(std::move(help_string))) + , m_custom_help("[OPTION...]") + , m_positional_help("positional parameters") + , m_show_positional(false) + , m_allow_unrecognised(false) + , m_options(std::make_shared()) + , m_next_positional(m_positional.end()) + { + } + + Options& + positional_help(std::string help_text) + { + m_positional_help = std::move(help_text); + return *this; + } + + Options& + custom_help(std::string help_text) + { + m_custom_help = std::move(help_text); + return *this; + } + + Options& + show_positional_help() + { + m_show_positional = true; + return *this; + } + + Options& + allow_unrecognised_options() + { + m_allow_unrecognised = true; + return *this; + } + + ParseResult + parse(int& argc, char**& argv); + + OptionAdder + add_options(std::string group = ""); + + void + add_options + ( + const std::string& group, + std::initializer_list