diff --git a/CMakeLists.txt b/CMakeLists.txt index f31c0eac1e..bc0c808695 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -104,7 +104,7 @@ list(APPEND CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake/modules/) enable_testing () -# Enable C++11 language standard. +# Enable C++17 language standard. set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) @@ -426,6 +426,12 @@ if(SLIC3R_STATIC) set(USE_BLOSC TRUE) endif() +find_package(OpenVDB 5.0 REQUIRED COMPONENTS openvdb) +if(OpenVDB_FOUND) + slic3r_remap_configs(IlmBase::Half RelWithDebInfo Release) + slic3r_remap_configs(Blosc::blosc RelWithDebInfo Release) +endif() + set(TOP_LEVEL_PROJECT_DIR ${PROJECT_SOURCE_DIR}) function(prusaslicer_copy_dlls target) if ("${CMAKE_SIZEOF_VOID_P}" STREQUAL "8") @@ -449,8 +455,6 @@ function(prusaslicer_copy_dlls target) endfunction() -#find_package(OpenVDB 5.0 COMPONENTS openvdb) -#slic3r_remap_configs(IlmBase::Half RelWithDebInfo Release) # libslic3r, PrusaSlicer GUI and the PrusaSlicer executable. add_subdirectory(src) diff --git a/cmake/modules/CheckAtomic.cmake b/cmake/modules/CheckAtomic.cmake new file mode 100644 index 0000000000..c045e30b22 --- /dev/null +++ b/cmake/modules/CheckAtomic.cmake @@ -0,0 +1,106 @@ +# atomic builtins are required for threading support. + +INCLUDE(CheckCXXSourceCompiles) +INCLUDE(CheckLibraryExists) + +# Sometimes linking against libatomic is required for atomic ops, if +# the platform doesn't support lock-free atomics. + +function(check_working_cxx_atomics varname) + set(OLD_CMAKE_REQUIRED_FLAGS ${CMAKE_REQUIRED_FLAGS}) + set(CMAKE_REQUIRED_FLAGS "${CMAKE_REQUIRED_FLAGS} -std=c++11") + CHECK_CXX_SOURCE_COMPILES(" +#include +std::atomic x; +int main() { + return x; +} +" ${varname}) + set(CMAKE_REQUIRED_FLAGS ${OLD_CMAKE_REQUIRED_FLAGS}) +endfunction(check_working_cxx_atomics) + +function(check_working_cxx_atomics64 varname) + set(OLD_CMAKE_REQUIRED_FLAGS ${CMAKE_REQUIRED_FLAGS}) + set(CMAKE_REQUIRED_FLAGS "-std=c++11 ${CMAKE_REQUIRED_FLAGS}") + CHECK_CXX_SOURCE_COMPILES(" +#include +#include +std::atomic x (0); +int main() { + uint64_t i = x.load(std::memory_order_relaxed); + return 0; +} +" ${varname}) + set(CMAKE_REQUIRED_FLAGS ${OLD_CMAKE_REQUIRED_FLAGS}) +endfunction(check_working_cxx_atomics64) + + +# This isn't necessary on MSVC, so avoid command-line switch annoyance +# by only running on GCC-like hosts. +if (LLVM_COMPILER_IS_GCC_COMPATIBLE) + # First check if atomics work without the library. + check_working_cxx_atomics(HAVE_CXX_ATOMICS_WITHOUT_LIB) + # If not, check if the library exists, and atomics work with it. + if(NOT HAVE_CXX_ATOMICS_WITHOUT_LIB) + check_library_exists(atomic __atomic_fetch_add_4 "" HAVE_LIBATOMIC) + if( HAVE_LIBATOMIC ) + list(APPEND CMAKE_REQUIRED_LIBRARIES "atomic") + check_working_cxx_atomics(HAVE_CXX_ATOMICS_WITH_LIB) + if (NOT HAVE_CXX_ATOMICS_WITH_LIB) + message(FATAL_ERROR "Host compiler must support std::atomic!") + endif() + else() + message(FATAL_ERROR "Host compiler appears to require libatomic, but cannot find it.") + endif() + endif() +endif() + +# Check for 64 bit atomic operations. +if(MSVC) + set(HAVE_CXX_ATOMICS64_WITHOUT_LIB True) +else() + check_working_cxx_atomics64(HAVE_CXX_ATOMICS64_WITHOUT_LIB) +endif() + +# If not, check if the library exists, and atomics work with it. +if(NOT HAVE_CXX_ATOMICS64_WITHOUT_LIB) + check_library_exists(atomic __atomic_load_8 "" HAVE_CXX_LIBATOMICS64) + if(HAVE_CXX_LIBATOMICS64) + list(APPEND CMAKE_REQUIRED_LIBRARIES "atomic") + check_working_cxx_atomics64(HAVE_CXX_ATOMICS64_WITH_LIB) + if (NOT HAVE_CXX_ATOMICS64_WITH_LIB) + message(FATAL_ERROR "Host compiler must support 64-bit std::atomic!") + endif() + else() + message(FATAL_ERROR "Host compiler appears to require libatomic for 64-bit operations, but cannot find it.") + endif() +endif() + +## TODO: This define is only used for the legacy atomic operations in +## llvm's Atomic.h, which should be replaced. Other code simply +## assumes C++11 works. +CHECK_CXX_SOURCE_COMPILES(" +#ifdef _MSC_VER +#include +#endif +int main() { +#ifdef _MSC_VER + volatile LONG val = 1; + MemoryBarrier(); + InterlockedCompareExchange(&val, 0, 1); + InterlockedIncrement(&val); + InterlockedDecrement(&val); +#else + volatile unsigned long val = 1; + __sync_synchronize(); + __sync_val_compare_and_swap(&val, 1, 0); + __sync_add_and_fetch(&val, 1); + __sync_sub_and_fetch(&val, 1); +#endif + return 0; + } +" LLVM_HAS_ATOMICS) + +if( NOT LLVM_HAS_ATOMICS ) + message(STATUS "Warning: LLVM will be built thread-unsafe because atomic builtins are missing") +endif() \ No newline at end of file diff --git a/cmake/modules/FindOpenVDB.cmake b/cmake/modules/FindOpenVDB.cmake index b75bc6ed01..3e9e5f082a 100644 --- a/cmake/modules/FindOpenVDB.cmake +++ b/cmake/modules/FindOpenVDB.cmake @@ -108,6 +108,18 @@ if(POLICY CMP0074) cmake_policy(SET CMP0074 NEW) endif() +if(OpenVDB_FIND_QUIETLY) + set (_quiet "QUIET") +else() + set (_quiet "") +endif() + +if(OpenVDB_FIND_REQUIRED) + set (_required "REQUIRED") +else() + set (_required "") +endif() + # Include utility functions for version information include(${CMAKE_CURRENT_LIST_DIR}/OpenVDBUtils.cmake) @@ -146,7 +158,7 @@ set(_OPENVDB_ROOT_SEARCH_DIR "") # Additionally try and use pkconfig to find OpenVDB -find_package(PkgConfig) +find_package(PkgConfig ${_quiet} ) pkg_check_modules(PC_OpenVDB QUIET OpenVDB) # ------------------------------------------------------------------------ @@ -230,12 +242,14 @@ foreach(COMPONENT ${OpenVDB_FIND_COMPONENTS}) else() set(OpenVDB_${COMPONENT}_FOUND FALSE) endif() + + set(OpenVDB_${COMPONENT}_LIBRARY ${OpenVDB_${COMPONENT}_LIBRARY_RELEASE}) else () string(TOUPPER "${CMAKE_BUILD_TYPE}" _BUILD_TYPE) set(OpenVDB_${COMPONENT}_LIBRARY ${OpenVDB_${COMPONENT}_LIBRARY_${_BUILD_TYPE}}) - if (NOT MSVC AND NOT OpenVDB_${COMPONENT}_LIBRARY) + if (NOT OpenVDB_${COMPONENT}_LIBRARY) set(OpenVDB_${COMPONENT}_LIBRARY ${OpenVDB_${COMPONENT}_LIBRARY_RELEASE}) endif () @@ -247,6 +261,7 @@ foreach(COMPONENT ${OpenVDB_FIND_COMPONENTS}) set(OpenVDB_${COMPONENT}_FOUND FALSE) endif() endif () + endforeach() if(UNIX AND OPENVDB_USE_STATIC_LIBS) @@ -280,7 +295,7 @@ OPENVDB_ABI_VERSION_FROM_PRINT( ABI OpenVDB_ABI ) -if(NOT OpenVDB_FIND_QUIET) +if(NOT OpenVDB_FIND_QUIETLY) if(NOT OpenVDB_ABI) message(WARNING "Unable to determine OpenVDB ABI version from OpenVDB " "installation. The library major version \"${OpenVDB_MAJOR_VERSION}\" " @@ -298,7 +313,17 @@ endif() # Add standard dependencies -find_package(IlmBase COMPONENTS Half) +macro(just_fail msg) + set(OpenVDB_FOUND FALSE) + if(OpenVDB_FIND_REQUIRED) + message(FATAL_ERROR ${msg}) + elseif(NOT OpenVDB_FIND_QUIETLY) + message(WARNING ${msg}) + endif() + return() +endmacro() + +find_package(IlmBase QUIET COMPONENTS Half) if(NOT IlmBase_FOUND) pkg_check_modules(IlmBase QUIET IlmBase) endif() @@ -306,20 +331,20 @@ if (IlmBase_FOUND AND NOT TARGET IlmBase::Half) message(STATUS "Falling back to IlmBase found by pkg-config...") find_library(IlmHalf_LIBRARY NAMES Half) - if(IlmHalf_LIBRARY-NOTFOUND) - message(FATAL_ERROR "IlmBase::Half can not be found!") + if(IlmHalf_LIBRARY-NOTFOUND OR NOT IlmBase_INCLUDE_DIRS) + just_fail("IlmBase::Half can not be found!") endif() add_library(IlmBase::Half UNKNOWN IMPORTED) set_target_properties(IlmBase::Half PROPERTIES IMPORTED_LOCATION "${IlmHalf_LIBRARY}" - INTERFACE_INCLUDE_DIRECTORIES ${IlmBase_INCLUDE_DIRS}) + INTERFACE_INCLUDE_DIRECTORIES "${IlmBase_INCLUDE_DIRS}") elseif(NOT IlmBase_FOUND) - message(FATAL_ERROR "IlmBase::Half can not be found!") + just_fail("IlmBase::Half can not be found!") endif() -find_package(TBB REQUIRED COMPONENTS tbb) -find_package(ZLIB REQUIRED) -find_package(Boost REQUIRED COMPONENTS iostreams system) +find_package(TBB ${_quiet} ${_required} COMPONENTS tbb) +find_package(ZLIB ${_quiet} ${_required}) +find_package(Boost ${_quiet} ${_required} COMPONENTS iostreams system ) # Use GetPrerequisites to see which libraries this OpenVDB lib has linked to # which we can query for optional deps. This basically runs ldd/otoll/objdump @@ -380,7 +405,7 @@ unset(_OPENVDB_PREREQUISITE_LIST) unset(_HAS_DEP) if(OpenVDB_USES_BLOSC) - find_package(Blosc ) + find_package(Blosc QUIET) if(NOT Blosc_FOUND OR NOT TARGET Blosc::blosc) message(STATUS "find_package could not find Blosc. Using fallback blosc search...") find_path(Blosc_INCLUDE_DIR blosc.h) @@ -392,25 +417,25 @@ if(OpenVDB_USES_BLOSC) IMPORTED_LOCATION "${Blosc_LIBRARY}" INTERFACE_INCLUDE_DIRECTORIES ${Blosc_INCLUDE_DIR}) elseif() - message(FATAL_ERROR "Blosc library can not be found!") + just_fail("Blosc library can not be found!") endif() endif() endif() if(OpenVDB_USES_LOG4CPLUS) - find_package(Log4cplus REQUIRED) + find_package(Log4cplus ${_quiet} ${_required}) endif() if(OpenVDB_USES_ILM) - find_package(IlmBase REQUIRED) + find_package(IlmBase ${_quiet} ${_required}) endif() if(OpenVDB_USES_EXR) - find_package(OpenEXR REQUIRED) + find_package(OpenEXR ${_quiet} ${_required}) endif() if(UNIX) - find_package(Threads REQUIRED) + find_package(Threads ${_quiet} ${_required}) endif() # Set deps. Note that the order here is important. If we're building against @@ -500,18 +525,15 @@ foreach(COMPONENT ${OpenVDB_FIND_COMPONENTS}) IMPORTED_LINK_DEPENDENT_LIBRARIES "${_OPENVDB_HIDDEN_DEPENDENCIES}" # non visible deps INTERFACE_LINK_LIBRARIES "${_OPENVDB_VISIBLE_DEPENDENCIES}" # visible deps (headers) INTERFACE_COMPILE_FEATURES cxx_std_11 + IMPORTED_LOCATION "${OpenVDB_${COMPONENT}_LIBRARY}" ) - if (_is_multi) - set_target_properties(OpenVDB::${COMPONENT} PROPERTIES - IMPORTED_LOCATION_RELEASE "${OpenVDB_${COMPONENT}_LIBRARY_RELEASE}" - IMPORTED_LOCATION_DEBUG "${OpenVDB_${COMPONENT}_LIBRARY_DEBUG}" - ) - else () - set_target_properties(OpenVDB::${COMPONENT} PROPERTIES - IMPORTED_LOCATION "${OpenVDB_${COMPONENT}_LIBRARY}" - ) - endif () + if (_is_multi) + set_target_properties(OpenVDB::${COMPONENT} PROPERTIES + IMPORTED_LOCATION_RELEASE "${OpenVDB_${COMPONENT}_LIBRARY_RELEASE}" + IMPORTED_LOCATION_DEBUG "${OpenVDB_${COMPONENT}_LIBRARY_DEBUG}" + ) + endif () if (OPENVDB_USE_STATIC_LIBS) set_target_properties(OpenVDB::${COMPONENT} PROPERTIES @@ -521,7 +543,7 @@ foreach(COMPONENT ${OpenVDB_FIND_COMPONENTS}) endif() endforeach() -if(OpenVDB_FOUND AND NOT ${CMAKE_FIND_PACKAGE_NAME}_FIND_QUIETLY) +if(OpenVDB_FOUND AND NOT OpenVDB_FIND_QUIETLY) message(STATUS "OpenVDB libraries: ${OpenVDB_LIBRARIES}") endif() diff --git a/cmake/modules/OpenVDBUtils.cmake b/cmake/modules/OpenVDBUtils.cmake index bb3ce6e65d..f64eda6f2c 100644 --- a/cmake/modules/OpenVDBUtils.cmake +++ b/cmake/modules/OpenVDBUtils.cmake @@ -125,7 +125,9 @@ function(OPENVDB_ABI_VERSION_FROM_PRINT OPENVDB_PRINT) cmake_parse_arguments(_VDB "QUIET" "ABI" "" ${ARGN}) if(NOT EXISTS ${OPENVDB_PRINT}) - message(WARNING "vdb_print not found! ${OPENVDB_PRINT}") + if(NOT OpenVDB_FIND_QUIETLY) + message(WARNING "vdb_print not found! ${OPENVDB_PRINT}") + endif() return() endif() @@ -148,7 +150,9 @@ function(OPENVDB_ABI_VERSION_FROM_PRINT OPENVDB_PRINT) endif() if(${_VDB_PRINT_RETURN_STATUS}) - message(WARNING "vdb_print returned with status ${_VDB_PRINT_RETURN_STATUS}") + if(NOT OpenVDB_FIND_QUIETLY) + message(WARNING "vdb_print returned with status ${_VDB_PRINT_RETURN_STATUS}") + endif() return() endif() diff --git a/deps/CGAL/CGAL.cmake b/deps/CGAL/CGAL.cmake index 285d819ab3..f0584c5b58 100644 --- a/deps/CGAL/CGAL.cmake +++ b/deps/CGAL/CGAL.cmake @@ -17,4 +17,4 @@ ExternalProject_Add_Step(dep_CGAL dep_CGAL_relocation_fix DEPENDEES install COMMAND ${CMAKE_COMMAND} -E remove CGALConfig-installation-dirs.cmake WORKING_DIRECTORY "${DESTDIR}/usr/local/${CMAKE_INSTALL_LIBDIR}/cmake/CGAL" -) \ No newline at end of file +) diff --git a/deps/GMP/GMP.cmake b/deps/GMP/GMP.cmake index 8bcf948592..4e8228cbac 100644 --- a/deps/GMP/GMP.cmake +++ b/deps/GMP/GMP.cmake @@ -18,7 +18,8 @@ if (MSVC) else () ExternalProject_Add(dep_GMP - URL https://gmplib.org/download/gmp/gmp-6.1.2.tar.bz2 + # URL https://gmplib.org/download/gmp/gmp-6.1.2.tar.bz2 + URL https://gmplib.org/download/gmp/gmp-6.2.0.tar.lz BUILD_IN_SOURCE ON CONFIGURE_COMMAND ./configure --enable-shared=no --enable-cxx=yes --enable-static=yes "--prefix=${DESTDIR}/usr/local" --with-pic BUILD_COMMAND make -j diff --git a/deps/openvdb-mods.patch b/deps/openvdb-mods.patch index 023cb53087..d80d0ffde1 100644 --- a/deps/openvdb-mods.patch +++ b/deps/openvdb-mods.patch @@ -1,24 +1,25 @@ -From dbe038fce8a15ddc9a5c83ec5156d7bc9e178015 Mon Sep 17 00:00:00 2001 +From d359098d9989ac7dbd149611d6ac941529fb4157 Mon Sep 17 00:00:00 2001 From: tamasmeszaros -Date: Wed, 16 Oct 2019 17:42:50 +0200 -Subject: [PATCH] Build fixes for PrusaSlicer integration +Date: Thu, 23 Jan 2020 17:17:36 +0100 +Subject: [PATCH] openvdb-mods -Signed-off-by: tamasmeszaros --- CMakeLists.txt | 3 - - cmake/FindBlosc.cmake | 218 --------------- + cmake/CheckAtomic.cmake | 106 ++++++ + cmake/FindBlosc.cmake | 218 ------------ cmake/FindCppUnit.cmake | 4 +- - cmake/FindIlmBase.cmake | 337 ---------------------- - cmake/FindOpenEXR.cmake | 329 ---------------------- + cmake/FindIlmBase.cmake | 337 ------------------ + cmake/FindOpenEXR.cmake | 329 ------------------ cmake/FindOpenVDB.cmake | 19 +- - cmake/FindTBB.cmake | 605 ++++++++++++++++++++-------------------- - openvdb/CMakeLists.txt | 13 +- + cmake/FindTBB.cmake | 599 ++++++++++++++++---------------- + openvdb/CMakeLists.txt | 16 +- openvdb/Grid.cc | 3 + openvdb/PlatformConfig.h | 9 +- openvdb/cmd/CMakeLists.txt | 4 +- openvdb/unittest/CMakeLists.txt | 3 +- openvdb/unittest/TestFile.cc | 2 +- - 13 files changed, 336 insertions(+), 1213 deletions(-) + 14 files changed, 442 insertions(+), 1210 deletions(-) + create mode 100644 cmake/CheckAtomic.cmake delete mode 100644 cmake/FindBlosc.cmake delete mode 100644 cmake/FindIlmBase.cmake delete mode 100644 cmake/FindOpenEXR.cmake @@ -40,6 +41,119 @@ index 580b353..6d364c1 100644 cmake/FindOpenVDB.cmake cmake/FindTBB.cmake cmake/OpenVDBGLFW3Setup.cmake +diff --git a/cmake/CheckAtomic.cmake b/cmake/CheckAtomic.cmake +new file mode 100644 +index 0000000..c045e30 +--- /dev/null ++++ b/cmake/CheckAtomic.cmake +@@ -0,0 +1,106 @@ ++# atomic builtins are required for threading support. ++ ++INCLUDE(CheckCXXSourceCompiles) ++INCLUDE(CheckLibraryExists) ++ ++# Sometimes linking against libatomic is required for atomic ops, if ++# the platform doesn't support lock-free atomics. ++ ++function(check_working_cxx_atomics varname) ++ set(OLD_CMAKE_REQUIRED_FLAGS ${CMAKE_REQUIRED_FLAGS}) ++ set(CMAKE_REQUIRED_FLAGS "${CMAKE_REQUIRED_FLAGS} -std=c++11") ++ CHECK_CXX_SOURCE_COMPILES(" ++#include ++std::atomic x; ++int main() { ++ return x; ++} ++" ${varname}) ++ set(CMAKE_REQUIRED_FLAGS ${OLD_CMAKE_REQUIRED_FLAGS}) ++endfunction(check_working_cxx_atomics) ++ ++function(check_working_cxx_atomics64 varname) ++ set(OLD_CMAKE_REQUIRED_FLAGS ${CMAKE_REQUIRED_FLAGS}) ++ set(CMAKE_REQUIRED_FLAGS "-std=c++11 ${CMAKE_REQUIRED_FLAGS}") ++ CHECK_CXX_SOURCE_COMPILES(" ++#include ++#include ++std::atomic x (0); ++int main() { ++ uint64_t i = x.load(std::memory_order_relaxed); ++ return 0; ++} ++" ${varname}) ++ set(CMAKE_REQUIRED_FLAGS ${OLD_CMAKE_REQUIRED_FLAGS}) ++endfunction(check_working_cxx_atomics64) ++ ++ ++# This isn't necessary on MSVC, so avoid command-line switch annoyance ++# by only running on GCC-like hosts. ++if (LLVM_COMPILER_IS_GCC_COMPATIBLE) ++ # First check if atomics work without the library. ++ check_working_cxx_atomics(HAVE_CXX_ATOMICS_WITHOUT_LIB) ++ # If not, check if the library exists, and atomics work with it. ++ if(NOT HAVE_CXX_ATOMICS_WITHOUT_LIB) ++ check_library_exists(atomic __atomic_fetch_add_4 "" HAVE_LIBATOMIC) ++ if( HAVE_LIBATOMIC ) ++ list(APPEND CMAKE_REQUIRED_LIBRARIES "atomic") ++ check_working_cxx_atomics(HAVE_CXX_ATOMICS_WITH_LIB) ++ if (NOT HAVE_CXX_ATOMICS_WITH_LIB) ++ message(FATAL_ERROR "Host compiler must support std::atomic!") ++ endif() ++ else() ++ message(FATAL_ERROR "Host compiler appears to require libatomic, but cannot find it.") ++ endif() ++ endif() ++endif() ++ ++# Check for 64 bit atomic operations. ++if(MSVC) ++ set(HAVE_CXX_ATOMICS64_WITHOUT_LIB True) ++else() ++ check_working_cxx_atomics64(HAVE_CXX_ATOMICS64_WITHOUT_LIB) ++endif() ++ ++# If not, check if the library exists, and atomics work with it. ++if(NOT HAVE_CXX_ATOMICS64_WITHOUT_LIB) ++ check_library_exists(atomic __atomic_load_8 "" HAVE_CXX_LIBATOMICS64) ++ if(HAVE_CXX_LIBATOMICS64) ++ list(APPEND CMAKE_REQUIRED_LIBRARIES "atomic") ++ check_working_cxx_atomics64(HAVE_CXX_ATOMICS64_WITH_LIB) ++ if (NOT HAVE_CXX_ATOMICS64_WITH_LIB) ++ message(FATAL_ERROR "Host compiler must support 64-bit std::atomic!") ++ endif() ++ else() ++ message(FATAL_ERROR "Host compiler appears to require libatomic for 64-bit operations, but cannot find it.") ++ endif() ++endif() ++ ++## TODO: This define is only used for the legacy atomic operations in ++## llvm's Atomic.h, which should be replaced. Other code simply ++## assumes C++11 works. ++CHECK_CXX_SOURCE_COMPILES(" ++#ifdef _MSC_VER ++#include ++#endif ++int main() { ++#ifdef _MSC_VER ++ volatile LONG val = 1; ++ MemoryBarrier(); ++ InterlockedCompareExchange(&val, 0, 1); ++ InterlockedIncrement(&val); ++ InterlockedDecrement(&val); ++#else ++ volatile unsigned long val = 1; ++ __sync_synchronize(); ++ __sync_val_compare_and_swap(&val, 1, 0); ++ __sync_add_and_fetch(&val, 1); ++ __sync_sub_and_fetch(&val, 1); ++#endif ++ return 0; ++ } ++" LLVM_HAS_ATOMICS) ++ ++if( NOT LLVM_HAS_ATOMICS ) ++ message(STATUS "Warning: LLVM will be built thread-unsafe because atomic builtins are missing") ++endif() +\ No newline at end of file diff --git a/cmake/FindBlosc.cmake b/cmake/FindBlosc.cmake deleted file mode 100644 index 5aacfdd..0000000 @@ -965,7 +1079,7 @@ index 339c1a2..0000000 - message(FATAL_ERROR "Unable to find OpenEXR") -endif() diff --git a/cmake/FindOpenVDB.cmake b/cmake/FindOpenVDB.cmake -index 63a2eda..6211071 100644 +index 63a2eda..d9f6d07 100644 --- a/cmake/FindOpenVDB.cmake +++ b/cmake/FindOpenVDB.cmake @@ -244,7 +244,7 @@ set(OpenVDB_LIB_COMPONENTS "") @@ -1004,7 +1118,7 @@ index 63a2eda..6211071 100644 ) + + if (OPENVDB_USE_STATIC_LIBS) -+ set_target_properties(OpenVDB::${COMPONENT} PROPERTIES ++ set_target_properties(OpenVDB::${COMPONENT} PROPERTIES + INTERFACE_COMPILE_DEFINITIONS "OPENVDB_STATICLIB;OPENVDB_OPENEXR_STATICLIB" + ) + endif() @@ -1012,7 +1126,7 @@ index 63a2eda..6211071 100644 endforeach() diff --git a/cmake/FindTBB.cmake b/cmake/FindTBB.cmake -index bdf9c81..c6bdec9 100644 +index bdf9c81..06093a4 100644 --- a/cmake/FindTBB.cmake +++ b/cmake/FindTBB.cmake @@ -1,333 +1,332 @@ @@ -1022,35 +1136,21 @@ index bdf9c81..c6bdec9 100644 -# All rights reserved. This software is distributed under the -# Mozilla Public License 2.0 ( http://www.mozilla.org/MPL/2.0/ ) +# Copyright (c) 2015 Justus Calvin -+# + # +-# Redistributions of source code must retain the above copyright +-# and license notice and the following restrictions and disclaimer. +# 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. -+ - # --# Redistributions of source code must retain the above copyright --# and license notice and the following restrictions and disclaimer. -+# FindTBB -+# ------- # -# * Neither the name of DreamWorks Animation nor the names of -# its contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -+# Find TBB include directories and libraries. ++# The above copyright notice and this permission notice shall be included in all ++# copies or substantial portions of the Software. # -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT @@ -1065,7 +1165,14 @@ index bdf9c81..c6bdec9 100644 -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -# IN NO EVENT SHALL THE COPYRIGHT HOLDERS' AND CONTRIBUTORS' AGGREGATE -# LIABILITY FOR ALL CLAIMS REGARDLESS OF THEIR BASIS EXCEED US$250.00. -+# Usage: ++# 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. ++ # -#[=======================================================================[.rst: - @@ -1142,19 +1249,26 @@ index bdf9c81..c6bdec9 100644 -if(POLICY CMP0057) - cmake_policy(SET CMP0057 NEW) -endif() ++# FindTBB ++# ------- ++# ++# Find TBB include directories and libraries. ++# ++# Usage: ++# +# find_package(TBB [major[.minor]] [EXACT] +# [QUIET] [REQUIRED] +# [[COMPONENTS] [components...]] -+# [OPTIONAL_COMPONENTS components...]) ++# [OPTIONAL_COMPONENTS components...]) +# -+# where the allowed components are tbbmalloc and tbb_preview. Users may modify ++# where the allowed components are tbbmalloc and tbb_preview. Users may modify +# the behavior of this module with the following variables: +# +# * TBB_ROOT_DIR - The base directory the of TBB installation. +# * TBB_INCLUDE_DIR - The directory that contains the TBB headers files. +# * TBB_LIBRARY - The directory that contains the TBB library files. -+# * TBB__LIBRARY - The path of the TBB the corresponding TBB library. -+# These libraries, if specified, override the ++# * TBB__LIBRARY - The path of the TBB the corresponding TBB library. ++# These libraries, if specified, override the +# corresponding library search results, where +# may be tbb, tbb_debug, tbbmalloc, tbbmalloc_debug, +# tbb_preview, or tbb_preview_debug. @@ -1167,7 +1281,7 @@ index bdf9c81..c6bdec9 100644 +# Users may modify the behavior of this module with the following environment +# variables: +# -+# * TBB_INSTALL_DIR ++# * TBB_INSTALL_DIR +# * TBBROOT +# * LIBRARY_PATH +# @@ -1180,15 +1294,15 @@ index bdf9c81..c6bdec9 100644 +# * TBB_VERSION - The full version string +# * TBB_VERSION_MAJOR - The major version +# * TBB_VERSION_MINOR - The minor version -+# * TBB_INTERFACE_VERSION - The interface version number defined in ++# * TBB_INTERFACE_VERSION - The interface version number defined in +# tbb/tbb_stddef.h. -+# * TBB__LIBRARY_RELEASE - The path of the TBB release version of ++# * TBB__LIBRARY_RELEASE - The path of the TBB release version of +# , where may be tbb, tbb_debug, -+# tbbmalloc, tbbmalloc_debug, tbb_preview, or ++# tbbmalloc, tbbmalloc_debug, tbb_preview, or +# tbb_preview_debug. -+# * TBB__LIBRARY_DEGUG - The path of the TBB release version of ++# * TBB__LIBRARY_DEGUG - The path of the TBB release version of +# , where may be tbb, tbb_debug, -+# tbbmalloc, tbbmalloc_debug, tbb_preview, or ++# tbbmalloc, tbbmalloc_debug, tbb_preview, or +# tbb_preview_debug. +# +# The following varibles should be used to build and link with TBB: @@ -1244,12 +1358,10 @@ index bdf9c81..c6bdec9 100644 - set(TBB_FIND_COMPONENTS ${_TBB_COMPONENT_LIST}) -endif() +include(FindPackageHandleStandardArgs) -+ -+find_package(Threads QUIET REQUIRED) -# Append TBB_ROOT or $ENV{TBB_ROOT} if set (prioritize the direct cmake var) -set(_TBB_ROOT_SEARCH_DIR "") -+if(NOT TBB_FOUND) ++find_package(Threads QUIET REQUIRED) -if(TBB_ROOT) - list(APPEND _TBB_ROOT_SEARCH_DIR ${TBB_ROOT}) @@ -1257,41 +1369,9 @@ index bdf9c81..c6bdec9 100644 - set(_ENV_TBB_ROOT $ENV{TBB_ROOT}) - if(_ENV_TBB_ROOT) - list(APPEND _TBB_ROOT_SEARCH_DIR ${_ENV_TBB_ROOT}) -+ ################################## -+ # Check the build type -+ ################################## -+ -+ if(NOT DEFINED TBB_USE_DEBUG_BUILD) -+ if(CMAKE_BUILD_TYPE MATCHES "(Debug|DEBUG|debug)") -+ set(TBB_BUILD_TYPE DEBUG) -+ else() -+ set(TBB_BUILD_TYPE RELEASE) -+ endif() -+ elseif(TBB_USE_DEBUG_BUILD) -+ set(TBB_BUILD_TYPE DEBUG) -+ else() -+ set(TBB_BUILD_TYPE RELEASE) - endif() +- endif() -endif() -+ -+ ################################## -+ # Set the TBB search directories -+ ################################## -+ -+ # Define search paths based on user input and environment variables -+ set(TBB_SEARCH_DIR ${TBB_ROOT_DIR} $ENV{TBB_INSTALL_DIR} $ENV{TBBROOT}) -+ -+ # Define the search directories based on the current platform -+ if(CMAKE_SYSTEM_NAME STREQUAL "Windows") -+ set(TBB_DEFAULT_SEARCH_DIR "C:/Program Files/Intel/TBB" -+ "C:/Program Files (x86)/Intel/TBB") -+ -+ # Set the target architecture -+ if(CMAKE_SIZEOF_VOID_P EQUAL 8) -+ set(TBB_ARCHITECTURE "intel64") -+ else() -+ set(TBB_ARCHITECTURE "ia32") -+ endif() ++if(NOT TBB_FOUND) -# Additionally try and use pkconfig to find Tbb - @@ -1339,6 +1419,57 @@ index bdf9c81..c6bdec9 100644 - - set(Tbb_VERSION ${Tbb_VERSION_MAJOR}.${Tbb_VERSION_MINOR}) -endif() ++ ################################## ++ # Check the build type ++ ################################## ++ ++ if(NOT DEFINED TBB_USE_DEBUG_BUILD) ++ if(CMAKE_BUILD_TYPE MATCHES "(Debug|DEBUG|debug)") ++ set(TBB_BUILD_TYPE DEBUG) ++ else() ++ set(TBB_BUILD_TYPE RELEASE) ++ endif() ++ elseif(TBB_USE_DEBUG_BUILD) ++ set(TBB_BUILD_TYPE DEBUG) ++ else() ++ set(TBB_BUILD_TYPE RELEASE) ++ endif() + +-# ------------------------------------------------------------------------ +-# Search for TBB lib DIR +-# ------------------------------------------------------------------------ ++ ################################## ++ # Set the TBB search directories ++ ################################## + +-set(_TBB_LIBRARYDIR_SEARCH_DIRS "") ++ # Define search paths based on user input and environment variables ++ set(TBB_SEARCH_DIR ${TBB_ROOT_DIR} $ENV{TBB_INSTALL_DIR} $ENV{TBBROOT}) + +-# Append to _TBB_LIBRARYDIR_SEARCH_DIRS in priority order ++ # Define the search directories based on the current platform ++ if(CMAKE_SYSTEM_NAME STREQUAL "Windows") ++ set(TBB_DEFAULT_SEARCH_DIR "C:/Program Files/Intel/TBB" ++ "C:/Program Files (x86)/Intel/TBB") + +-set(_TBB_LIBRARYDIR_SEARCH_DIRS "") +-list(APPEND _TBB_LIBRARYDIR_SEARCH_DIRS +- ${TBB_LIBRARYDIR} +- ${_TBB_ROOT_SEARCH_DIR} +- ${PC_Tbb_LIBRARY_DIRS} +- ${SYSTEM_LIBRARY_PATHS} +-) ++ # Set the target architecture ++ if(CMAKE_SIZEOF_VOID_P EQUAL 8) ++ set(TBB_ARCHITECTURE "intel64") ++ else() ++ set(TBB_ARCHITECTURE "ia32") ++ endif() + +-set(TBB_PATH_SUFFIXES +- lib64 +- lib +-) + # Set the TBB search library path search suffix based on the version of VC + if(WINDOWS_STORE) + set(TBB_LIB_PATH_SUFFIX "lib/${TBB_ARCHITECTURE}/vc11_ui") @@ -1352,104 +1483,16 @@ index bdf9c81..c6bdec9 100644 + set(TBB_LIB_PATH_SUFFIX "lib/${TBB_ARCHITECTURE}/vc10") + endif() --# ------------------------------------------------------------------------ --# Search for TBB lib DIR --# ------------------------------------------------------------------------ +-# platform branching + # Add the library path search suffix for the VC independent version of TBB + list(APPEND TBB_LIB_PATH_SUFFIX "lib/${TBB_ARCHITECTURE}/vc_mt") -+ -+ elseif(CMAKE_SYSTEM_NAME STREQUAL "Darwin") -+ # OS X -+ set(TBB_DEFAULT_SEARCH_DIR "/opt/intel/tbb") -+ -+ # TODO: Check to see which C++ library is being used by the compiler. -+ if(NOT ${CMAKE_SYSTEM_VERSION} VERSION_LESS 13.0) -+ # The default C++ library on OS X 10.9 and later is libc++ -+ set(TBB_LIB_PATH_SUFFIX "lib/libc++" "lib") -+ else() -+ set(TBB_LIB_PATH_SUFFIX "lib") -+ endif() -+ elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux") -+ # Linux -+ set(TBB_DEFAULT_SEARCH_DIR "/opt/intel/tbb") -+ -+ # TODO: Check compiler version to see the suffix should be /gcc4.1 or -+ # /gcc4.1. For now, assume that the compiler is more recent than -+ # gcc 4.4.x or later. -+ if(CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64") -+ set(TBB_LIB_PATH_SUFFIX "lib/intel64/gcc4.4") -+ elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "^i.86$") -+ set(TBB_LIB_PATH_SUFFIX "lib/ia32/gcc4.4") -+ endif() -+ endif() -+ -+ ################################## -+ # Find the TBB include dir -+ ################################## -+ -+ find_path(TBB_INCLUDE_DIRS tbb/tbb.h -+ HINTS ${TBB_INCLUDE_DIR} ${TBB_SEARCH_DIR} -+ PATHS ${TBB_DEFAULT_SEARCH_DIR} -+ PATH_SUFFIXES include) -+ -+ ################################## -+ # Set version strings -+ ################################## -+ -+ if(TBB_INCLUDE_DIRS) -+ file(READ "${TBB_INCLUDE_DIRS}/tbb/tbb_stddef.h" _tbb_version_file) -+ string(REGEX REPLACE ".*#define TBB_VERSION_MAJOR ([0-9]+).*" "\\1" -+ TBB_VERSION_MAJOR "${_tbb_version_file}") -+ string(REGEX REPLACE ".*#define TBB_VERSION_MINOR ([0-9]+).*" "\\1" -+ TBB_VERSION_MINOR "${_tbb_version_file}") -+ string(REGEX REPLACE ".*#define TBB_INTERFACE_VERSION ([0-9]+).*" "\\1" -+ TBB_INTERFACE_VERSION "${_tbb_version_file}") -+ set(TBB_VERSION "${TBB_VERSION_MAJOR}.${TBB_VERSION_MINOR}") -+ endif() - --set(_TBB_LIBRARYDIR_SEARCH_DIRS "") -+ ################################## -+ # Find TBB components -+ ################################## - --# Append to _TBB_LIBRARYDIR_SEARCH_DIRS in priority order -+ if(TBB_VERSION VERSION_LESS 4.3) -+ set(TBB_SEARCH_COMPOMPONENTS tbb_preview tbbmalloc tbb) -+ else() -+ set(TBB_SEARCH_COMPOMPONENTS tbb_preview tbbmalloc_proxy tbbmalloc tbb) -+ endif() - --set(_TBB_LIBRARYDIR_SEARCH_DIRS "") --list(APPEND _TBB_LIBRARYDIR_SEARCH_DIRS -- ${TBB_LIBRARYDIR} -- ${_TBB_ROOT_SEARCH_DIR} -- ${PC_Tbb_LIBRARY_DIRS} -- ${SYSTEM_LIBRARY_PATHS} --) -+ if(TBB_STATIC) -+ set(TBB_STATIC_SUFFIX "_static") -+ endif() - --set(TBB_PATH_SUFFIXES -- lib64 -- lib --) -+ # Find each component -+ foreach(_comp ${TBB_SEARCH_COMPOMPONENTS}) -+ if(";${TBB_FIND_COMPONENTS};tbb;" MATCHES ";${_comp};") - --# platform branching -+ unset(TBB_${_comp}_LIBRARY_DEBUG CACHE) -+ unset(TBB_${_comp}_LIBRARY_RELEASE CACHE) -if(UNIX) - list(INSERT TBB_PATH_SUFFIXES 0 lib/x86_64-linux-gnu) -endif() -+ # Search for the libraries -+ find_library(TBB_${_comp}_LIBRARY_RELEASE ${_comp}${TBB_STATIC_SUFFIX} -+ HINTS ${TBB_LIBRARY} ${TBB_SEARCH_DIR} -+ PATHS ${TBB_DEFAULT_SEARCH_DIR} ENV LIBRARY_PATH -+ PATH_SUFFIXES ${TBB_LIB_PATH_SUFFIX}) ++ elseif(CMAKE_SYSTEM_NAME STREQUAL "Darwin") ++ # OS X ++ set(TBB_DEFAULT_SEARCH_DIR "/opt/intel/tbb") -if(APPLE) - if(TBB_FOR_CLANG) @@ -1471,29 +1514,33 @@ index bdf9c81..c6bdec9 100644 - list(GET GCC_VERSION_COMPONENTS 0 GCC_MAJOR) - list(GET GCC_VERSION_COMPONENTS 1 GCC_MINOR) - list(INSERT TBB_PATH_SUFFIXES 0 lib/intel64/gcc${GCC_MAJOR}.${GCC_MINOR}) -- else() ++ # TODO: Check to see which C++ library is being used by the compiler. ++ if(NOT ${CMAKE_SYSTEM_VERSION} VERSION_LESS 13.0) ++ # The default C++ library on OS X 10.9 and later is libc++ ++ set(TBB_LIB_PATH_SUFFIX "lib/libc++" "lib") + else() - list(INSERT TBB_PATH_SUFFIXES 0 lib/intel64/gcc4.4) -- endif() -- endif() ++ set(TBB_LIB_PATH_SUFFIX "lib") ++ endif() ++ elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux") ++ # Linux ++ set(TBB_DEFAULT_SEARCH_DIR "/opt/intel/tbb") ++ ++ # TODO: Check compiler version to see the suffix should be /gcc4.1 or ++ # /gcc4.1. For now, assume that the compiler is more recent than ++ # gcc 4.4.x or later. ++ if(CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64") ++ set(TBB_LIB_PATH_SUFFIX "lib/intel64/gcc4.4") ++ elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "^i.86$") ++ set(TBB_LIB_PATH_SUFFIX "lib/ia32/gcc4.4") + endif() + endif() -endif() -+ find_library(TBB_${_comp}_LIBRARY_DEBUG ${_comp}${TBB_STATIC_SUFFIX}_debug -+ HINTS ${TBB_LIBRARY} ${TBB_SEARCH_DIR} -+ PATHS ${TBB_DEFAULT_SEARCH_DIR} ENV LIBRARY_PATH -+ PATH_SUFFIXES ${TBB_LIB_PATH_SUFFIX}) - +- -if(UNIX AND TBB_USE_STATIC_LIBS) - set(_TBB_ORIG_CMAKE_FIND_LIBRARY_SUFFIXES ${CMAKE_FIND_LIBRARY_SUFFIXES}) - set(CMAKE_FIND_LIBRARY_SUFFIXES ".a") -endif() -+ if(TBB_${_comp}_LIBRARY_DEBUG) -+ list(APPEND TBB_LIBRARIES_DEBUG "${TBB_${_comp}_LIBRARY_DEBUG}") -+ endif() -+ if(TBB_${_comp}_LIBRARY_RELEASE) -+ list(APPEND TBB_LIBRARIES_RELEASE "${TBB_${_comp}_LIBRARY_RELEASE}") -+ endif() -+ if(TBB_${_comp}_LIBRARY_${TBB_BUILD_TYPE} AND NOT TBB_${_comp}_LIBRARY) -+ set(TBB_${_comp}_LIBRARY "${TBB_${_comp}_LIBRARY_${TBB_BUILD_TYPE}}") -+ endif() -set(Tbb_LIB_COMPONENTS "") - @@ -1516,39 +1563,44 @@ index bdf9c81..c6bdec9 100644 - # Extract the directory and apply the matched text (in brackets) - get_filename_component(Tbb_${COMPONENT}_DIR "${Tbb_${COMPONENT}_LIBRARY}" DIRECTORY) - set(Tbb_${COMPONENT}_LIBRARY "${Tbb_${COMPONENT}_DIR}/${CMAKE_MATCH_1}") -+ if(TBB_${_comp}_LIBRARY AND EXISTS "${TBB_${_comp}_LIBRARY}") -+ set(TBB_${_comp}_FOUND TRUE) -+ else() -+ set(TBB_${_comp}_FOUND FALSE) - endif() +- endif() +- endif() ++ ################################## ++ # Find the TBB include dir ++ ################################## + -+ # Mark internal variables as advanced -+ mark_as_advanced(TBB_${_comp}_LIBRARY_RELEASE) -+ mark_as_advanced(TBB_${_comp}_LIBRARY_DEBUG) -+ mark_as_advanced(TBB_${_comp}_LIBRARY) ++ find_path(TBB_INCLUDE_DIRS tbb/tbb.h ++ HINTS ${TBB_INCLUDE_DIR} ${TBB_SEARCH_DIR} ++ PATHS ${TBB_DEFAULT_SEARCH_DIR} ++ PATH_SUFFIXES include) + - endif() -- endif() -+ endforeach() ++ ################################## ++ # Set version strings ++ ################################## ++ ++ if(TBB_INCLUDE_DIRS) ++ file(READ "${TBB_INCLUDE_DIRS}/tbb/tbb_stddef.h" _tbb_version_file) ++ string(REGEX REPLACE ".*#define TBB_VERSION_MAJOR ([0-9]+).*" "\\1" ++ TBB_VERSION_MAJOR "${_tbb_version_file}") ++ string(REGEX REPLACE ".*#define TBB_VERSION_MINOR ([0-9]+).*" "\\1" ++ TBB_VERSION_MINOR "${_tbb_version_file}") ++ string(REGEX REPLACE ".*#define TBB_INTERFACE_VERSION ([0-9]+).*" "\\1" ++ TBB_INTERFACE_VERSION "${_tbb_version_file}") ++ set(TBB_VERSION "${TBB_VERSION_MAJOR}.${TBB_VERSION_MINOR}") + endif() - list(APPEND Tbb_LIB_COMPONENTS ${Tbb_${COMPONENT}_LIBRARY}) + ################################## -+ # Set compile flags and libraries ++ # Find TBB components + ################################## - if(Tbb_${COMPONENT}_LIBRARY) - set(TBB_${COMPONENT}_FOUND TRUE) -- else() ++ if(TBB_VERSION VERSION_LESS 4.3) ++ set(TBB_SEARCH_COMPOMPONENTS tbb_preview tbbmalloc tbb) + else() - set(TBB_${COMPONENT}_FOUND FALSE) -+ set(TBB_DEFINITIONS_RELEASE "") -+ set(TBB_DEFINITIONS_DEBUG "TBB_USE_DEBUG=1") -+ -+ if(TBB_LIBRARIES_${TBB_BUILD_TYPE}) -+ set(TBB_LIBRARIES "${TBB_LIBRARIES_${TBB_BUILD_TYPE}}") -+ endif() -+ -+ if(NOT MSVC AND NOT TBB_LIBRARIES) -+ set(TBB_LIBRARIES ${TBB_LIBRARIES_RELEASE}) ++ set(TBB_SEARCH_COMPOMPONENTS tbb_preview tbbmalloc_proxy tbbmalloc tbb) endif() -endforeach() @@ -1556,61 +1608,51 @@ index bdf9c81..c6bdec9 100644 - set(CMAKE_FIND_LIBRARY_SUFFIXES ${_TBB_ORIG_CMAKE_FIND_LIBRARY_SUFFIXES}) - unset(_TBB_ORIG_CMAKE_FIND_LIBRARY_SUFFIXES) -endif() -+ set(TBB_DEFINITIONS "") -+ if (MSVC AND TBB_STATIC) -+ set(TBB_DEFINITIONS __TBB_NO_IMPLICIT_LINKAGE) -+ endif () -+ -+ unset (TBB_STATIC_SUFFIX) -+ -+ find_package_handle_standard_args(TBB -+ REQUIRED_VARS TBB_INCLUDE_DIRS TBB_LIBRARIES -+ FAIL_MESSAGE "TBB library cannot be found. Consider set TBBROOT environment variable." -+ HANDLE_COMPONENTS -+ VERSION_VAR TBB_VERSION) -+ -+ ################################## -+ # Create targets -+ ################################## -+ -+ if(NOT CMAKE_VERSION VERSION_LESS 3.0 AND TBB_FOUND) -+ add_library(TBB::tbb UNKNOWN IMPORTED) -+ set_target_properties(TBB::tbb PROPERTIES -+ INTERFACE_COMPILE_DEFINITIONS "${TBB_DEFINITIONS}" -+ INTERFACE_LINK_LIBRARIES "Threads::Threads;${CMAKE_DL_LIBS}" -+ INTERFACE_INCLUDE_DIRECTORIES ${TBB_INCLUDE_DIRS} -+ IMPORTED_LOCATION ${TBB_LIBRARIES}) -+ if(TBB_LIBRARIES_RELEASE AND TBB_LIBRARIES_DEBUG) -+ set_target_properties(TBB::tbb PROPERTIES -+ INTERFACE_COMPILE_DEFINITIONS "${TBB_DEFINITIONS};$<$,$>:${TBB_DEFINITIONS_DEBUG}>;$<$:${TBB_DEFINITIONS_RELEASE}>" -+ IMPORTED_LOCATION_DEBUG ${TBB_LIBRARIES_DEBUG} -+ IMPORTED_LOCATION_RELWITHDEBINFO ${TBB_LIBRARIES_RELEASE} -+ IMPORTED_LOCATION_RELEASE ${TBB_LIBRARIES_RELEASE} -+ IMPORTED_LOCATION_MINSIZEREL ${TBB_LIBRARIES_RELEASE} -+ ) -+ endif() ++ if(TBB_STATIC) ++ set(TBB_STATIC_SUFFIX "_static") + endif() -# ------------------------------------------------------------------------ -# Cache and set TBB_FOUND -# ------------------------------------------------------------------------ -+ mark_as_advanced(TBB_INCLUDE_DIRS TBB_LIBRARIES) ++ # Find each component ++ foreach(_comp ${TBB_SEARCH_COMPOMPONENTS}) ++ if(";${TBB_FIND_COMPONENTS};tbb;" MATCHES ";${_comp};") + -+ unset(TBB_ARCHITECTURE) -+ unset(TBB_BUILD_TYPE) -+ unset(TBB_LIB_PATH_SUFFIX) -+ unset(TBB_DEFAULT_SEARCH_DIR) ++ unset(TBB_${_comp}_LIBRARY_DEBUG CACHE) ++ unset(TBB_${_comp}_LIBRARY_RELEASE CACHE) + -+ if(TBB_DEBUG) -+ message(STATUS " TBB_FOUND = ${TBB_FOUND}") -+ message(STATUS " TBB_INCLUDE_DIRS = ${TBB_INCLUDE_DIRS}") -+ message(STATUS " TBB_DEFINITIONS = ${TBB_DEFINITIONS}") -+ message(STATUS " TBB_LIBRARIES = ${TBB_LIBRARIES}") -+ message(STATUS " TBB_DEFINITIONS_DEBUG = ${TBB_DEFINITIONS_DEBUG}") -+ message(STATUS " TBB_LIBRARIES_DEBUG = ${TBB_LIBRARIES_DEBUG}") -+ message(STATUS " TBB_DEFINITIONS_RELEASE = ${TBB_DEFINITIONS_RELEASE}") -+ message(STATUS " TBB_LIBRARIES_RELEASE = ${TBB_LIBRARIES_RELEASE}") -+ endif() ++ # Search for the libraries ++ find_library(TBB_${_comp}_LIBRARY_RELEASE ${_comp}${TBB_STATIC_SUFFIX} ++ HINTS ${TBB_LIBRARY} ${TBB_SEARCH_DIR} ++ PATHS ${TBB_DEFAULT_SEARCH_DIR} ENV LIBRARY_PATH ++ PATH_SUFFIXES ${TBB_LIB_PATH_SUFFIX}) ++ ++ find_library(TBB_${_comp}_LIBRARY_DEBUG ${_comp}${TBB_STATIC_SUFFIX}_debug ++ HINTS ${TBB_LIBRARY} ${TBB_SEARCH_DIR} ++ PATHS ${TBB_DEFAULT_SEARCH_DIR} ENV LIBRARY_PATH ++ PATH_SUFFIXES ${TBB_LIB_PATH_SUFFIX}) ++ ++ if(TBB_${_comp}_LIBRARY_DEBUG) ++ list(APPEND TBB_LIBRARIES_DEBUG "${TBB_${_comp}_LIBRARY_DEBUG}") ++ endif() ++ if(TBB_${_comp}_LIBRARY_RELEASE) ++ list(APPEND TBB_LIBRARIES_RELEASE "${TBB_${_comp}_LIBRARY_RELEASE}") ++ endif() ++ if(TBB_${_comp}_LIBRARY_${TBB_BUILD_TYPE} AND NOT TBB_${_comp}_LIBRARY) ++ set(TBB_${_comp}_LIBRARY "${TBB_${_comp}_LIBRARY_${TBB_BUILD_TYPE}}") ++ endif() ++ ++ if(TBB_${_comp}_LIBRARY AND EXISTS "${TBB_${_comp}_LIBRARY}") ++ set(TBB_${_comp}_FOUND TRUE) ++ else() ++ set(TBB_${_comp}_FOUND FALSE) ++ endif() ++ ++ # Mark internal variables as advanced ++ mark_as_advanced(TBB_${_comp}_LIBRARY_RELEASE) ++ mark_as_advanced(TBB_${_comp}_LIBRARY_DEBUG) ++ mark_as_advanced(TBB_${_comp}_LIBRARY) -include(FindPackageHandleStandardArgs) -find_package_handle_standard_args(TBB @@ -1646,13 +1688,82 @@ index bdf9c81..c6bdec9 100644 - INTERFACE_COMPILE_OPTIONS "${Tbb_DEFINITIONS}" - INTERFACE_INCLUDE_DIRECTORIES "${Tbb_INCLUDE_DIR}" - ) -- endif() -- endforeach() + endif() + endforeach() -elseif(TBB_FIND_REQUIRED) - message(FATAL_ERROR "Unable to find TBB") ++ ++ ################################## ++ # Set compile flags and libraries ++ ################################## ++ ++ set(TBB_DEFINITIONS_RELEASE "") ++ set(TBB_DEFINITIONS_DEBUG "TBB_USE_DEBUG=1") ++ ++ if(TBB_LIBRARIES_${TBB_BUILD_TYPE}) ++ set(TBB_LIBRARIES "${TBB_LIBRARIES_${TBB_BUILD_TYPE}}") ++ endif() ++ ++ if(NOT MSVC AND NOT TBB_LIBRARIES) ++ set(TBB_LIBRARIES ${TBB_LIBRARIES_RELEASE}) ++ endif() ++ ++ set(TBB_DEFINITIONS "") ++ if (MSVC AND TBB_STATIC) ++ set(TBB_DEFINITIONS __TBB_NO_IMPLICIT_LINKAGE) ++ endif () ++ ++ unset (TBB_STATIC_SUFFIX) ++ ++ find_package_handle_standard_args(TBB ++ REQUIRED_VARS TBB_INCLUDE_DIRS TBB_LIBRARIES ++ FAIL_MESSAGE "TBB library cannot be found. Consider set TBBROOT environment variable." ++ HANDLE_COMPONENTS ++ VERSION_VAR TBB_VERSION) ++ ++ ################################## ++ # Create targets ++ ################################## ++ ++ if(NOT CMAKE_VERSION VERSION_LESS 3.0 AND TBB_FOUND) ++ add_library(TBB::tbb UNKNOWN IMPORTED) ++ set_target_properties(TBB::tbb PROPERTIES ++ INTERFACE_COMPILE_DEFINITIONS "${TBB_DEFINITIONS}" ++ INTERFACE_LINK_LIBRARIES "Threads::Threads;${CMAKE_DL_LIBS}" ++ INTERFACE_INCLUDE_DIRECTORIES ${TBB_INCLUDE_DIRS} ++ IMPORTED_LOCATION ${TBB_LIBRARIES}) ++ if(TBB_LIBRARIES_RELEASE AND TBB_LIBRARIES_DEBUG) ++ set_target_properties(TBB::tbb PROPERTIES ++ INTERFACE_COMPILE_DEFINITIONS "${TBB_DEFINITIONS};$<$,$>:${TBB_DEFINITIONS_DEBUG}>;$<$:${TBB_DEFINITIONS_RELEASE}>" ++ IMPORTED_LOCATION_DEBUG ${TBB_LIBRARIES_DEBUG} ++ IMPORTED_LOCATION_RELWITHDEBINFO ${TBB_LIBRARIES_RELEASE} ++ IMPORTED_LOCATION_RELEASE ${TBB_LIBRARIES_RELEASE} ++ IMPORTED_LOCATION_MINSIZEREL ${TBB_LIBRARIES_RELEASE} ++ ) ++ endif() ++ endif() ++ ++ mark_as_advanced(TBB_INCLUDE_DIRS TBB_LIBRARIES) ++ ++ unset(TBB_ARCHITECTURE) ++ unset(TBB_BUILD_TYPE) ++ unset(TBB_LIB_PATH_SUFFIX) ++ unset(TBB_DEFAULT_SEARCH_DIR) ++ ++ if(TBB_DEBUG) ++ message(STATUS " TBB_FOUND = ${TBB_FOUND}") ++ message(STATUS " TBB_INCLUDE_DIRS = ${TBB_INCLUDE_DIRS}") ++ message(STATUS " TBB_DEFINITIONS = ${TBB_DEFINITIONS}") ++ message(STATUS " TBB_LIBRARIES = ${TBB_LIBRARIES}") ++ message(STATUS " TBB_DEFINITIONS_DEBUG = ${TBB_DEFINITIONS_DEBUG}") ++ message(STATUS " TBB_LIBRARIES_DEBUG = ${TBB_LIBRARIES_DEBUG}") ++ message(STATUS " TBB_DEFINITIONS_RELEASE = ${TBB_DEFINITIONS_RELEASE}") ++ message(STATUS " TBB_LIBRARIES_RELEASE = ${TBB_LIBRARIES_RELEASE}") ++ endif() ++ endif() diff --git a/openvdb/CMakeLists.txt b/openvdb/CMakeLists.txt -index 89301bd..df27aae 100644 +index 89301bd..6a3c90c 100644 --- a/openvdb/CMakeLists.txt +++ b/openvdb/CMakeLists.txt @@ -78,7 +78,7 @@ else() @@ -1664,7 +1775,21 @@ index 89301bd..df27aae 100644 message(DEPRECATION "Support for TBB versions < ${FUTURE_MINIMUM_TBB_VERSION} " "is deprecated and will be removed.") endif() -@@ -185,11 +185,6 @@ if(WIN32) +@@ -129,10 +129,13 @@ endif() + # include paths from shared installs (including houdini) may pull in the wrong + # headers + ++include (CheckAtomic) ++ + set(OPENVDB_CORE_DEPENDENT_LIBS + Boost::iostreams + Boost::system + IlmBase::Half ++ ${CMAKE_REQUIRED_LIBRARIES} + ) + + if(USE_EXR) +@@ -185,11 +188,6 @@ if(WIN32) endif() endif() @@ -1676,7 +1801,7 @@ index 89301bd..df27aae 100644 ##### Core library configuration set(OPENVDB_LIBRARY_SOURCE_FILES -@@ -374,10 +369,16 @@ set(OPENVDB_LIBRARY_UTIL_INCLUDE_FILES +@@ -374,10 +372,16 @@ set(OPENVDB_LIBRARY_UTIL_INCLUDE_FILES if(OPENVDB_CORE_SHARED) add_library(openvdb_shared SHARED ${OPENVDB_LIBRARY_SOURCE_FILES}) @@ -1779,5 +1904,5 @@ index df51830..0ab0c12 100644 /// @todo This changes the compressor setting globally. if (blosc_set_compressor(compname) < 0) continue; -- -2.16.2.windows.1 +2.17.1 diff --git a/resources/icons/hollow.svg b/resources/icons/hollow.svg new file mode 100644 index 0000000000..13e96ada96 --- /dev/null +++ b/resources/icons/hollow.svg @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/icons/hollowing.svg b/resources/icons/hollowing.svg new file mode 100644 index 0000000000..65c7592c83 --- /dev/null +++ b/resources/icons/hollowing.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/icons/white/hollowing.svg b/resources/icons/white/hollowing.svg new file mode 100644 index 0000000000..65c7592c83 --- /dev/null +++ b/resources/icons/white/hollowing.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sandboxes/CMakeLists.txt b/sandboxes/CMakeLists.txt index 91d6ca225e..a2bd13bb0b 100644 --- a/sandboxes/CMakeLists.txt +++ b/sandboxes/CMakeLists.txt @@ -1,3 +1,4 @@ #add_subdirectory(slasupporttree) #add_subdirectory(openvdb) add_subdirectory(meshboolean) +add_subdirectory(opencsg) diff --git a/sandboxes/meshboolean/CMakeLists.txt b/sandboxes/meshboolean/CMakeLists.txt index 17e876573b..55fb42fd14 100644 --- a/sandboxes/meshboolean/CMakeLists.txt +++ b/sandboxes/meshboolean/CMakeLists.txt @@ -1,12 +1,6 @@ -if (SLIC3R_STATIC) - set(CGAL_Boost_USE_STATIC_LIBS ON) -endif () - -find_package(CGAL REQUIRED) - add_executable(meshboolean MeshBoolean.cpp) -target_link_libraries(meshboolean libslic3r CGAL::CGAL) +target_link_libraries(meshboolean libslic3r) if (WIN32) prusaslicer_copy_dlls(meshboolean) diff --git a/sandboxes/meshboolean/MeshBoolean.cpp b/sandboxes/meshboolean/MeshBoolean.cpp index 1aaa4d2b8b..d339ef5c30 100644 --- a/sandboxes/meshboolean/MeshBoolean.cpp +++ b/sandboxes/meshboolean/MeshBoolean.cpp @@ -1,85 +1,76 @@ -#include -#undef PI -#include -//#undef IGL_STATIC_LIBRARY -#include - -#include #include +#include -#include +#include +#include +#include +#include +#include + +#include -#include #include namespace Slic3r { -bool its_write_obj(const Eigen::MatrixXd &V, Eigen::MatrixXi &F, const char *file) -{ - - FILE *fp = boost::nowide::fopen(file, "w"); - if (fp == nullptr) { - BOOST_LOG_TRIVIAL(error) << "stl_write_obj: Couldn't open " << file << " for writing"; - return false; - } - - for (size_t i = 0; i < V.rows(); ++ i) - fprintf(fp, "v %lf %lf %lf\n", V(i, 0), V(i, 1), V(i, 2)); - for (size_t i = 0; i < F.rows(); ++ i) - fprintf(fp, "f %d %d %d\n", F(i, 0) + 1, F(i, 1) + 1, F(i, 2) + 1); - fclose(fp); - return true; -} - -void mesh_boolean_test(const std::string &fname) -{ - using namespace Eigen; - using namespace std; -// igl::readOFF(TUTORIAL_SHARED_PATH "/cheburashka.off",VA,FA); -// igl::readOFF(TUTORIAL_SHARED_PATH "/decimated-knight.off",VB,FB); - // Plot the mesh with pseudocolors -// igl::opengl::glfw::Viewer viewer; - - // Initialize -// update(viewer); - - //igl::copyleft::cgal::mesh_boolean(VA,FA,VB,FB,boolean_type,VC,FC,J); - - - std::cout << fname.c_str() << std::endl; - TriangleMesh mesh; - - mesh.ReadSTLFile(fname.c_str()); - mesh.repair(true); - its_write_obj(mesh.its, (fname + "-imported0.obj").c_str()); - - - Eigen::MatrixXd VA,VB,VC; - Eigen::VectorXi J,I; - Eigen::MatrixXi FA,FB,FC; - igl::MeshBooleanType boolean_type(igl::MESH_BOOLEAN_TYPE_UNION); - - - typedef Eigen::Map> MapMatrixXfUnaligned; - typedef Eigen::Map> MapMatrixXiUnaligned; - - Eigen::MatrixXd V = MapMatrixXfUnaligned(mesh.its.vertices.front().data(), mesh.its.vertices.size(), 3).cast(); - Eigen::MatrixXi F = MapMatrixXiUnaligned(mesh.its.indices.front().data(), mesh.its.indices.size(), 3); - - its_write_obj(V, F, (fname + "-imported.obj").c_str()); - // Self-union to clean up - igl::copyleft::cgal::mesh_boolean(V, F, Eigen::MatrixXd(), Eigen::MatrixXi(), boolean_type, VC, FC); - - its_write_obj(VC, FC, (fname + "-fixed.obj").c_str()); -} - } // namespace Slic3r int main(const int argc, const char * argv[]) { - if (argc < 1) return -1; + using namespace Slic3r; - Slic3r::mesh_boolean_test(argv[1]); + if (argc <= 1) return EXIT_FAILURE; + + DynamicPrintConfig cfg; + auto model = Model::read_from_file(argv[1], &cfg); + + if (model.objects.empty()) return EXIT_SUCCESS; + + SLAPrint print; + print.apply(model, cfg); + PrintBase::TaskParams task; + task.to_object_step = slaposHollowing; + + print.set_task(task); + print.process(); + + Benchmark bench; + + for (SLAPrintObject *po : print.objects()) { + TriangleMesh holes; + sla::DrainHoles holepts = po->transformed_drainhole_points(); + + for (auto &hole: holepts) + holes.merge(sla::to_triangle_mesh(hole.to_mesh())); + + TriangleMesh hollowed_mesh = po->transformed_mesh(); + hollowed_mesh.merge(po->hollowed_interior_mesh()); + + hollowed_mesh.require_shared_vertices(); + holes.require_shared_vertices(); + + TriangleMesh drilled_mesh_igl = hollowed_mesh; + bench.start(); + MeshBoolean::minus(drilled_mesh_igl, holes); + bench.stop(); + + std::cout << "Mesh boolean duration with IGL: " << bench.getElapsedSec() << std::endl; + + TriangleMesh drilled_mesh_cgal = hollowed_mesh; + bench.start(); + MeshBoolean::cgal::self_union(drilled_mesh_cgal); + MeshBoolean::cgal::minus(drilled_mesh_cgal, holes); + bench.stop(); + + std::cout << "Mesh boolean duration with CGAL: " << bench.getElapsedSec() << std::endl; + + std::string name("obj"), outf; + outf = name + "igl" + std::to_string(po->model_object()->id().id) + ".obj"; + drilled_mesh_igl.WriteOBJFile(outf.c_str()); + + outf = name + "cgal" + std::to_string(po->model_object()->id().id) + ".obj"; + drilled_mesh_cgal.WriteOBJFile(outf.c_str()); + } return 0; } diff --git a/sandboxes/opencsg/CMakeLists.txt b/sandboxes/opencsg/CMakeLists.txt new file mode 100644 index 0000000000..ec1f4cae91 --- /dev/null +++ b/sandboxes/opencsg/CMakeLists.txt @@ -0,0 +1,29 @@ +cmake_minimum_required(VERSION 3.0) + +project(OpenCSG-example) + +add_executable(opencsg_example WIN32 + main.cpp + Engine.hpp Engine.cpp + ShaderCSGDisplay.hpp ShaderCSGDisplay.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../src/slic3r/GUI/ProgressStatusBar.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../src/slic3r/GUI/I18N.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../src/slic3r/GUI/I18N.cpp) + +find_package(wxWidgets 3.1 REQUIRED COMPONENTS core base gl html) +find_package(OpenGL REQUIRED) +find_package(GLEW REQUIRED) +find_package(OpenCSG REQUIRED) +include(${wxWidgets_USE_FILE}) + +target_link_libraries(opencsg_example libslic3r) +target_include_directories(opencsg_example PRIVATE ${wxWidgets_INCLUDE_DIRS}) +target_compile_definitions(opencsg_example PRIVATE ${wxWidgets_DEFINITIONS}) + +slic3r_remap_configs(OpenCSG::opencsg RelWithDebInfo Release) +target_link_libraries(opencsg_example ${wxWidgets_LIBRARIES} + OpenCSG::opencsg + GLEW::GLEW + OpenGL::GL + #-lXrandr -lXext -lX11 + ) diff --git a/sandboxes/opencsg/Engine.cpp b/sandboxes/opencsg/Engine.cpp new file mode 100644 index 0000000000..f110b23c5c --- /dev/null +++ b/sandboxes/opencsg/Engine.cpp @@ -0,0 +1,498 @@ +#include "Engine.hpp" +#include +#include + +#include + +#include + +#ifndef NDEBUG +#define HAS_GLSAFE +#endif + +#ifdef HAS_GLSAFE +extern void glAssertRecentCallImpl(const char *file_name, unsigned int line, const char *function_name); +inline void glAssertRecentCall() { glAssertRecentCallImpl(__FILE__, __LINE__, __FUNCTION__); } +#define glsafe(cmd) do { cmd; glAssertRecentCallImpl(__FILE__, __LINE__, __FUNCTION__); } while (false) +#define glcheck() do { glAssertRecentCallImpl(__FILE__, __LINE__, __FUNCTION__); } while (false) + +void glAssertRecentCallImpl(const char *file_name, unsigned int line, const char *function_name) +{ + GLenum err = glGetError(); + if (err == GL_NO_ERROR) + return; + const char *sErr = 0; + switch (err) { + case GL_INVALID_ENUM: sErr = "Invalid Enum"; break; + case GL_INVALID_VALUE: sErr = "Invalid Value"; break; + // be aware that GL_INVALID_OPERATION is generated if glGetError is executed between the execution of glBegin and the corresponding execution of glEnd + case GL_INVALID_OPERATION: sErr = "Invalid Operation"; break; + case GL_STACK_OVERFLOW: sErr = "Stack Overflow"; break; + case GL_STACK_UNDERFLOW: sErr = "Stack Underflow"; break; + case GL_OUT_OF_MEMORY: sErr = "Out Of Memory"; break; + default: sErr = "Unknown"; break; + } + BOOST_LOG_TRIVIAL(error) << "OpenGL error in " << file_name << ":" << line << ", function " << function_name << "() : " << (int)err << " - " << sErr; + assert(false); +} + +#else +inline void glAssertRecentCall() { } +#define glsafe(cmd) cmd +#define glcheck() +#endif + +namespace Slic3r { namespace GL { + +Scene::Scene() = default; +Scene::~Scene() = default; + +void CSGDisplay::render_scene() +{ + GLfloat color[] = {1.f, 1.f, 0.f, 0.f}; + glsafe(::glColor4fv(color)); + + if (m_csgsettings.is_enabled()) { + OpenCSG::render(m_scene_cache.primitives_csg); + glDepthFunc(GL_EQUAL); + } + + for (auto& p : m_scene_cache.primitives_csg) p->render(); + if (m_csgsettings.is_enabled()) glDepthFunc(GL_LESS); + + for (auto& p : m_scene_cache.primitives_free) p->render(); + + glFlush(); +} + +void Scene::set_print(uqptr &&print) +{ + m_print = std::move(print); + + // Notify displays + call(&Listener::on_scene_updated, m_listeners, *this); +} + +BoundingBoxf3 Scene::get_bounding_box() const +{ + return m_print->model().bounding_box(); +} + +void CSGDisplay::SceneCache::clear() +{ + primitives_csg.clear(); + primitives_free.clear(); + primitives.clear(); +} + +shptr CSGDisplay::SceneCache::add_mesh(const TriangleMesh &mesh) +{ + auto p = std::make_shared(); + p->load_mesh(mesh); + primitives.emplace_back(p); + primitives_free.emplace_back(p.get()); + return p; +} + +shptr CSGDisplay::SceneCache::add_mesh(const TriangleMesh &mesh, + OpenCSG::Operation o, + unsigned c) +{ + auto p = std::make_shared(o, c); + p->load_mesh(mesh); + primitives.emplace_back(p); + primitives_csg.emplace_back(p.get()); + return p; +} + +void IndexedVertexArray::push_geometry(float x, float y, float z, float nx, float ny, float nz) +{ + assert(this->vertices_and_normals_interleaved_VBO_id == 0); + if (this->vertices_and_normals_interleaved_VBO_id != 0) + return; + + if (this->vertices_and_normals_interleaved.size() + 6 > this->vertices_and_normals_interleaved.capacity()) + this->vertices_and_normals_interleaved.reserve(next_highest_power_of_2(this->vertices_and_normals_interleaved.size() + 6)); + this->vertices_and_normals_interleaved.emplace_back(nx); + this->vertices_and_normals_interleaved.emplace_back(ny); + this->vertices_and_normals_interleaved.emplace_back(nz); + this->vertices_and_normals_interleaved.emplace_back(x); + this->vertices_and_normals_interleaved.emplace_back(y); + this->vertices_and_normals_interleaved.emplace_back(z); + + this->vertices_and_normals_interleaved_size = this->vertices_and_normals_interleaved.size(); +} + +void IndexedVertexArray::push_triangle(int idx1, int idx2, int idx3) { + assert(this->vertices_and_normals_interleaved_VBO_id == 0); + if (this->vertices_and_normals_interleaved_VBO_id != 0) + return; + + if (this->triangle_indices.size() + 3 > this->vertices_and_normals_interleaved.capacity()) + this->triangle_indices.reserve(next_highest_power_of_2(this->triangle_indices.size() + 3)); + this->triangle_indices.emplace_back(idx1); + this->triangle_indices.emplace_back(idx2); + this->triangle_indices.emplace_back(idx3); + this->triangle_indices_size = this->triangle_indices.size(); +} + +void IndexedVertexArray::load_mesh(const TriangleMesh &mesh) +{ + assert(triangle_indices.empty() && vertices_and_normals_interleaved_size == 0); + assert(quad_indices.empty() && triangle_indices_size == 0); + assert(vertices_and_normals_interleaved.size() % 6 == 0 && quad_indices_size == vertices_and_normals_interleaved.size()); + + this->vertices_and_normals_interleaved.reserve(this->vertices_and_normals_interleaved.size() + 3 * 3 * 2 * mesh.facets_count()); + + int vertices_count = 0; + for (size_t i = 0; i < mesh.stl.stats.number_of_facets; ++i) { + const stl_facet &facet = mesh.stl.facet_start[i]; + for (int j = 0; j < 3; ++j) + this->push_geometry(facet.vertex[j](0), facet.vertex[j](1), facet.vertex[j](2), facet.normal(0), facet.normal(1), facet.normal(2)); + + this->push_triangle(vertices_count, vertices_count + 1, vertices_count + 2); + vertices_count += 3; + } +} + +void IndexedVertexArray::finalize_geometry() +{ + assert(this->vertices_and_normals_interleaved_VBO_id == 0); + assert(this->triangle_indices_VBO_id == 0); + assert(this->quad_indices_VBO_id == 0); + + if (!this->vertices_and_normals_interleaved.empty()) { + glsafe( + ::glGenBuffers(1, &this->vertices_and_normals_interleaved_VBO_id)); + glsafe(::glBindBuffer(GL_ARRAY_BUFFER, + this->vertices_and_normals_interleaved_VBO_id)); + glsafe( + ::glBufferData(GL_ARRAY_BUFFER, + GLsizeiptr( + this->vertices_and_normals_interleaved.size() * + 4), + this->vertices_and_normals_interleaved.data(), + GL_STATIC_DRAW)); + glsafe(::glBindBuffer(GL_ARRAY_BUFFER, 0)); + this->vertices_and_normals_interleaved.clear(); + } + if (!this->triangle_indices.empty()) { + glsafe(::glGenBuffers(1, &this->triangle_indices_VBO_id)); + glsafe(::glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, + this->triangle_indices_VBO_id)); + glsafe(::glBufferData(GL_ELEMENT_ARRAY_BUFFER, + GLsizeiptr(this->triangle_indices.size() * 4), + this->triangle_indices.data(), GL_STATIC_DRAW)); + glsafe(::glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0)); + this->triangle_indices.clear(); + } + if (!this->quad_indices.empty()) { + glsafe(::glGenBuffers(1, &this->quad_indices_VBO_id)); + glsafe(::glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, + this->quad_indices_VBO_id)); + glsafe(::glBufferData(GL_ELEMENT_ARRAY_BUFFER, + GLsizeiptr(this->quad_indices.size() * 4), + this->quad_indices.data(), GL_STATIC_DRAW)); + glsafe(::glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0)); + this->quad_indices.clear(); + } +} + +void IndexedVertexArray::release_geometry() +{ + if (this->vertices_and_normals_interleaved_VBO_id) { + glsafe( + ::glDeleteBuffers(1, + &this->vertices_and_normals_interleaved_VBO_id)); + this->vertices_and_normals_interleaved_VBO_id = 0; + } + if (this->triangle_indices_VBO_id) { + glsafe(::glDeleteBuffers(1, &this->triangle_indices_VBO_id)); + this->triangle_indices_VBO_id = 0; + } + if (this->quad_indices_VBO_id) { + glsafe(::glDeleteBuffers(1, &this->quad_indices_VBO_id)); + this->quad_indices_VBO_id = 0; + } + this->clear(); +} + +void IndexedVertexArray::render() const +{ + assert(this->vertices_and_normals_interleaved_VBO_id != 0); + assert(this->triangle_indices_VBO_id != 0 || + this->quad_indices_VBO_id != 0); + + glsafe(::glBindBuffer(GL_ARRAY_BUFFER, + this->vertices_and_normals_interleaved_VBO_id)); + glsafe(::glVertexPointer(3, GL_FLOAT, 6 * sizeof(float), + reinterpret_cast(3 * sizeof(float)))); + glsafe(::glNormalPointer(GL_FLOAT, 6 * sizeof(float), nullptr)); + + glsafe(::glEnableClientState(GL_VERTEX_ARRAY)); + glsafe(::glEnableClientState(GL_NORMAL_ARRAY)); + + // Render using the Vertex Buffer Objects. + if (this->triangle_indices_size > 0) { + glsafe(::glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, + this->triangle_indices_VBO_id)); + glsafe(::glDrawElements(GL_TRIANGLES, + GLsizei(this->triangle_indices_size), + GL_UNSIGNED_INT, nullptr)); + glsafe(glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0)); + } + if (this->quad_indices_size > 0) { + glsafe(::glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, + this->quad_indices_VBO_id)); + glsafe(::glDrawElements(GL_QUADS, GLsizei(this->quad_indices_size), + GL_UNSIGNED_INT, nullptr)); + glsafe(glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0)); + } + + glsafe(::glDisableClientState(GL_VERTEX_ARRAY)); + glsafe(::glDisableClientState(GL_NORMAL_ARRAY)); + + glsafe(::glBindBuffer(GL_ARRAY_BUFFER, 0)); +} + +void IndexedVertexArray::clear() { + this->vertices_and_normals_interleaved.clear(); + this->triangle_indices.clear(); + this->quad_indices.clear(); + vertices_and_normals_interleaved_size = 0; + triangle_indices_size = 0; + quad_indices_size = 0; +} + +void IndexedVertexArray::shrink_to_fit() { + this->vertices_and_normals_interleaved.shrink_to_fit(); + this->triangle_indices.shrink_to_fit(); + this->quad_indices.shrink_to_fit(); +} + +void Volume::render() +{ + glsafe(::glPushMatrix()); + glsafe(::glMultMatrixd(m_trafo.get_matrix().data())); + m_geom.render(); + glsafe(::glPopMatrix()); +} + +void Display::clear_screen() +{ + glViewport(0, 0, GLsizei(m_size.x()), GLsizei(m_size.y())); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); +} + +Display::~Display() +{ + OpenCSG::freeResources(); +} + +void Display::set_active(long width, long height) +{ + if (!m_initialized) { + glewInit(); + m_initialized = true; + } + + // gray background + glClearColor(0.9f, 0.9f, 0.9f, 1.0f); + + // Enable two OpenGL lights + GLfloat light_diffuse[] = { 1.0f, 1.0f, 0.0f, 1.0f}; // White diffuse light + GLfloat light_position0[] = {-1.0f, -1.0f, -1.0f, 0.0f}; // Infinite light location + GLfloat light_position1[] = { 1.0f, 1.0f, 1.0f, 0.0f}; // Infinite light location + + glLightfv(GL_LIGHT0, GL_DIFFUSE, light_diffuse); + glLightfv(GL_LIGHT0, GL_POSITION, light_position0); + glEnable(GL_LIGHT0); + glLightfv(GL_LIGHT1, GL_DIFFUSE, light_diffuse); + glLightfv(GL_LIGHT1, GL_POSITION, light_position1); + glEnable(GL_LIGHT1); + glEnable(GL_LIGHTING); + glEnable(GL_NORMALIZE); + + // Use depth buffering for hidden surface elimination + glEnable(GL_DEPTH_TEST); + glEnable(GL_STENCIL_TEST); + + set_screen_size(width, height); +} + +void Display::set_screen_size(long width, long height) +{ + if (m_size.x() != width || m_size.y() != height) + m_camera->set_screen(width, height); + + m_size = {width, height}; +} + +void Display::repaint() +{ + clear_screen(); + + m_camera->view(); + render_scene(); + + m_fps_counter.update(); + + swap_buffers(); +} + +void Controller::on_scene_updated(const Scene &scene) +{ + const SLAPrint *print = scene.get_print(); + if (!print) return; + + auto bb = scene.get_bounding_box(); + double d = std::max(std::max(bb.size().x(), bb.size().y()), bb.size().z()); + m_wheel_pos = long(2 * d); + + call_cameras(&Camera::set_zoom, m_wheel_pos); + call(&Display::on_scene_updated, m_displays, scene); +} + +void Controller::on_scroll(long v, long d, MouseInput::WheelAxis /*wa*/) +{ + m_wheel_pos += v / d; + + call_cameras(&Camera::set_zoom, m_wheel_pos); + call(&Display::repaint, m_displays); +} + +void Controller::on_moved_to(long x, long y) +{ + if (m_left_btn) { + call_cameras(&Camera::rotate, (Vec2i{x, y} - m_mouse_pos).cast()); + call(&Display::repaint, m_displays); + } + + m_mouse_pos = {x, y}; +} + +void CSGDisplay::apply_csgsettings(const CSGSettings &settings) +{ + using namespace OpenCSG; + + bool needupdate = m_csgsettings.get_convexity() != settings.get_convexity(); + + m_csgsettings = settings; + setOption(AlgorithmSetting, m_csgsettings.get_algo()); + setOption(DepthComplexitySetting, m_csgsettings.get_depth_algo()); + setOption(DepthBoundsOptimization, m_csgsettings.get_optimization()); + + if (needupdate) { + for (OpenCSG::Primitive * p : m_scene_cache.primitives_csg) + if (p->getConvexity() > 1) + p->setConvexity(m_csgsettings.get_convexity()); + } +} + +void CSGDisplay::on_scene_updated(const Scene &scene) +{ + const SLAPrint *print = scene.get_print(); + if (!print) return; + + m_scene_cache.clear(); + + for (const SLAPrintObject *po : print->objects()) { + const ModelObject *mo = po->model_object(); + TriangleMesh msh = mo->raw_mesh(); + + sla::DrainHoles holedata = mo->sla_drain_holes; + + for (const ModelInstance *mi : mo->instances) { + + TriangleMesh mshinst = msh; + auto interior = po->hollowed_interior_mesh(); + interior.transform(po->trafo().inverse()); + + mshinst.merge(interior); + mshinst.require_shared_vertices(); + + mi->transform_mesh(&mshinst); + + auto bb = mshinst.bounding_box(); + auto center = bb.center().cast(); + mshinst.translate(-center); + + mshinst.require_shared_vertices(); + m_scene_cache.add_mesh(mshinst, OpenCSG::Intersection, + m_csgsettings.get_convexity()); + } + + for (const sla::DrainHole &holept : holedata) { + TriangleMesh holemesh = sla::to_triangle_mesh(holept.to_mesh()); + holemesh.require_shared_vertices(); + m_scene_cache.add_mesh(holemesh, OpenCSG::Subtraction, 1); + } + } + + repaint(); +} + +void Camera::view() +{ + glMatrixMode(GL_MODELVIEW); + glLoadIdentity(); + gluLookAt(0.0, m_zoom, 0.0, /* eye is at (0,zoom,0) */ + m_referene.x(), m_referene.y(), m_referene.z(), + 0.0, 0.0, 1.0); /* up is in positive Y direction */ + + // TODO Could have been set in prevoius gluLookAt in first argument + glRotatef(m_rot.y(), 1.0, 0.0, 0.0); + glRotatef(m_rot.x(), 0.0, 0.0, 1.0); + + if (m_clip_z > 0.) { + GLdouble plane[] = {0., 0., 1., m_clip_z}; + glClipPlane(GL_CLIP_PLANE0, plane); + glEnable(GL_CLIP_PLANE0); + } else { + glDisable(GL_CLIP_PLANE0); + } +} + +void PerspectiveCamera::set_screen(long width, long height) +{ + // Setup the view of the CSG shape + glMatrixMode(GL_PROJECTION); + glLoadIdentity(); + gluPerspective(45.0, width / double(height), .1, 200.0); + glMatrixMode(GL_MODELVIEW); +} + +bool enable_multisampling(bool e) +{ + if (!e) { glDisable(GL_MULTISAMPLE); return false; } + + GLint is_ms_context; + glGetIntegerv(GL_SAMPLE_BUFFERS, &is_ms_context); + + if (is_ms_context) { glEnable(GL_MULTISAMPLE); return true; } + else return false; +} + +MouseInput::Listener::~Listener() = default; + +void FpsCounter::update() +{ + ++m_frames; + + TimePoint msec = Clock::now(); + + double seconds_window = to_sec(msec - m_window); + m_fps = 0.5 * m_fps + 0.5 * (m_frames / seconds_window); + + if (to_sec(msec - m_last) >= m_resolution) { + m_last = msec; + for (auto &l : m_listeners) l(m_fps); + } + + if (seconds_window >= m_window_size) { + m_frames = 0; + m_window = msec; + } +} + +}} // namespace Slic3r::GL diff --git a/sandboxes/opencsg/Engine.hpp b/sandboxes/opencsg/Engine.hpp new file mode 100644 index 0000000000..fc76c1b313 --- /dev/null +++ b/sandboxes/opencsg/Engine.hpp @@ -0,0 +1,493 @@ +#ifndef SLIC3R_OCSG_EXMP_ENGINE_HPP +#define SLIC3R_OCSG_EXMP_ENGINE_HPP + +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace Slic3r { + +class SLAPrint; + +namespace GL { + +// Simple shorthands for smart pointers +template using shptr = std::shared_ptr; +template using uqptr = std::unique_ptr; +template using wkptr = std::weak_ptr; + +template> using vector = std::vector; + +// remove empty weak pointers from a vector +template inline void cleanup(vector> &listeners) { + auto it = std::remove_if(listeners.begin(), listeners.end(), + [](auto &l) { return !l.lock(); }); + listeners.erase(it, listeners.end()); +} + +// Call a class method on each element of a vector of objects (weak pointers) +// of the same type. +template +inline void call(F &&f, vector> &listeners, Args&&... args) { + for (auto &l : listeners) + if (auto p = l.lock()) ((p.get())->*f)(std::forward(args)...); +} + +// A representation of a mouse input for the engine. +class MouseInput +{ +public: + enum WheelAxis { waVertical, waHorizontal }; + + // Interface to implement if an object wants to receive notifications + // about mouse events. + class Listener { + public: + virtual ~Listener(); + + virtual void on_left_click_down() {} + virtual void on_left_click_up() {} + virtual void on_right_click_down() {} + virtual void on_right_click_up() {} + virtual void on_double_click() {} + virtual void on_scroll(long /*v*/, long /*delta*/, WheelAxis ) {} + virtual void on_moved_to(long /*x*/, long /*y*/) {} + }; + +private: + vector> m_listeners; + +public: + virtual ~MouseInput() = default; + + virtual void left_click_down() + { + call(&Listener::on_left_click_down, m_listeners); + } + virtual void left_click_up() + { + call(&Listener::on_left_click_up, m_listeners); + } + virtual void right_click_down() + { + call(&Listener::on_right_click_down, m_listeners); + } + virtual void right_click_up() + { + call(&Listener::on_right_click_up, m_listeners); + } + virtual void double_click() + { + call(&Listener::on_double_click, m_listeners); + } + virtual void scroll(long v, long d, WheelAxis wa) + { + call(&Listener::on_scroll, m_listeners, v, d, wa); + } + virtual void move_to(long x, long y) + { + call(&Listener::on_moved_to, m_listeners, x, y); + } + + void add_listener(shptr listener) + { + m_listeners.emplace_back(listener); + cleanup(m_listeners); + } +}; + +// This is a stripped down version of Slic3r::IndexedVertexArray +class IndexedVertexArray { +public: + ~IndexedVertexArray() { release_geometry(); } + + // Vertices and their normals, interleaved to be used by void + // glInterleavedArrays(GL_N3F_V3F, 0, x) + vector vertices_and_normals_interleaved; + vector triangle_indices; + vector quad_indices; + + // When the geometry data is loaded into the graphics card as Vertex + // Buffer Objects, the above mentioned std::vectors are cleared and the + // following variables keep their original length. + size_t vertices_and_normals_interleaved_size{ 0 }; + size_t triangle_indices_size{ 0 }; + size_t quad_indices_size{ 0 }; + + // IDs of the Vertex Array Objects, into which the geometry has been loaded. + // Zero if the VBOs are not sent to GPU yet. + unsigned int vertices_and_normals_interleaved_VBO_id{ 0 }; + unsigned int triangle_indices_VBO_id{ 0 }; + unsigned int quad_indices_VBO_id{ 0 }; + + + void push_geometry(float x, float y, float z, float nx, float ny, float nz); + + inline void push_geometry( + double x, double y, double z, double nx, double ny, double nz) + { + push_geometry(float(x), float(y), float(z), float(nx), float(ny), float(nz)); + } + + inline void push_geometry(const Vec3d &p, const Vec3d &n) + { + push_geometry(p(0), p(1), p(2), n(0), n(1), n(2)); + } + + void push_triangle(int idx1, int idx2, int idx3); + + void load_mesh(const TriangleMesh &mesh); + + inline bool has_VBOs() const + { + return vertices_and_normals_interleaved_VBO_id != 0; + } + + // Finalize the initialization of the geometry & indices, + // upload the geometry and indices to OpenGL VBO objects + // and shrink the allocated data, possibly relasing it if it has been + // loaded into the VBOs. + void finalize_geometry(); + // Release the geometry data, release OpenGL VBOs. + void release_geometry(); + + void render() const; + + // Is there any geometry data stored? + bool empty() const { return vertices_and_normals_interleaved_size == 0; } + + void clear(); + + // Shrink the internal storage to tighly fit the data stored. + void shrink_to_fit(); +}; + +// Try to enable or disable multisampling. +bool enable_multisampling(bool e = true); + +class Volume { + IndexedVertexArray m_geom; + Geometry::Transformation m_trafo; + +public: + + void render(); + + void translation(const Vec3d &offset) { m_trafo.set_offset(offset); } + void rotation(const Vec3d &rot) { m_trafo.set_rotation(rot); } + void scale(const Vec3d &scaleing) { m_trafo.set_scaling_factor(scaleing); } + void scale(double s) { scale({s, s, s}); } + + inline void load_mesh(const TriangleMesh &mesh) + { + m_geom.load_mesh(mesh); + m_geom.finalize_geometry(); + } +}; + +// A primitive that can be used with OpenCSG rendering algorithms. +// Does a similar job to GLVolume. +class Primitive : public Volume, public OpenCSG::Primitive +{ +public: + using OpenCSG::Primitive::Primitive; + + Primitive() : OpenCSG::Primitive(OpenCSG::Intersection, 1) {} + + void render() override { Volume::render(); } +}; + +// A simple representation of a camera in a 3D scene +class Camera { +protected: + Vec2f m_rot = {0., 0.}; + Vec3d m_referene = {0., 0., 0.}; + double m_zoom = 0.; + double m_clip_z = 0.; +public: + + virtual ~Camera() = default; + + virtual void view(); + virtual void set_screen(long width, long height) = 0; + + void set_rotation(const Vec2f &rotation) { m_rot = rotation; } + void rotate(const Vec2f &rotation) { m_rot += rotation; } + void set_zoom(double z) { m_zoom = z; } + void set_reference_point(const Vec3d &p) { m_referene = p; } + void set_clip_z(double z) { m_clip_z = z; } +}; + +// Reset a camera object +inline void reset(Camera &cam) +{ + cam.set_rotation({0., 0.}); + cam.set_zoom(0.); + cam.set_reference_point({0., 0., 0.}); + cam.set_clip_z(0.); +} + +// Specialization of a camera which shows in perspective projection +class PerspectiveCamera: public Camera { +public: + + void set_screen(long width, long height) override; +}; + +// A simple counter of FPS. Subscribed objects will receive updates of the +// current fps. +class FpsCounter { + vector> m_listeners; + + using Clock = std::chrono::high_resolution_clock; + using Duration = Clock::duration; + using TimePoint = Clock::time_point; + + int m_frames = 0; + TimePoint m_last = Clock::now(), m_window = m_last; + + double m_resolution = 0.1, m_window_size = 1.0; + double m_fps = 0.; + + static double to_sec(Duration d) + { + return d.count() * double(Duration::period::num) / Duration::period::den; + } + +public: + + void update(); + + void add_listener(std::function lst) + { + m_listeners.emplace_back(lst); + } + + void clear_listeners() { m_listeners = {}; } + + void set_notification_interval(double seconds); + void set_measure_window_size(double seconds); + + double get_notification_interval() const { return m_resolution; } + double get_mesure_window_size() const { return m_window_size; } +}; + +// Collection of the used OpenCSG library settings. +class CSGSettings { +public: + static const constexpr unsigned DEFAULT_CONVEXITY = 10; + +private: + OpenCSG::Algorithm m_csgalg = OpenCSG::Algorithm::Automatic; + OpenCSG::DepthComplexityAlgorithm m_depth_algo = OpenCSG::NoDepthComplexitySampling; + OpenCSG::Optimization m_optim = OpenCSG::OptimizationDefault; + bool m_enable = true; + unsigned int m_convexity = DEFAULT_CONVEXITY; + +public: + int get_algo() const { return int(m_csgalg); } + void set_algo(int alg) + { + if (alg < OpenCSG::Algorithm::AlgorithmUnused) + m_csgalg = OpenCSG::Algorithm(alg); + } + + int get_depth_algo() const { return int(m_depth_algo); } + void set_depth_algo(int alg) + { + if (alg < OpenCSG::DepthComplexityAlgorithmUnused) + m_depth_algo = OpenCSG::DepthComplexityAlgorithm(alg); + } + + int get_optimization() const { return int(m_optim); } + void set_optimization(int o) + { + if (o < OpenCSG::Optimization::OptimizationUnused) + m_optim = OpenCSG::Optimization(o); + } + + void enable_csg(bool en = true) { m_enable = en; } + bool is_enabled() const { return m_enable; } + + unsigned get_convexity() const { return m_convexity; } + void set_convexity(unsigned c) { m_convexity = c; } +}; + +// The scene is a wrapper around SLAPrint which holds the data to be visualized. +class Scene +{ + uqptr m_print; +public: + + // Subscribers will be notified if the model is changed. This might be a + // display which will have to load the meshes and repaint itself when + // the scene data changes. + // eg. We load a new 3mf through the UI, this will notify the controller + // associated with the scene and all the displays that the controller is + // connected with. + class Listener { + public: + virtual ~Listener() = default; + virtual void on_scene_updated(const Scene &scene) = 0; + }; + + Scene(); + ~Scene(); + + void set_print(uqptr &&print); + const SLAPrint * get_print() const { return m_print.get(); } + + BoundingBoxf3 get_bounding_box() const; + + void add_listener(shptr listener) + { + m_listeners.emplace_back(listener); + cleanup(m_listeners); + } + +private: + vector> m_listeners; +}; + +// The basic Display. This is almost just an interface but will do all the +// initialization and show the fps values. Overriding the render_scene is +// needed to show the scene content. The specific method of displaying the +// scene is up the the particular implementation (OpenCSG or other screen space +// boolean algorithms) +class Display : public Scene::Listener +{ +protected: + Vec2i m_size; + bool m_initialized = false; + + shptr m_camera; + FpsCounter m_fps_counter; + +public: + + explicit Display(shptr camera = nullptr) + : m_camera(camera ? camera : std::make_shared()) + {} + + ~Display() override; + + shptr get_camera() const { return m_camera; } + shptr get_camera() { return m_camera; } + void set_camera(shptr cam) { m_camera = cam; } + + virtual void swap_buffers() = 0; + virtual void set_active(long width, long height); + virtual void set_screen_size(long width, long height); + Vec2i get_screen_size() const { return m_size; } + + virtual void repaint(); + + bool is_initialized() const { return m_initialized; } + + virtual void clear_screen(); + virtual void render_scene() {} + + template void set_fps_counter(_FpsCounter &&fpsc) + { + m_fps_counter = std::forward<_FpsCounter>(fpsc); + } + + const FpsCounter &get_fps_counter() const { return m_fps_counter; } + FpsCounter &get_fps_counter() { return m_fps_counter; } +}; + +// Special dispaly using OpenCSG for rendering the scene. +class CSGDisplay : public Display { +protected: + CSGSettings m_csgsettings; + + // Cache the renderable primitives. These will be fetched when the scene + // is modified. + struct SceneCache { + vector> primitives; + vector primitives_free; + vector primitives_csg; + + void clear(); + + shptr add_mesh(const TriangleMesh &mesh); + shptr add_mesh(const TriangleMesh &mesh, + OpenCSG::Operation op, + unsigned covexity); + } m_scene_cache; + +public: + + // Receive or apply the new settings. + const CSGSettings & get_csgsettings() const { return m_csgsettings; } + void apply_csgsettings(const CSGSettings &settings); + + void render_scene() override; + + void on_scene_updated(const Scene &scene) override; +}; + + +// The controller is a hub which dispatches mouse events to the connected +// displays. It keeps track of the mouse wheel position, the states whether +// the mouse is being held, dragged, etc... All the connected displays will +// mirror the camera movement (if there is more than one display). +class Controller : public std::enable_shared_from_this, + public MouseInput::Listener, + public Scene::Listener +{ + long m_wheel_pos = 0; + Vec2i m_mouse_pos, m_mouse_pos_rprev, m_mouse_pos_lprev; + bool m_left_btn = false, m_right_btn = false; + + shptr m_scene; + vector> m_displays; + + // Call a method of Camera on all the cameras of the attached displays + template + void call_cameras(F &&f, Args&&... args) { + for (wkptr &l : m_displays) + if (auto disp = l.lock()) if (auto cam = disp->get_camera()) + (cam.get()->*f)(std::forward(args)...); + } + +public: + + // Set the scene that will be controlled. + void set_scene(shptr scene) + { + m_scene = scene; + m_scene->add_listener(shared_from_this()); + } + + const Scene * get_scene() const { return m_scene.get(); } + + void add_display(shptr disp) + { + m_displays.emplace_back(disp); + cleanup(m_displays); + } + + void remove_displays() { m_displays = {}; } + + void on_scene_updated(const Scene &scene) override; + + void on_left_click_down() override { m_left_btn = true; } + void on_left_click_up() override { m_left_btn = false; } + void on_right_click_down() override { m_right_btn = true; } + void on_right_click_up() override { m_right_btn = false; } + + void on_scroll(long v, long d, MouseInput::WheelAxis wa) override; + void on_moved_to(long x, long y) override; + + void move_clip_plane(double z) { call_cameras(&Camera::set_clip_z, z); } +}; + +}} // namespace Slic3r::GL +#endif // SLIC3R_OCSG_EXMP_ENGINE_HPP diff --git a/sandboxes/opencsg/ShaderCSGDisplay.cpp b/sandboxes/opencsg/ShaderCSGDisplay.cpp new file mode 100644 index 0000000000..8ceb234be0 --- /dev/null +++ b/sandboxes/opencsg/ShaderCSGDisplay.cpp @@ -0,0 +1,68 @@ +#include "ShaderCSGDisplay.hpp" +#include "libslic3r/SLAPrint.hpp" +#include + +namespace Slic3r { namespace GL { + +void ShaderCSGDisplay::add_mesh(const TriangleMesh &mesh) +{ + auto v = std::make_shared(); + v->load_mesh(mesh); + m_volumes.emplace_back(v); +} + +void ShaderCSGDisplay::render_scene() +{ + GLfloat color[] = {1.f, 1.f, 0.f, 0.f}; + glColor4fv(color); + glDepthFunc(GL_LESS); + for (auto &v : m_volumes) v->render(); + glFlush(); +} + +void ShaderCSGDisplay::on_scene_updated(const Scene &scene) +{ + // TriangleMesh mesh = print->objects().front()->hollowed_interior_mesh(); + // Look at CSGDisplay::on_scene_updated to see how its done there. + + const SLAPrint *print = scene.get_print(); + if (!print) return; + + m_volumes.clear(); + + for (const SLAPrintObject *po : print->objects()) { + const ModelObject *mo = po->model_object(); + TriangleMesh msh = mo->raw_mesh(); + + sla::DrainHoles holedata = mo->sla_drain_holes; + + for (const ModelInstance *mi : mo->instances) { + + TriangleMesh mshinst = msh; + auto interior = po->hollowed_interior_mesh(); + interior.transform(po->trafo().inverse()); + + mshinst.merge(interior); + mshinst.require_shared_vertices(); + + mi->transform_mesh(&mshinst); + + auto bb = mshinst.bounding_box(); + auto center = bb.center().cast(); + mshinst.translate(-center); + + mshinst.require_shared_vertices(); + add_mesh(mshinst); + } + + for (const sla::DrainHole &holept : holedata) { + TriangleMesh holemesh = sla::to_triangle_mesh(holept.to_mesh()); + holemesh.require_shared_vertices(); + add_mesh(holemesh); + } + } + + repaint(); +} + +}} // namespace Slic3r::GL diff --git a/sandboxes/opencsg/ShaderCSGDisplay.hpp b/sandboxes/opencsg/ShaderCSGDisplay.hpp new file mode 100644 index 0000000000..bf0c3a4240 --- /dev/null +++ b/sandboxes/opencsg/ShaderCSGDisplay.hpp @@ -0,0 +1,27 @@ +#ifndef SHADERCSGDISPLAY_HPP +#define SHADERCSGDISPLAY_HPP + +#include "Engine.hpp" + +namespace Slic3r { namespace GL { + +class CSGVolume: public Volume +{ + // Extend... +}; + +class ShaderCSGDisplay: public Display { +protected: + vector> m_volumes; + + void add_mesh(const TriangleMesh &mesh); +public: + + void render_scene() override; + + void on_scene_updated(const Scene &scene) override; +}; + +}} + +#endif // SHADERCSGDISPLAY_HPP diff --git a/sandboxes/opencsg/main.cpp b/sandboxes/opencsg/main.cpp new file mode 100644 index 0000000000..adf9cc457f --- /dev/null +++ b/sandboxes/opencsg/main.cpp @@ -0,0 +1,734 @@ +#include +#include +#include + +#include "Engine.hpp" +#include "ShaderCSGDisplay.hpp" + +#include + +#include +// For compilers that support precompilation, includes "wx/wx.h". +#include +#ifndef WX_PRECOMP +#include +#endif + +#include +#include +#include +#include +#include +#include +#include + +#include "libslic3r/Model.hpp" +#include "libslic3r/Format/3mf.hpp" +#include "libslic3r/SLAPrint.hpp" + +#include "slic3r/GUI/Job.hpp" +#include "slic3r/GUI/ProgressStatusBar.hpp" + +using namespace Slic3r::GL; + +class Renderer { +protected: + wxGLCanvas *m_canvas; + shptr m_context; +public: + + Renderer(wxGLCanvas *c): m_canvas{c} { + auto ctx = new wxGLContext(m_canvas); + if (!ctx || !ctx->IsOK()) { + wxMessageBox("Could not create OpenGL context.", "Error", + wxOK | wxICON_ERROR); + return; + } + + m_context.reset(ctx); + } + + wxGLContext * context() { return m_context.get(); } + const wxGLContext * context() const { return m_context.get(); } +}; + +// Tell the CSGDisplay how to swap buffers and set the gl context. +class OCSGRenderer: public Renderer, public Slic3r::GL::CSGDisplay { +public: + + OCSGRenderer(wxGLCanvas *c): Renderer{c} {} + + void set_active(long w, long h) override + { + m_canvas->SetCurrent(*m_context); + Slic3r::GL::Display::set_active(w, h); + } + + void swap_buffers() override { m_canvas->SwapBuffers(); } +}; + +// Tell the CSGDisplay how to swap buffers and set the gl context. +class ShaderCSGRenderer : public Renderer, public Slic3r::GL::ShaderCSGDisplay { +public: + + ShaderCSGRenderer(wxGLCanvas *c): Renderer{c} {} + + void set_active(long w, long h) override + { + m_canvas->SetCurrent(*m_context); + Slic3r::GL::Display::set_active(w, h); + } + + void swap_buffers() override { m_canvas->SwapBuffers(); } +}; + +// The opengl rendering facility. Here we implement the rendering objects. +class Canvas: public wxGLCanvas +{ + // One display is active at a time, the OCSGRenderer by default. + shptr m_display; + +public: + + template + Canvas(Args &&...args): wxGLCanvas(std::forward(args)...) {} + + shptr get_display() const { return m_display; } + + void set_display(shptr d) { m_display = d; } +}; + +// Enumerate possible mouse events, we will record them. +enum EEvents { LCLK_U, RCLK_U, LCLK_D, RCLK_D, DDCLK, SCRL, MV }; +struct Event +{ + EEvents type; + long a, b; + Event(EEvents t, long x = 0, long y = 0) : type{t}, a{x}, b{y} {} +}; + +// Create a special mouse input adapter, which can store (record) the received +// mouse signals into a file and play back the stored events later. +class RecorderMouseInput: public MouseInput { + std::vector m_events; + bool m_recording = false, m_playing = false; + +public: + void left_click_down() override + { + if (m_recording) m_events.emplace_back(LCLK_D); + if (!m_playing) MouseInput::left_click_down(); + } + void left_click_up() override + { + if (m_recording) m_events.emplace_back(LCLK_U); + if (!m_playing) MouseInput::left_click_up(); + } + void right_click_down() override + { + if (m_recording) m_events.emplace_back(RCLK_D); + if (!m_playing) MouseInput::right_click_down(); + } + void right_click_up() override + { + if (m_recording) m_events.emplace_back(RCLK_U); + if (!m_playing) MouseInput::right_click_up(); + } + void double_click() override + { + if (m_recording) m_events.emplace_back(DDCLK); + if (!m_playing) MouseInput::double_click(); + } + void scroll(long v, long d, WheelAxis wa) override + { + if (m_recording) m_events.emplace_back(SCRL, v, d); + if (!m_playing) MouseInput::scroll(v, d, wa); + } + void move_to(long x, long y) override + { + if (m_recording) m_events.emplace_back(MV, x, y); + if (!m_playing) MouseInput::move_to(x, y); + } + + void save(std::ostream &stream) + { + for (const Event &evt : m_events) + stream << evt.type << " " << evt.a << " " << evt.b << std::endl; + } + + void load(std::istream &stream) + { + m_events.clear(); + while (stream.good()) { + int type; long a, b; + stream >> type >> a >> b; + m_events.emplace_back(EEvents(type), a, b); + } + } + + void record(bool r) { m_recording = r; if (r) m_events.clear(); } + + void play() + { + m_playing = true; + for (const Event &evt : m_events) { + switch (evt.type) { + case LCLK_U: MouseInput::left_click_up(); break; + case LCLK_D: MouseInput::left_click_down(); break; + case RCLK_U: MouseInput::right_click_up(); break; + case RCLK_D: MouseInput::right_click_down(); break; + case DDCLK: MouseInput::double_click(); break; + case SCRL: MouseInput::scroll(evt.a, evt.b, WheelAxis::waVertical); break; + case MV: MouseInput::move_to(evt.a, evt.b); break; + } + + wxTheApp->Yield(); + if (!m_playing) + break; + } + m_playing = false; + } + + void stop() { m_playing = false; } + bool is_playing() const { return m_playing; } +}; + +// The top level frame of the application. +class MyFrame: public wxFrame +{ + // Instantiate the 3D engine. + shptr m_scene; // Model + shptr m_canvas; // Views store + shptr m_ocsgdisplay; // View + shptr m_shadercsg_display; // Another view + shptr m_ctl; // Controller + + // Add a status bar with progress indication. + shptr m_stbar; + + RecorderMouseInput m_mouse; + + // When loading a Model from 3mf and preparing it, we use a separate thread. + class SLAJob: public Slic3r::GUI::Job { + MyFrame *m_parent; + std::unique_ptr m_print; + std::string m_fname; + + public: + SLAJob(MyFrame *frame, const std::string &fname) + : Slic3r::GUI::Job{frame->m_stbar} + , m_parent{frame} + , m_fname{fname} + {} + + // Runs in separate thread + void process() override; + + const std::string & get_project_fname() const { return m_fname; } + + protected: + + // Runs in the UI thread. + void finalize() override + { + m_parent->m_scene->set_print(std::move(m_print)); + m_parent->m_stbar->set_status_text( + wxString::Format("Model %s loaded.", m_fname)); + } + }; + + uqptr m_ui_job; + + // To keep track of the running average of measured fps values. + double m_fps_avg = 0.; + + // We need the record button across methods + wxToggleButton *m_record_btn; + wxComboBox * m_alg_select; + wxComboBox * m_depth_select; + wxComboBox * m_optimization_select; + wxSpinCtrl * m_convexity_spin; + wxToggleButton *m_csg_toggle; + wxToggleButton *m_ms_toggle; + wxStaticText *m_fpstext; + + CSGSettings m_csg_settings; + + void read_csg_settings(const wxCmdLineParser &parser); + + void set_renderer_algorithm(const wxString &alg); + + void activate_canvas_display(); + +public: + MyFrame(const wxString & title, + const wxPoint & pos, + const wxSize & size, + const wxCmdLineParser &parser); + + // Grab a 3mf and load (hollow it out) within the UI job. + void load_model(const std::string &fname) { + m_ui_job = std::make_unique(this, fname); + m_ui_job->start(); + } + + // Load a previously stored mouse event log and play it back. + void play_back_mouse(const std::string &events_fname) + { + std::fstream stream(events_fname, std::fstream::in); + + if (stream.good()) { + std::string model_name; + std::getline(stream, model_name); + load_model(model_name); + + while (!m_ui_job->is_finalized()) + wxTheApp->Yield();; + + int w, h; + stream >> w >> h; + SetSize(w, h); + + m_mouse.load(stream); + if (m_record_btn) m_record_btn->Disable(); + m_mouse.play(); + } + } + + Canvas * canvas() { return m_canvas.get(); } + const Canvas * canvas() const { return m_canvas.get(); } + + // Bind the canvas mouse events to a class implementing MouseInput interface + void bind_canvas_events(MouseInput &msinput); + + double get_fps_average() const { return m_fps_avg; } +}; + +// Possible OpenCSG configuration values. Will be used on the command line and +// on the UI widgets. +static const std::vector CSG_ALGS = {"Auto", "Goldfeather", "SCS", "EnricoShader"}; +static const std::vector CSG_DEPTH = {"Off", "OcclusionQuery", "On"}; +static const std::vector CSG_OPT = { "Default", "ForceOn", "On", "Off" }; + +inline long get_idx(const wxString &a, const std::vector &v) +{ + auto it = std::find(v.begin(), v.end(), a.ToStdString()); + return it - v.begin(); +}; + +class App : public wxApp { + MyFrame *m_frame = nullptr; + wxString m_fname; +public: + bool OnInit() override { + + wxCmdLineParser parser(argc, argv); + + parser.AddOption("p", "play", "play back file", wxCMD_LINE_VAL_STRING, wxCMD_LINE_PARAM_OPTIONAL); + parser.AddOption("a", "algorithm", "OpenCSG algorithm [Auto|Goldfeather|SCS]", wxCMD_LINE_VAL_STRING, wxCMD_LINE_PARAM_OPTIONAL); + parser.AddOption("d", "depth", "OpenCSG depth strategy [Off|OcclusionQuery|On]", wxCMD_LINE_VAL_STRING, wxCMD_LINE_PARAM_OPTIONAL); + parser.AddOption("o", "optimization", "OpenCSG optimization strategy [Default|ForceOn|On|Off]", wxCMD_LINE_VAL_STRING, wxCMD_LINE_PARAM_OPTIONAL); + parser.AddOption("c", "convexity", "OpenCSG convexity parameter for generic meshes", wxCMD_LINE_VAL_NUMBER, wxCMD_LINE_PARAM_OPTIONAL); + parser.AddSwitch("", "disable-csg", "Disable csg rendering", wxCMD_LINE_PARAM_OPTIONAL); + + parser.Parse(); + + bool is_play = parser.Found("play", &m_fname); + + m_frame = new MyFrame("PrusaSlicer OpenCSG Demo", wxDefaultPosition, wxSize(1024, 768), parser); + + if (is_play) { + Bind(wxEVT_IDLE, &App::Play, this); + m_frame->Show( true ); + } else m_frame->Show( true ); + + return true; + } + + void Play(wxIdleEvent &) { + Unbind(wxEVT_IDLE, &App::Play, this); + m_frame->play_back_mouse(m_fname.ToStdString()); + m_frame->Destroy(); + } +}; + +wxIMPLEMENT_APP(App); + +void MyFrame::read_csg_settings(const wxCmdLineParser &parser) +{ + wxString alg; + parser.Found("algorithm", &alg); + + wxString depth; + parser.Found("depth", &depth); + + wxString opt; + parser.Found("optimization", &opt); + + long convexity = 1; + parser.Found("convexity", &convexity); + + bool csg_off = parser.Found("disable-csg"); + + if (auto a = get_idx(alg, CSG_ALGS) < OpenCSG::AlgorithmUnused) + m_csg_settings.set_algo(OpenCSG::Algorithm(a)); + + if (auto a = get_idx(depth, CSG_DEPTH) < OpenCSG::DepthComplexityAlgorithmUnused) + m_csg_settings.set_depth_algo(OpenCSG::DepthComplexityAlgorithm(a)); + + if (auto a = get_idx(opt, CSG_OPT) < OpenCSG::OptimizationUnused) + m_csg_settings.set_optimization(OpenCSG::Optimization(a)); + + m_csg_settings.set_convexity(unsigned(convexity)); + m_csg_settings.enable_csg(!csg_off); + + if (m_ocsgdisplay) m_ocsgdisplay->apply_csgsettings(m_csg_settings); +} + +void MyFrame::set_renderer_algorithm(const wxString &alg) +{ + long alg_idx = get_idx(alg, CSG_ALGS); + if (alg_idx < 0 || alg_idx >= long(CSG_ALGS.size())) return; + + // If there is a valid display in place, save its camera. + auto cam = m_canvas->get_display() ? + m_canvas->get_display()->get_camera() : nullptr; + + if (alg == "EnricoShader") { + m_alg_select->SetSelection(int(alg_idx)); + m_depth_select->Disable(); + m_optimization_select->Disable(); + m_csg_toggle->Disable(); + + m_ocsgdisplay.reset(); + canvas()->set_display(nullptr); + m_shadercsg_display = std::make_shared(canvas()); + canvas()->set_display(m_shadercsg_display); + } else { + if (m_csg_settings.get_algo() > 0) m_depth_select->Enable(true); + m_alg_select->SetSelection(m_csg_settings.get_algo()); + m_depth_select->SetSelection(m_csg_settings.get_depth_algo()); + m_optimization_select->SetSelection(m_csg_settings.get_optimization()); + m_convexity_spin->SetValue(int(m_csg_settings.get_convexity())); + m_csg_toggle->SetValue(m_csg_settings.is_enabled()); + m_optimization_select->Enable(); + m_csg_toggle->Enable(); + + m_shadercsg_display.reset(); + canvas()->set_display(nullptr); + m_ocsgdisplay = std::make_shared(canvas()); + m_ocsgdisplay->apply_csgsettings(m_csg_settings); + canvas()->set_display(m_ocsgdisplay); + } + + if (cam) + m_canvas->get_display()->set_camera(cam); + + m_ctl->remove_displays(); + m_ctl->add_display(m_canvas->get_display()); + m_canvas->get_display()->get_fps_counter().add_listener([this](double fps) { + m_fpstext->SetLabel(wxString::Format("fps: %.2f", fps)); + m_fps_avg = 0.9 * m_fps_avg + 0.1 * fps; + }); + + if (IsShown()) { + activate_canvas_display(); + m_canvas->get_display()->on_scene_updated(*m_scene); + } +} + +void MyFrame::activate_canvas_display() +{ + const wxSize ClientSize = m_canvas->GetClientSize(); + m_canvas->get_display()->set_active(ClientSize.x, ClientSize.y); + enable_multisampling(m_ms_toggle->GetValue()); + + m_canvas->Bind(wxEVT_PAINT, [this](wxPaintEvent &) { + // This is required even though dc is not used otherwise. + wxPaintDC dc(m_canvas.get()); + const wxSize csize = m_canvas->GetClientSize(); + m_canvas->get_display()->set_screen_size(csize.x, csize.y); + m_canvas->get_display()->repaint(); + }); + + m_canvas->Bind(wxEVT_SIZE, [this](wxSizeEvent &) { + const wxSize csize = m_canvas->GetClientSize(); + m_canvas->get_display()->set_screen_size(csize.x, csize.y); + m_canvas->get_display()->repaint(); + }); + + // Do the repaint continuously + m_canvas->Bind(wxEVT_IDLE, [this](wxIdleEvent &evt) { + m_canvas->get_display()->repaint(); + evt.RequestMore(); + }); + + bind_canvas_events(m_mouse); +} + +MyFrame::MyFrame(const wxString &title, const wxPoint &pos, const wxSize &size, + const wxCmdLineParser &parser): + wxFrame(nullptr, wxID_ANY, title, pos, size) +{ + wxMenu *menuFile = new wxMenu; + menuFile->Append(wxID_OPEN); + menuFile->Append(wxID_EXIT); + wxMenuBar *menuBar = new wxMenuBar; + menuBar->Append( menuFile, "&File" ); + SetMenuBar( menuBar ); + + m_stbar = std::make_shared(this); + m_stbar->embed(this); + + SetStatusText( "Welcome to wxWidgets!" ); + + int attribList[] = + {WX_GL_RGBA, WX_GL_DOUBLEBUFFER, + // RGB channels each should be allocated with 8 bit depth. One + // should almost certainly get these bit depths by default. + WX_GL_MIN_RED, 8, WX_GL_MIN_GREEN, 8, WX_GL_MIN_BLUE, 8, + // Requesting an 8 bit alpha channel. Interestingly, the NVIDIA + // drivers would most likely work with some alpha plane, but + // glReadPixels would not return the alpha channel on NVIDIA if + // not requested when the GL context is created. + WX_GL_MIN_ALPHA, 8, WX_GL_DEPTH_SIZE, 8, WX_GL_STENCIL_SIZE, 8, + WX_GL_SAMPLE_BUFFERS, GL_TRUE, WX_GL_SAMPLES, 4, 0}; + + m_scene = std::make_shared(); + m_ctl = std::make_shared(); + m_ctl->set_scene(m_scene); + + m_canvas = std::make_shared(this, wxID_ANY, attribList, + wxDefaultPosition, wxDefaultSize, + wxWANTS_CHARS | wxFULL_REPAINT_ON_RESIZE); + + read_csg_settings(parser); + + wxPanel *control_panel = new wxPanel(this); + + auto controlsizer = new wxBoxSizer(wxHORIZONTAL); + auto slider_sizer = new wxBoxSizer(wxVERTICAL); + auto console_sizer = new wxBoxSizer(wxVERTICAL); + + auto slider = new wxSlider(control_panel, wxID_ANY, 0, 0, 100, + wxDefaultPosition, wxDefaultSize, + wxSL_VERTICAL); + slider_sizer->Add(slider, 1, wxEXPAND); + + m_ms_toggle = new wxToggleButton(control_panel, wxID_ANY, "Multisampling"); + console_sizer->Add(m_ms_toggle, 0, wxALL | wxEXPAND, 5); + + m_csg_toggle = new wxToggleButton(control_panel, wxID_ANY, "CSG"); + m_csg_toggle->SetValue(true); + console_sizer->Add(m_csg_toggle, 0, wxALL | wxEXPAND, 5); + + auto add_combobox = [control_panel, console_sizer] + (const wxString &label, const std::vector &list) + { + auto widget = new wxComboBox(control_panel, wxID_ANY, list[0], + wxDefaultPosition, wxDefaultSize, + int(list.size()), list.data()); + + auto sz = new wxBoxSizer(wxHORIZONTAL); + sz->Add(new wxStaticText(control_panel, wxID_ANY, label), 0, + wxALL | wxALIGN_CENTER, 5); + sz->Add(widget, 1, wxALL | wxEXPAND, 5); + console_sizer->Add(sz, 0, wxEXPAND); + return widget; + }; + + auto add_spinctl = [control_panel, console_sizer] + (const wxString &label, int initial, int min, int max) + { + auto widget = new wxSpinCtrl( + control_panel, wxID_ANY, + wxString::Format("%d", initial), + wxDefaultPosition, wxDefaultSize, wxSP_ARROW_KEYS, min, max, + initial); + + auto sz = new wxBoxSizer(wxHORIZONTAL); + sz->Add(new wxStaticText(control_panel, wxID_ANY, label), 0, + wxALL | wxALIGN_CENTER, 5); + sz->Add(widget, 1, wxALL | wxEXPAND, 5); + console_sizer->Add(sz, 0, wxEXPAND); + return widget; + }; + + m_convexity_spin = add_spinctl("Convexity", CSGSettings::DEFAULT_CONVEXITY, 0, 100); + + m_alg_select = add_combobox("Algorithm", CSG_ALGS); + m_depth_select = add_combobox("Depth Complexity", CSG_DEPTH); + m_optimization_select = add_combobox("Optimization", CSG_OPT); + + m_fpstext = new wxStaticText(control_panel, wxID_ANY, ""); + console_sizer->Add(m_fpstext, 0, wxALL, 5); + + m_record_btn = new wxToggleButton(control_panel, wxID_ANY, "Record"); + console_sizer->Add(m_record_btn, 0, wxALL | wxEXPAND, 5); + + controlsizer->Add(slider_sizer, 0, wxEXPAND); + controlsizer->Add(console_sizer, 1, wxEXPAND); + + control_panel->SetSizer(controlsizer); + + auto sizer = new wxBoxSizer(wxHORIZONTAL); + sizer->Add(m_canvas.get(), 1, wxEXPAND); + sizer->Add(control_panel, 0, wxEXPAND); + SetSizer(sizer); + + wxString alg; + if (!parser.Found("algorithm", &alg)) alg = "Auto"; + + set_renderer_algorithm(alg); + + Bind(wxEVT_CLOSE_WINDOW, [this](wxCloseEvent &evt){ + if (m_canvas) RemoveChild(m_canvas.get()); + m_canvas.reset(); + if (!m_mouse.is_playing()) evt.Skip(); + else m_mouse.stop(); + }); + + Bind(wxEVT_MENU, [this](wxCommandEvent &) { + wxFileDialog dlg(this, "Select project file", wxEmptyString, + wxEmptyString, "*.3mf", wxFD_OPEN|wxFD_FILE_MUST_EXIST); + + if (dlg.ShowModal() == wxID_OK) load_model(dlg.GetPath().ToStdString()); + }, wxID_OPEN); + + Bind(wxEVT_MENU, [this](wxCommandEvent &) { Close(true); }, wxID_EXIT); + + Bind(wxEVT_SHOW, [this](wxShowEvent &) { + activate_canvas_display(); + }); + + Bind(wxEVT_SLIDER, [this, slider](wxCommandEvent &) { + m_ctl->move_clip_plane(double(slider->GetValue())); + }); + + m_ms_toggle->Bind(wxEVT_TOGGLEBUTTON, [this](wxCommandEvent &){ + enable_multisampling(m_ms_toggle->GetValue()); + m_canvas->get_display()->repaint(); + }); + + m_csg_toggle->Bind(wxEVT_TOGGLEBUTTON, [this](wxCommandEvent &){ + CSGSettings stt = m_ocsgdisplay->get_csgsettings(); + stt.enable_csg(m_csg_toggle->GetValue()); + m_ocsgdisplay->apply_csgsettings(stt); + }); + + m_alg_select->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent &) { + wxString alg = m_alg_select->GetValue(); + int sel = m_alg_select->GetSelection(); + m_csg_settings.set_algo(sel); + set_renderer_algorithm(alg); + }); + + m_depth_select->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent &) { + int sel = m_depth_select->GetSelection(); + m_csg_settings.set_depth_algo(sel); + if (m_ocsgdisplay) m_ocsgdisplay->apply_csgsettings(m_csg_settings); + }); + + m_optimization_select->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent &) { + int sel = m_optimization_select->GetSelection(); + m_csg_settings.set_optimization(sel); + if (m_ocsgdisplay) m_ocsgdisplay->apply_csgsettings(m_csg_settings); + }); + + m_convexity_spin->Bind(wxEVT_SPINCTRL, [this](wxSpinEvent &) { + int c = m_convexity_spin->GetValue(); + if (c > 0) { + m_csg_settings.set_convexity(unsigned(c)); + if (m_ocsgdisplay) m_ocsgdisplay->apply_csgsettings(m_csg_settings); + } + }); + + m_record_btn->Bind(wxEVT_TOGGLEBUTTON, [this](wxCommandEvent &) { + if (!m_ui_job) { + m_stbar->set_status_text("No project loaded!"); + return; + } + + if (m_record_btn->GetValue()) { + if (auto c = m_canvas->get_display()->get_camera()) reset(*c); + m_ctl->on_scene_updated(*m_scene); + m_mouse.record(true); + } else { + m_mouse.record(false); + wxFileDialog dlg(this, "Select output file", + wxEmptyString, wxEmptyString, "*.events", + wxFD_SAVE|wxFD_OVERWRITE_PROMPT); + + if (dlg.ShowModal() == wxID_OK) { + std::fstream stream(dlg.GetPath().ToStdString(), + std::fstream::out); + + if (stream.good()) { + stream << m_ui_job->get_project_fname() << "\n"; + wxSize winsize = GetSize(); + stream << winsize.x << " " << winsize.y << "\n"; + m_mouse.save(stream); + } + } + } + }); +} + +void MyFrame::bind_canvas_events(MouseInput &ms) +{ + m_canvas->Bind(wxEVT_MOUSEWHEEL, [&ms](wxMouseEvent &evt) { + ms.scroll(evt.GetWheelRotation(), evt.GetWheelDelta(), + evt.GetWheelAxis() == wxMOUSE_WHEEL_VERTICAL ? + Slic3r::GL::MouseInput::waVertical : + Slic3r::GL::MouseInput::waHorizontal); + }); + + m_canvas->Bind(wxEVT_MOTION, [&ms](wxMouseEvent &evt) { + ms.move_to(evt.GetPosition().x, evt.GetPosition().y); + }); + + m_canvas->Bind(wxEVT_RIGHT_DOWN, [&ms](wxMouseEvent & /*evt*/) { + ms.right_click_down(); + }); + + m_canvas->Bind(wxEVT_RIGHT_UP, [&ms](wxMouseEvent & /*evt*/) { + ms.right_click_up(); + }); + + m_canvas->Bind(wxEVT_LEFT_DOWN, [&ms](wxMouseEvent & /*evt*/) { + ms.left_click_down(); + }); + + m_canvas->Bind(wxEVT_LEFT_UP, [&ms](wxMouseEvent & /*evt*/) { + ms.left_click_up(); + }); + + ms.add_listener(m_ctl); +} + +void MyFrame::SLAJob::process() +{ + using SlStatus = Slic3r::PrintBase::SlicingStatus; + + Slic3r::DynamicPrintConfig cfg; + auto model = Slic3r::Model::read_from_file(m_fname, &cfg); + + m_print = std::make_unique(); + m_print->apply(model, cfg); + + Slic3r::PrintBase::TaskParams params; + params.to_object_step = Slic3r::slaposHollowing; + m_print->set_task(params); + + m_print->set_status_callback([this](const SlStatus &status) { + update_status(status.percent, status.text); + }); + + try { + m_print->process(); + } catch(std::exception &e) { + update_status(0, wxString("Exception during processing: ") + e.what()); + } +} + +//int main() {} diff --git a/sandboxes/openvdb/CMakeLists.txt b/sandboxes/openvdb/CMakeLists.txt index 184452e833..c32d6c8d63 100644 --- a/sandboxes/openvdb/CMakeLists.txt +++ b/sandboxes/openvdb/CMakeLists.txt @@ -1,2 +1,7 @@ -add_executable(openvdb_example openvdb_example.cpp) -target_link_libraries(openvdb_example libslic3r) +if(TARGET OpenVDB::openvdb) + add_executable(openvdb_example openvdb_example.cpp) + + target_link_libraries(openvdb_example libslic3r) + target_link_libraries(openvdb_example OpenVDB::openvdb) +endif() + diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index 1c110d7cdc..6d5b3e5f56 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -9,6 +9,11 @@ if (MINGW) add_compile_options(-Wa,-mbig-obj) endif () +set(OpenVDBUtils_SOURCES "") +if (TARGET OpenVDB::openvdb) + set(OpenVDBUtils_SOURCES OpenVDBUtils.cpp OpenVDBUtils.hpp) +endif() + add_library(libslic3r STATIC pchheader.cpp pchheader.hpp @@ -149,9 +154,9 @@ add_library(libslic3r STATIC ShortestPath.cpp ShortestPath.hpp SLAPrint.cpp + SLAPrintSteps.cpp + SLAPrintSteps.hpp SLAPrint.hpp - SLA/SLAAutoSupports.hpp - SLA/SLAAutoSupports.cpp Slicing.cpp Slicing.hpp SlicingAdaptive.cpp @@ -180,35 +185,65 @@ add_library(libslic3r STATIC MinAreaBoundingBox.cpp miniz_extension.hpp miniz_extension.cpp - SLA/SLACommon.hpp - SLA/SLABoilerPlate.hpp - SLA/SLAPad.hpp - SLA/SLAPad.cpp - SLA/SLASupportTreeBuilder.hpp - SLA/SLASupportTreeBuildsteps.hpp - SLA/SLASupportTreeBuildsteps.cpp - SLA/SLASupportTreeBuilder.cpp - SLA/SLAConcurrency.hpp - SLA/SLASupportTree.hpp - SLA/SLASupportTree.cpp - SLA/SLASupportTreeIGL.cpp - SLA/SLARotfinder.hpp - SLA/SLARotfinder.cpp - SLA/SLABoostAdapter.hpp - SLA/SLASpatIndex.hpp - SLA/SLARaster.hpp - SLA/SLARaster.cpp - SLA/SLARasterWriter.hpp - SLA/SLARasterWriter.cpp + SimplifyMesh.hpp + SimplifyMeshImpl.hpp + SimplifyMesh.cpp + ${OpenVDBUtils_SOURCES} + SLA/Common.hpp + SLA/Common.cpp + SLA/Pad.hpp + SLA/Pad.cpp + SLA/SupportTreeBuilder.hpp + SLA/SupportTreeBuildsteps.hpp + SLA/SupportTreeBuildsteps.cpp + SLA/SupportTreeBuilder.cpp + SLA/Concurrency.hpp + SLA/SupportTree.hpp + SLA/SupportTree.cpp +# SLA/SupportTreeIGL.cpp + SLA/Rotfinder.hpp + SLA/Rotfinder.cpp + SLA/BoostAdapter.hpp + SLA/SpatIndex.hpp + SLA/Raster.hpp + SLA/Raster.cpp + SLA/RasterWriter.hpp + SLA/RasterWriter.cpp SLA/ConcaveHull.hpp SLA/ConcaveHull.cpp + SLA/Hollowing.hpp + SLA/Hollowing.cpp + SLA/JobController.hpp + SLA/SupportPoint.hpp + SLA/SupportPointGenerator.hpp + SLA/SupportPointGenerator.cpp + SLA/Contour3D.hpp + SLA/Contour3D.cpp + SLA/EigenMesh3D.hpp + SLA/Clustering.hpp ) -encoding_check(libslic3r) - -if (SLIC3R_PCH AND NOT SLIC3R_SYNTAXONLY) - add_precompiled_header(libslic3r pchheader.hpp FORCEINCLUDE) +if (SLIC3R_STATIC) + set(CGAL_Boost_USE_STATIC_LIBS ON CACHE BOOL "" FORCE) endif () +set(CGAL_DO_NOT_WARN_ABOUT_CMAKE_BUILD_TYPE ON CACHE BOOL "" FORCE) + +cmake_policy(PUSH) +cmake_policy(SET CMP0011 NEW) +find_package(CGAL REQUIRED) +cmake_policy(POP) + +add_library(libslic3r_cgal OBJECT MeshBoolean.cpp MeshBoolean.hpp) +target_include_directories(libslic3r_cgal PRIVATE + ${CMAKE_CURRENT_BINARY_DIR} + $ + $) +target_compile_definitions(libslic3r_cgal PRIVATE + $) +target_compile_options(libslic3r_cgal PRIVATE + $) + +encoding_check(libslic3r) target_compile_definitions(libslic3r PUBLIC -DUSE_TBB -DTBB_USE_CAPTURED_EXCEPTION=0) target_include_directories(libslic3r PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} ${LIBNEST2D_INCLUDES} PUBLIC ${CMAKE_CURRENT_BINARY_DIR}) @@ -228,10 +263,14 @@ target_link_libraries(libslic3r qhull semver TBB::tbb - # OpenVDB::openvdb + $ ${CMAKE_DL_LIBS} ) +if (TARGET OpenVDB::openvdb) + target_link_libraries(libslic3r OpenVDB::openvdb) +endif() + if(WIN32) target_link_libraries(libslic3r Psapi.lib) endif() @@ -239,3 +278,9 @@ endif() if(SLIC3R_PROFILE) target_link_libraries(slic3r Shiny) endif() + +if (SLIC3R_PCH AND NOT SLIC3R_SYNTAXONLY) + add_precompiled_header(libslic3r pchheader.hpp FORCEINCLUDE) +endif () + +target_sources(libslic3r PRIVATE $) diff --git a/src/libslic3r/Format/3mf.cpp b/src/libslic3r/Format/3mf.cpp index 8acab22e00..45f39c7873 100644 --- a/src/libslic3r/Format/3mf.cpp +++ b/src/libslic3r/Format/3mf.cpp @@ -55,6 +55,7 @@ const std::string MODEL_CONFIG_FILE = "Metadata/Slic3r_PE_model.config"; const std::string LAYER_HEIGHTS_PROFILE_FILE = "Metadata/Slic3r_PE_layer_heights_profile.txt"; const std::string LAYER_CONFIG_RANGES_FILE = "Metadata/Prusa_Slicer_layer_config_ranges.xml"; const std::string SLA_SUPPORT_POINTS_FILE = "Metadata/Slic3r_PE_sla_support_points.txt"; +const std::string SLA_DRAIN_HOLES_FILE = "Metadata/Slic3r_PE_sla_drain_holes.txt"; const std::string CUSTOM_GCODE_PER_PRINT_Z_FILE = "Metadata/Prusa_Slicer_custom_gcode_per_print_z.xml"; const char* MODEL_TAG = "model"; @@ -385,6 +386,7 @@ namespace Slic3r { typedef std::map> IdToLayerHeightsProfileMap; typedef std::map IdToLayerConfigRangesMap; typedef std::map> IdToSlaSupportPointsMap; + typedef std::map> IdToSlaDrainHolesMap; // Version of the 3mf file unsigned int m_version; @@ -403,6 +405,7 @@ namespace Slic3r { IdToLayerHeightsProfileMap m_layer_heights_profiles; IdToLayerConfigRangesMap m_layer_config_ranges; IdToSlaSupportPointsMap m_sla_support_points; + IdToSlaDrainHolesMap m_sla_drain_holes; std::string m_curr_metadata_name; std::string m_curr_characters; std::string m_name; @@ -422,6 +425,7 @@ namespace Slic3r { void _extract_layer_heights_profile_config_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat); void _extract_layer_config_ranges_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat); void _extract_sla_support_points_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat); + void _extract_sla_drain_holes_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat); void _extract_custom_gcode_per_print_z_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat); @@ -629,6 +633,11 @@ namespace Slic3r { // extract sla support points file _extract_sla_support_points_from_archive(archive, stat); } + else if (boost::algorithm::iequals(name, SLA_DRAIN_HOLES_FILE)) + { + // extract sla support points file + _extract_sla_drain_holes_from_archive(archive, stat); + } else if (boost::algorithm::iequals(name, PRINT_CONFIG_FILE)) { // extract slic3r print config file @@ -683,6 +692,11 @@ namespace Slic3r { model_object->sla_support_points = obj_sla_support_points->second; model_object->sla_points_status = sla::PointsStatus::UserModified; } + + IdToSlaDrainHolesMap::iterator obj_drain_holes = m_sla_drain_holes.find(object.second + 1); + if (obj_drain_holes != m_sla_drain_holes.end() && !obj_drain_holes->second.empty()) { + model_object->sla_drain_holes = obj_drain_holes->second; + } IdToMetadataMap::iterator obj_metadata = m_objects_metadata.find(object.first); if (obj_metadata != m_objects_metadata.end()) @@ -955,8 +969,9 @@ namespace Slic3r { // Info on format versioning - see 3mf.hpp int version = 0; - if (!objects.empty() && objects[0].find("support_points_format_version=") != std::string::npos) { - objects[0].erase(objects[0].begin(), objects[0].begin() + 30); // removes the string + std::string key("support_points_format_version="); + if (!objects.empty() && objects[0].find(key) != std::string::npos) { + objects[0].erase(objects[0].begin(), objects[0].begin() + long(key.size())); // removes the string version = std::stoi(objects[0]); objects.erase(objects.begin()); // pop the header } @@ -1022,6 +1037,90 @@ namespace Slic3r { } } } + + void _3MF_Importer::_extract_sla_drain_holes_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat) + { + if (stat.m_uncomp_size > 0) + { + std::string buffer(size_t(stat.m_uncomp_size), 0); + mz_bool res = mz_zip_reader_extract_file_to_mem(&archive, stat.m_filename, (void*)buffer.data(), (size_t)stat.m_uncomp_size, 0); + if (res == 0) + { + add_error("Error while reading sla support points data to buffer"); + return; + } + + if (buffer.back() == '\n') + buffer.pop_back(); + + std::vector objects; + boost::split(objects, buffer, boost::is_any_of("\n"), boost::token_compress_off); + + // Info on format versioning - see 3mf.hpp + int version = 0; + std::string key("drain_holes_format_version="); + if (!objects.empty() && objects[0].find(key) != std::string::npos) { + objects[0].erase(objects[0].begin(), objects[0].begin() + long(key.size())); // removes the string + version = std::stoi(objects[0]); + objects.erase(objects.begin()); // pop the header + } + + for (const std::string& object : objects) + { + std::vector object_data; + boost::split(object_data, object, boost::is_any_of("|"), boost::token_compress_off); + + if (object_data.size() != 2) + { + add_error("Error while reading object data"); + continue; + } + + std::vector object_data_id; + boost::split(object_data_id, object_data[0], boost::is_any_of("="), boost::token_compress_off); + if (object_data_id.size() != 2) + { + add_error("Error while reading object id"); + continue; + } + + int object_id = std::atoi(object_data_id[1].c_str()); + if (object_id == 0) + { + add_error("Found invalid object id"); + continue; + } + + IdToSlaDrainHolesMap::iterator object_item = m_sla_drain_holes.find(object_id); + if (object_item != m_sla_drain_holes.end()) + { + add_error("Found duplicated SLA drain holes"); + continue; + } + + std::vector object_data_points; + boost::split(object_data_points, object_data[1], boost::is_any_of(" "), boost::token_compress_off); + + sla::DrainHoles sla_drain_holes; + + if (version == 1) { + for (unsigned int i=0; isla_drain_holes; + if (!drain_holes.empty()) + { + out += string_printf(fmt, count); + + // Store the layer height profile as a single space separated list. + for (size_t i = 0; i < drain_holes.size(); ++i) + out += string_printf((i == 0 ? "%f %f %f %f %f %f %f %f" : " %f %f %f %f %f %f %f %f"), + drain_holes[i].pos(0), + drain_holes[i].pos(1), + drain_holes[i].pos(2), + drain_holes[i].normal(0), + drain_holes[i].normal(1), + drain_holes[i].normal(2), + drain_holes[i].radius, + drain_holes[i].height); + + out += "\n"; + } + } + + if (!out.empty()) + { + // Adds version header at the beginning: + out = std::string("drain_holes_format_version=") + std::to_string(drain_holes_format_version) + std::string("\n") + out; + + if (!mz_zip_writer_add_mem(&archive, SLA_DRAIN_HOLES_FILE.c_str(), static_cast(out.data()), out.length(), mz_uint(MZ_DEFAULT_COMPRESSION))) + { + add_error("Unable to add sla support points file to archive"); + return false; + } + } + return true; + } bool _3MF_Exporter::_add_print_config_file_to_archive(mz_zip_archive& archive, const DynamicPrintConfig &config) { diff --git a/src/libslic3r/Format/3mf.hpp b/src/libslic3r/Format/3mf.hpp index c28716fffc..79ac61a234 100644 --- a/src/libslic3r/Format/3mf.hpp +++ b/src/libslic3r/Format/3mf.hpp @@ -19,6 +19,10 @@ namespace Slic3r { enum { support_points_format_version = 1 }; + + enum { + drain_holes_format_version = 1 + }; class Model; class DynamicPrintConfig; diff --git a/src/libslic3r/Format/objparser.cpp b/src/libslic3r/Format/objparser.cpp index 28f2fdbbd5..8c1a53459d 100644 --- a/src/libslic3r/Format/objparser.cpp +++ b/src/libslic3r/Format/objparser.cpp @@ -355,6 +355,35 @@ bool objparse(const char *path, ObjData &data) return true; } +bool objparse(std::istream &stream, ObjData &data) +{ + try { + char buf[65536 * 2]; + size_t len = 0; + size_t lenPrev = 0; + while ((len = size_t(stream.read(buf + lenPrev, 65536).gcount())) != 0) { + len += lenPrev; + size_t lastLine = 0; + for (size_t i = 0; i < len; ++ i) + if (buf[i] == '\r' || buf[i] == '\n') { + buf[i] = 0; + char *c = buf + lastLine; + while (*c == ' ' || *c == '\t') + ++ c; + obj_parseline(c, data); + lastLine = i + 1; + } + lenPrev = len - lastLine; + memmove(buf, buf + lastLine, lenPrev); + } + } + catch (std::bad_alloc&) { + printf("Out of memory\r\n"); + } + + return true; +} + template bool savevector(FILE *pFile, const std::vector &v) { diff --git a/src/libslic3r/Format/objparser.hpp b/src/libslic3r/Format/objparser.hpp index 5fc25e297b..5f3f010e4a 100644 --- a/src/libslic3r/Format/objparser.hpp +++ b/src/libslic3r/Format/objparser.hpp @@ -3,6 +3,7 @@ #include #include +#include namespace ObjParser { @@ -97,6 +98,7 @@ struct ObjData { }; extern bool objparse(const char *path, ObjData &data); +extern bool objparse(std::istream &stream, ObjData &data); extern bool objbinsave(const char *path, const ObjData &data); diff --git a/src/libslic3r/MeshBoolean.cpp b/src/libslic3r/MeshBoolean.cpp new file mode 100644 index 0000000000..69db96d3fc --- /dev/null +++ b/src/libslic3r/MeshBoolean.cpp @@ -0,0 +1,170 @@ +#include "MeshBoolean.hpp" +#include "libslic3r/TriangleMesh.hpp" +#undef PI + +// Include igl first. It defines "L" macro which then clashes with our localization +#include +#undef L + +// CGAL headers +#include +#include +#include + +namespace Slic3r { +namespace MeshBoolean { + +typedef Eigen::Map> MapMatrixXfUnaligned; +typedef Eigen::Map> MapMatrixXiUnaligned; + +typedef std::pair EigenMesh; + +static TriangleMesh eigen_to_triangle_mesh(const Eigen::MatrixXd& VC, const Eigen::MatrixXi& FC) +{ + Pointf3s points(size_t(VC.rows())); + std::vector facets(size_t(FC.rows())); + + for (Eigen::Index i = 0; i < VC.rows(); ++i) + points[size_t(i)] = VC.row(i); + + for (Eigen::Index i = 0; i < FC.rows(); ++i) + facets[size_t(i)] = FC.row(i); + + TriangleMesh out{points, facets}; + out.require_shared_vertices(); + return out; +} + +static EigenMesh triangle_mesh_to_eigen_mesh(const TriangleMesh &mesh) +{ + EigenMesh emesh; + emesh.first = MapMatrixXfUnaligned(mesh.its.vertices.front().data(), + Eigen::Index(mesh.its.vertices.size()), + 3).cast(); + + emesh.second = MapMatrixXiUnaligned(mesh.its.indices.front().data(), + Eigen::Index(mesh.its.indices.size()), + 3); + return emesh; +} + +void minus(TriangleMesh& A, const TriangleMesh& B) +{ + auto [VA, FA] = triangle_mesh_to_eigen_mesh(A); + auto [VB, FB] = triangle_mesh_to_eigen_mesh(B); + + Eigen::MatrixXd VC; + Eigen::MatrixXi FC; + igl::MeshBooleanType boolean_type(igl::MESH_BOOLEAN_TYPE_MINUS); + igl::copyleft::cgal::mesh_boolean(VA, FA, VB, FB, boolean_type, VC, FC); + + A = eigen_to_triangle_mesh(VC, FC); +} + +void self_union(TriangleMesh& mesh) +{ + auto [V, F] = triangle_mesh_to_eigen_mesh(mesh); + + Eigen::MatrixXd VC; + Eigen::MatrixXi FC; + + igl::MeshBooleanType boolean_type(igl::MESH_BOOLEAN_TYPE_UNION); + igl::copyleft::cgal::mesh_boolean(V, F, Eigen::MatrixXd(), Eigen::MatrixXi(), boolean_type, VC, FC); + + mesh = eigen_to_triangle_mesh(VC, FC); +} + +namespace cgal { + +namespace CGALProc = CGAL::Polygon_mesh_processing; +namespace CGALParams = CGAL::Polygon_mesh_processing::parameters; + +using Kernel = CGAL::Exact_predicates_inexact_constructions_kernel; +using _CGALMesh = CGAL::Surface_mesh; + +struct CGALMesh { _CGALMesh m; }; + +static void triangle_mesh_to_cgal(const TriangleMesh &M, _CGALMesh &out) +{ + for (const Vec3f &v : M.its.vertices) + out.add_vertex(_CGALMesh::Point(v.x(), v.y(), v.z())); + + for (const Vec3crd &face : M.its.indices) { + auto f = face.cast(); + out.add_face(f(0), f(1), f(2)); + } +} + +static TriangleMesh cgal_to_triangle_mesh(const _CGALMesh &cgalmesh) +{ + Pointf3s points; + std::vector facets; + points.reserve(cgalmesh.num_vertices()); + facets.reserve(cgalmesh.num_faces()); + + for (auto &vi : cgalmesh.vertices()) { + auto &v = cgalmesh.point(vi); // Don't ask... + points.emplace_back(v.x(), v.y(), v.z()); + } + + for (auto &face : cgalmesh.faces()) { + auto vtc = cgalmesh.vertices_around_face(cgalmesh.halfedge(face)); + int i = 0; + Vec3crd trface; + for (auto v : vtc) trface(i++) = int(v.idx()); + facets.emplace_back(trface); + } + + TriangleMesh out{points, facets}; + out.require_shared_vertices(); + return out; +} + +std::unique_ptr triangle_mesh_to_cgal(const TriangleMesh &M) +{ + auto out = std::make_unique(); + triangle_mesh_to_cgal(M, out->m); + return out; +} + +void cgal_to_triangle_mesh(const CGALMesh &cgalmesh, TriangleMesh &out) +{ + out = cgal_to_triangle_mesh(cgalmesh.m); +} + +void minus(CGALMesh &A, CGALMesh &B) +{ + CGALProc::corefine_and_compute_difference(A.m, B.m, A.m); +} + +void self_union(CGALMesh &A) +{ + CGALProc::corefine(A.m, A.m); +} + +void minus(TriangleMesh &A, const TriangleMesh &B) +{ + CGALMesh meshA; + CGALMesh meshB; + triangle_mesh_to_cgal(A, meshA.m); + triangle_mesh_to_cgal(B, meshB.m); + + CGALMesh meshResult; + CGALProc::corefine_and_compute_difference(meshA.m, meshB.m, meshResult.m); + + A = cgal_to_triangle_mesh(meshResult.m); +} + +void self_union(TriangleMesh &m) +{ + _CGALMesh cgalmesh; + triangle_mesh_to_cgal(m, cgalmesh); + CGALProc::corefine(cgalmesh, cgalmesh); + + m = cgal_to_triangle_mesh(cgalmesh); +} + +} // namespace cgal + +} // namespace MeshBoolean +} // namespace Slic3r diff --git a/src/libslic3r/MeshBoolean.hpp b/src/libslic3r/MeshBoolean.hpp new file mode 100644 index 0000000000..14e3d3b7b2 --- /dev/null +++ b/src/libslic3r/MeshBoolean.hpp @@ -0,0 +1,38 @@ +#ifndef libslic3r_MeshBoolean_hpp_ +#define libslic3r_MeshBoolean_hpp_ + +#include + +namespace Slic3r { + +class TriangleMesh; + +namespace MeshBoolean { + +void minus(TriangleMesh& A, const TriangleMesh& B); +void self_union(TriangleMesh& mesh); + +namespace cgal { + +struct CGALMesh; + +std::unique_ptr triangle_mesh_to_cgal(const TriangleMesh &M); +void cgal_to_triangle_mesh(const CGALMesh &cgalmesh, TriangleMesh &out); + +// Do boolean mesh difference with CGAL bypassing igl. +void minus(TriangleMesh &A, const TriangleMesh &B); + +// Do self union only with CGAL. +void self_union(TriangleMesh& mesh); + +// does A = A - B +// CGAL takes non-const objects as arguments. I suppose it doesn't change B but +// there is no official garantee. +void minus(CGALMesh &A, CGALMesh &B); +void self_union(CGALMesh &A); + +} + +} // namespace MeshBoolean +} // namespace Slic3r +#endif // libslic3r_MeshBoolean_hpp_ diff --git a/src/libslic3r/Model.cpp b/src/libslic3r/Model.cpp index b01b2b99d5..c5c457e8f1 100644 --- a/src/libslic3r/Model.cpp +++ b/src/libslic3r/Model.cpp @@ -620,6 +620,7 @@ ModelObject& ModelObject::assign_copy(const ModelObject &rhs) assert(this->config.id() == rhs.config.id()); this->sla_support_points = rhs.sla_support_points; this->sla_points_status = rhs.sla_points_status; + this->sla_drain_holes = rhs.sla_drain_holes; this->layer_config_ranges = rhs.layer_config_ranges; // #ys_FIXME_experiment this->layer_height_profile = rhs.layer_height_profile; this->printable = rhs.printable; @@ -660,6 +661,7 @@ ModelObject& ModelObject::assign_copy(ModelObject &&rhs) assert(this->config.id() == rhs.config.id()); this->sla_support_points = std::move(rhs.sla_support_points); this->sla_points_status = std::move(rhs.sla_points_status); + this->sla_drain_holes = std::move(rhs.sla_drain_holes); this->layer_config_ranges = std::move(rhs.layer_config_ranges); // #ys_FIXME_experiment this->layer_height_profile = std::move(rhs.layer_height_profile); this->origin_translation = std::move(rhs.origin_translation); @@ -1113,6 +1115,7 @@ ModelObjectPtrs ModelObject::cut(size_t instance, coordf_t z, bool keep_upper, b if (keep_upper) { upper->set_model(nullptr); upper->sla_support_points.clear(); + lower->sla_drain_holes.clear(); upper->sla_points_status = sla::PointsStatus::NoPoints; upper->clear_volumes(); upper->input_file = ""; @@ -1121,6 +1124,7 @@ ModelObjectPtrs ModelObject::cut(size_t instance, coordf_t z, bool keep_upper, b if (keep_lower) { lower->set_model(nullptr); lower->sla_support_points.clear(); + lower->sla_drain_holes.clear(); lower->sla_points_status = sla::PointsStatus::NoPoints; lower->clear_volumes(); lower->input_file = ""; @@ -1156,7 +1160,8 @@ ModelObjectPtrs ModelObject::cut(size_t instance, coordf_t z, bool keep_upper, b if (keep_upper) { upper->add_volume(*volume); } if (keep_lower) { lower->add_volume(*volume); } } - else { + else if (! volume->mesh().empty()) { + TriangleMesh upper_mesh, lower_mesh; // Transform the mesh by the combined transformation matrix. @@ -1164,7 +1169,9 @@ ModelObjectPtrs ModelObject::cut(size_t instance, coordf_t z, bool keep_upper, b TriangleMesh mesh(volume->mesh()); mesh.transform(instance_matrix * volume_matrix, true); volume->reset_mesh(); - + + mesh.require_shared_vertices(); + // Perform cut TriangleMeshSlicer tms(&mesh); tms.cut(float(z), &upper_mesh, &lower_mesh); diff --git a/src/libslic3r/Model.hpp b/src/libslic3r/Model.hpp index 5f5b36b475..e0859e81dd 100644 --- a/src/libslic3r/Model.hpp +++ b/src/libslic3r/Model.hpp @@ -8,7 +8,8 @@ #include "Point.hpp" #include "PrintConfig.hpp" #include "Slicing.hpp" -#include "SLA/SLACommon.hpp" +#include "SLA/SupportPoint.hpp" +#include "SLA/Hollowing.hpp" #include "TriangleMesh.hpp" #include "Arrange.hpp" #include "CustomGCode.hpp" @@ -199,10 +200,13 @@ public: // This vector holds position of selected support points for SLA. The data are // saved in mesh coordinates to allow using them for several instances. // The format is (x, y, z, point_size, supports_island) - std::vector sla_support_points; + sla::SupportPoints sla_support_points; // To keep track of where the points came from (used for synchronization between // the SLA gizmo and the backend). - sla::PointsStatus sla_points_status = sla::PointsStatus::NoPoints; + sla::PointsStatus sla_points_status = sla::PointsStatus::NoPoints; + + // Holes to be drilled into the object so resin can flow out + sla::DrainHoles sla_drain_holes; /* This vector accumulates the total translation applied to the object by the center_around_origin() method. Callers might want to apply the same translation @@ -373,7 +377,7 @@ private: template void serialize(Archive &ar) { ar(cereal::base_class(this)); Internal::StaticSerializationWrapper config_wrapper(config); - ar(name, input_file, instances, volumes, config_wrapper, layer_config_ranges, layer_height_profile, sla_support_points, sla_points_status, printable, origin_translation, + ar(name, input_file, instances, volumes, config_wrapper, layer_config_ranges, layer_height_profile, sla_support_points, sla_points_status, sla_drain_holes, printable, origin_translation, m_bounding_box, m_bounding_box_valid, m_raw_bounding_box, m_raw_bounding_box_valid, m_raw_mesh_bounding_box, m_raw_mesh_bounding_box_valid); } }; diff --git a/src/libslic3r/OpenVDBUtils.cpp b/src/libslic3r/OpenVDBUtils.cpp new file mode 100644 index 0000000000..c30052036e --- /dev/null +++ b/src/libslic3r/OpenVDBUtils.cpp @@ -0,0 +1,135 @@ +#define NOMINMAX +#include "OpenVDBUtils.hpp" +#include +#include +#include + +//#include "MTUtils.hpp" + +namespace Slic3r { + +class TriangleMeshDataAdapter { +public: + const TriangleMesh &mesh; + + size_t polygonCount() const { return mesh.its.indices.size(); } + size_t pointCount() const { return mesh.its.vertices.size(); } + size_t vertexCount(size_t) const { return 3; } + + // Return position pos in local grid index space for polygon n and vertex v + void getIndexSpacePoint(size_t n, size_t v, openvdb::Vec3d& pos) const; +}; + +class Contour3DDataAdapter { +public: + const sla::Contour3D &mesh; + + size_t polygonCount() const { return mesh.faces3.size() + mesh.faces4.size(); } + size_t pointCount() const { return mesh.points.size(); } + size_t vertexCount(size_t n) const { return n < mesh.faces3.size() ? 3 : 4; } + + // Return position pos in local grid index space for polygon n and vertex v + void getIndexSpacePoint(size_t n, size_t v, openvdb::Vec3d& pos) const; +}; + +void TriangleMeshDataAdapter::getIndexSpacePoint(size_t n, + size_t v, + openvdb::Vec3d &pos) const +{ + auto vidx = size_t(mesh.its.indices[n](Eigen::Index(v))); + Slic3r::Vec3d p = mesh.its.vertices[vidx].cast(); + pos = {p.x(), p.y(), p.z()}; +} + +void Contour3DDataAdapter::getIndexSpacePoint(size_t n, + size_t v, + openvdb::Vec3d &pos) const +{ + size_t vidx = 0; + if (n < mesh.faces3.size()) vidx = size_t(mesh.faces3[n](Eigen::Index(v))); + else vidx = size_t(mesh.faces4[n - mesh.faces3.size()](Eigen::Index(v))); + + Slic3r::Vec3d p = mesh.points[vidx]; + pos = {p.x(), p.y(), p.z()}; +} + + +// TODO: Do I need to call initialize? Seems to work without it as well but the +// docs say it should be called ones. It does a mutex lock-unlock sequence all +// even if was called previously. + +openvdb::FloatGrid::Ptr mesh_to_grid(const TriangleMesh &mesh, + const openvdb::math::Transform &tr, + float exteriorBandWidth, + float interiorBandWidth, + int flags) +{ + openvdb::initialize(); + return openvdb::tools::meshToVolume( + TriangleMeshDataAdapter{mesh}, tr, exteriorBandWidth, + interiorBandWidth, flags); +} + +openvdb::FloatGrid::Ptr mesh_to_grid(const sla::Contour3D &mesh, + const openvdb::math::Transform &tr, + float exteriorBandWidth, + float interiorBandWidth, + int flags) +{ + openvdb::initialize(); + return openvdb::tools::meshToVolume( + Contour3DDataAdapter{mesh}, tr, exteriorBandWidth, interiorBandWidth, + flags); +} + +template +sla::Contour3D _volumeToMesh(const Grid &grid, + double isovalue, + double adaptivity, + bool relaxDisorientedTriangles) +{ + openvdb::initialize(); + + std::vector points; + std::vector triangles; + std::vector quads; + + openvdb::tools::volumeToMesh(grid, points, triangles, quads, isovalue, + adaptivity, relaxDisorientedTriangles); + + sla::Contour3D ret; + ret.points.reserve(points.size()); + ret.faces3.reserve(triangles.size()); + ret.faces4.reserve(quads.size()); + + for (auto &v : points) ret.points.emplace_back(to_vec3d(v)); + for (auto &v : triangles) ret.faces3.emplace_back(to_vec3i(v)); + for (auto &v : quads) ret.faces4.emplace_back(to_vec4i(v)); + + return ret; +} + +TriangleMesh grid_to_mesh(const openvdb::FloatGrid &grid, + double isovalue, + double adaptivity, + bool relaxDisorientedTriangles) +{ + return to_triangle_mesh( + _volumeToMesh(grid, isovalue, adaptivity, relaxDisorientedTriangles)); +} + +sla::Contour3D grid_to_contour3d(const openvdb::FloatGrid &grid, + double isovalue, + double adaptivity, + bool relaxDisorientedTriangles) +{ + return _volumeToMesh(grid, isovalue, adaptivity, + relaxDisorientedTriangles); +} + +openvdb::FloatGrid::Ptr redistance_grid(const openvdb::FloatGrid &grid, double iso, double er, double ir) +{ + return openvdb::tools::levelSetRebuild(grid, float(iso), float(er), float(ir)); +} + +} // namespace Slic3r diff --git a/src/libslic3r/OpenVDBUtils.hpp b/src/libslic3r/OpenVDBUtils.hpp new file mode 100644 index 0000000000..c493845a1c --- /dev/null +++ b/src/libslic3r/OpenVDBUtils.hpp @@ -0,0 +1,45 @@ +#ifndef OPENVDBUTILS_HPP +#define OPENVDBUTILS_HPP + +#include +#include +#include +#include + +namespace Slic3r { + +inline Vec3f to_vec3f(const openvdb::Vec3s &v) { return Vec3f{v.x(), v.y(), v.z()}; } +inline Vec3d to_vec3d(const openvdb::Vec3s &v) { return to_vec3f(v).cast(); } +inline Vec3i to_vec3i(const openvdb::Vec3I &v) { return Vec3i{int(v[0]), int(v[1]), int(v[2])}; } +inline Vec4i to_vec4i(const openvdb::Vec4I &v) { return Vec4i{int(v[0]), int(v[1]), int(v[2]), int(v[3])}; } + +openvdb::FloatGrid::Ptr mesh_to_grid(const TriangleMesh & mesh, + const openvdb::math::Transform &tr = {}, + float exteriorBandWidth = 3.0f, + float interiorBandWidth = 3.0f, + int flags = 0); + +openvdb::FloatGrid::Ptr mesh_to_grid(const sla::Contour3D & mesh, + const openvdb::math::Transform &tr = {}, + float exteriorBandWidth = 3.0f, + float interiorBandWidth = 3.0f, + int flags = 0); + +sla::Contour3D grid_to_contour3d(const openvdb::FloatGrid &grid, + double isovalue, + double adaptivity, + bool relaxDisorientedTriangles = true); + +TriangleMesh grid_to_mesh(const openvdb::FloatGrid &grid, + double isovalue = 0.0, + double adaptivity = 0.0, + bool relaxDisorientedTriangles = true); + +openvdb::FloatGrid::Ptr redistance_grid(const openvdb::FloatGrid &grid, + double iso, + double ext_range = 3., + double int_range = 3.); + +} // namespace Slic3r + +#endif // OPENVDBUTILS_HPP diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 9fe50d82eb..47a72b829c 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -2873,6 +2873,41 @@ void PrintConfigDef::init_sla_params() def->min = 0; def->mode = comExpert; def->set_default_value(new ConfigOptionFloat(0.3)); + + def = this->add("hollowing_enable", coBool); + def->label = L("Enable hollowing"); + def->category = L("Hollowing"); + def->tooltip = L("Hollow out a model to have an empty interior"); + def->mode = comSimple; + def->set_default_value(new ConfigOptionBool(false)); + + def = this->add("hollowing_min_thickness", coFloat); + def->label = L("Hollowing thickness"); + def->category = L("Hollowing"); + def->tooltip = L("Minimum wall thickness of a hollowed model."); + def->sidetext = L("mm"); + def->min = 1; + def->max = 10; + def->mode = comSimple; + def->set_default_value(new ConfigOptionFloat(3.)); + + def = this->add("hollowing_quality", coFloat); + def->label = L("Hollowing accuracy"); + def->category = L("Hollowing"); + def->tooltip = L("Performance vs accuracy of calculation. Lower values may produce unwanted artifacts."); + def->min = 0; + def->max = 1; + def->mode = comExpert; + def->set_default_value(new ConfigOptionFloat(0.5)); + + def = this->add("hollowing_closing_distance", coFloat); + def->label = L("Hollowing closing distance"); + def->category = L("Hollowing"); + def->tooltip = L(""); + def->min = 0; + def->max = 10; + def->mode = comExpert; + def->set_default_value(new ConfigOptionFloat(2.0)); } void PrintConfigDef::handle_legacy(t_config_option_key &opt_key, std::string &value) diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 6d70f54082..8d14969c9b 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -1017,7 +1017,7 @@ public: ConfigOptionFloat support_base_height /*= 1.0*/; // The minimum distance of the pillar base from the model in mm. - ConfigOptionFloat support_base_safety_distance; /*= 1.0*/; + ConfigOptionFloat support_base_safety_distance; /*= 1.0*/ // The default angle for connecting support sticks and junctions. ConfigOptionFloat support_critical_angle /*= 45*/; @@ -1062,7 +1062,7 @@ public: // ///////////////////////////////////////////////////////////////////////// // Zero elevation mode parameters: - // - The object pad will be derived from the the model geometry. + // - The object pad will be derived from the model geometry. // - There will be a gap between the object pad and the generated pad // according to the support_base_safety_distance parameter. // - The two pads will be connected with tiny connector sticks @@ -1084,6 +1084,28 @@ public: // How much should the tiny connectors penetrate into the model body ConfigOptionFloat pad_object_connector_penetration; + + // ///////////////////////////////////////////////////////////////////////// + // Model hollowing parameters: + // - Models can be hollowed out as part of the SLA print process + // - Thickness of the hollowed model walls can be adjusted + // - + // - Additional holes will be drilled into the hollow model to allow for + // - resin removal. + // ///////////////////////////////////////////////////////////////////////// + + ConfigOptionBool hollowing_enable; + + // The minimum thickness of the model walls to maintain. Note that the + // resulting walls may be thicker due to smoothing out fine cavities where + // resin could stuck. + ConfigOptionFloat hollowing_min_thickness; + + // Indirectly controls the voxel size (resolution) used by openvdb + ConfigOptionFloat hollowing_quality; + + // Indirectly controls the minimum size of created cavities. + ConfigOptionFloat hollowing_closing_distance; protected: void initialize(StaticCacheBase &cache, const char *base_ptr) @@ -1121,6 +1143,10 @@ protected: OPT_PTR(pad_object_connector_stride); OPT_PTR(pad_object_connector_width); OPT_PTR(pad_object_connector_penetration); + OPT_PTR(hollowing_enable); + OPT_PTR(hollowing_min_thickness); + OPT_PTR(hollowing_quality); + OPT_PTR(hollowing_closing_distance); } }; diff --git a/src/libslic3r/SLA/SLABoostAdapter.hpp b/src/libslic3r/SLA/BoostAdapter.hpp similarity index 97% rename from src/libslic3r/SLA/SLABoostAdapter.hpp rename to src/libslic3r/SLA/BoostAdapter.hpp index 1e9daf4613..b7b3c63a6c 100644 --- a/src/libslic3r/SLA/SLABoostAdapter.hpp +++ b/src/libslic3r/SLA/BoostAdapter.hpp @@ -1,7 +1,7 @@ -#ifndef SLABOOSTADAPTER_HPP -#define SLABOOSTADAPTER_HPP +#ifndef SLA_BOOSTADAPTER_HPP +#define SLA_BOOSTADAPTER_HPP -#include "SLA/SLABoilerPlate.hpp" +#include #include namespace boost { diff --git a/src/libslic3r/SLA/Clustering.hpp b/src/libslic3r/SLA/Clustering.hpp new file mode 100644 index 0000000000..1b0d47d953 --- /dev/null +++ b/src/libslic3r/SLA/Clustering.hpp @@ -0,0 +1,30 @@ +#ifndef SLA_CLUSTERING_HPP +#define SLA_CLUSTERING_HPP + +#include +#include +#include + +namespace Slic3r { namespace sla { + +using ClusterEl = std::vector; +using ClusteredPoints = std::vector; + +// Clustering a set of points by the given distance. +ClusteredPoints cluster(const std::vector& indices, + std::function pointfn, + double dist, + unsigned max_points); + +ClusteredPoints cluster(const PointSet& points, + double dist, + unsigned max_points); + +ClusteredPoints cluster( + const std::vector& indices, + std::function pointfn, + std::function predicate, + unsigned max_points); + +}} +#endif // CLUSTERING_HPP diff --git a/src/libslic3r/SLA/Common.cpp b/src/libslic3r/SLA/Common.cpp new file mode 100644 index 0000000000..3d31c55226 --- /dev/null +++ b/src/libslic3r/SLA/Common.cpp @@ -0,0 +1,741 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +// Workaround: IGL signed_distance.h will define PI in the igl namespace. +#undef PI + +// HEAVY headers... takes eternity to compile + +// for concave hull merging decisions +#include +#include "boost/geometry/index/rtree.hpp" + +#ifdef _MSC_VER +#pragma warning(push) +#pragma warning(disable: 4244) +#pragma warning(disable: 4267) +#endif +#include +#include +#include +#include +#ifdef _MSC_VER +#pragma warning(pop) +#endif + +#include + +#include "ClipperUtils.hpp" + +namespace Slic3r { +namespace sla { + +// Bring back PI from the igl namespace +using igl::PI; + +/* ************************************************************************** + * PointIndex implementation + * ************************************************************************** */ + +class PointIndex::Impl { +public: + using BoostIndex = boost::geometry::index::rtree< PointIndexEl, + boost::geometry::index::rstar<16, 4> /* ? */ >; + + BoostIndex m_store; +}; + +PointIndex::PointIndex(): m_impl(new Impl()) {} +PointIndex::~PointIndex() {} + +PointIndex::PointIndex(const PointIndex &cpy): m_impl(new Impl(*cpy.m_impl)) {} +PointIndex::PointIndex(PointIndex&& cpy): m_impl(std::move(cpy.m_impl)) {} + +PointIndex& PointIndex::operator=(const PointIndex &cpy) +{ + m_impl.reset(new Impl(*cpy.m_impl)); + return *this; +} + +PointIndex& PointIndex::operator=(PointIndex &&cpy) +{ + m_impl.swap(cpy.m_impl); + return *this; +} + +void PointIndex::insert(const PointIndexEl &el) +{ + m_impl->m_store.insert(el); +} + +bool PointIndex::remove(const PointIndexEl& el) +{ + return m_impl->m_store.remove(el) == 1; +} + +std::vector +PointIndex::query(std::function fn) const +{ + namespace bgi = boost::geometry::index; + + std::vector ret; + m_impl->m_store.query(bgi::satisfies(fn), std::back_inserter(ret)); + return ret; +} + +std::vector PointIndex::nearest(const Vec3d &el, unsigned k = 1) const +{ + namespace bgi = boost::geometry::index; + std::vector ret; ret.reserve(k); + m_impl->m_store.query(bgi::nearest(el, k), std::back_inserter(ret)); + return ret; +} + +size_t PointIndex::size() const +{ + return m_impl->m_store.size(); +} + +void PointIndex::foreach(std::function fn) +{ + for(auto& el : m_impl->m_store) fn(el); +} + +void PointIndex::foreach(std::function fn) const +{ + for(const auto &el : m_impl->m_store) fn(el); +} + +/* ************************************************************************** + * BoxIndex implementation + * ************************************************************************** */ + +class BoxIndex::Impl { +public: + using BoostIndex = boost::geometry::index:: + rtree /* ? */>; + + BoostIndex m_store; +}; + +BoxIndex::BoxIndex(): m_impl(new Impl()) {} +BoxIndex::~BoxIndex() {} + +BoxIndex::BoxIndex(const BoxIndex &cpy): m_impl(new Impl(*cpy.m_impl)) {} +BoxIndex::BoxIndex(BoxIndex&& cpy): m_impl(std::move(cpy.m_impl)) {} + +BoxIndex& BoxIndex::operator=(const BoxIndex &cpy) +{ + m_impl.reset(new Impl(*cpy.m_impl)); + return *this; +} + +BoxIndex& BoxIndex::operator=(BoxIndex &&cpy) +{ + m_impl.swap(cpy.m_impl); + return *this; +} + +void BoxIndex::insert(const BoxIndexEl &el) +{ + m_impl->m_store.insert(el); +} + +bool BoxIndex::remove(const BoxIndexEl& el) +{ + return m_impl->m_store.remove(el) == 1; +} + +std::vector BoxIndex::query(const BoundingBox &qrbb, + BoxIndex::QueryType qt) +{ + namespace bgi = boost::geometry::index; + + std::vector ret; ret.reserve(m_impl->m_store.size()); + + switch (qt) { + case qtIntersects: + m_impl->m_store.query(bgi::intersects(qrbb), std::back_inserter(ret)); + break; + case qtWithin: + m_impl->m_store.query(bgi::within(qrbb), std::back_inserter(ret)); + } + + return ret; +} + +size_t BoxIndex::size() const +{ + return m_impl->m_store.size(); +} + +void BoxIndex::foreach(std::function fn) +{ + for(auto& el : m_impl->m_store) fn(el); +} + + +/* **************************************************************************** + * EigenMesh3D implementation + * ****************************************************************************/ + +class EigenMesh3D::AABBImpl: public igl::AABB { +public: +#ifdef SLIC3R_SLA_NEEDS_WINDTREE + igl::WindingNumberAABB windtree; +#endif /* SLIC3R_SLA_NEEDS_WINDTREE */ +}; + +EigenMesh3D::EigenMesh3D(const TriangleMesh& tmesh): m_aabb(new AABBImpl()) { + static const double dEPS = 1e-6; + + const stl_file& stl = tmesh.stl; + + auto&& bb = tmesh.bounding_box(); + m_ground_level += bb.min(Z); + + Eigen::MatrixXd V; + Eigen::MatrixXi F; + + V.resize(3*stl.stats.number_of_facets, 3); + F.resize(stl.stats.number_of_facets, 3); + for (unsigned int i = 0; i < stl.stats.number_of_facets; ++i) { + const stl_facet &facet = stl.facet_start[i]; + V.block<1, 3>(3 * i + 0, 0) = facet.vertex[0].cast(); + V.block<1, 3>(3 * i + 1, 0) = facet.vertex[1].cast(); + V.block<1, 3>(3 * i + 2, 0) = facet.vertex[2].cast(); + F(i, 0) = int(3*i+0); + F(i, 1) = int(3*i+1); + F(i, 2) = int(3*i+2); + } + + // We will convert this to a proper 3d mesh with no duplicate points. + Eigen::VectorXi SVI, SVJ; + igl::remove_duplicate_vertices(V, F, dEPS, m_V, SVI, SVJ, m_F); + + // Build the AABB accelaration tree + m_aabb->init(m_V, m_F); +#ifdef SLIC3R_SLA_NEEDS_WINDTREE + m_aabb->windtree.set_mesh(m_V, m_F); +#endif /* SLIC3R_SLA_NEEDS_WINDTREE */ +} + +EigenMesh3D::~EigenMesh3D() {} + +EigenMesh3D::EigenMesh3D(const EigenMesh3D &other): + m_V(other.m_V), m_F(other.m_F), m_ground_level(other.m_ground_level), + m_aabb( new AABBImpl(*other.m_aabb) ) {} + +EigenMesh3D::EigenMesh3D(const Contour3D &other) +{ + m_V.resize(Eigen::Index(other.points.size()), 3); + m_F.resize(Eigen::Index(other.faces3.size() + 2 * other.faces4.size()), 3); + + for (Eigen::Index i = 0; i < Eigen::Index(other.points.size()); ++i) + m_V.row(i) = other.points[size_t(i)]; + + for (Eigen::Index i = 0; i < Eigen::Index(other.faces3.size()); ++i) + m_F.row(i) = other.faces3[size_t(i)]; + + size_t N = other.faces3.size() + 2 * other.faces4.size(); + for (size_t i = other.faces3.size(); i < N; i += 2) { + size_t quad_idx = (i - other.faces3.size()) / 2; + auto & quad = other.faces4[quad_idx]; + m_F.row(Eigen::Index(i)) = Vec3i{quad(0), quad(1), quad(2)}; + m_F.row(Eigen::Index(i + 1)) = Vec3i{quad(2), quad(3), quad(0)}; + } +} + +EigenMesh3D &EigenMesh3D::operator=(const EigenMesh3D &other) +{ + m_V = other.m_V; + m_F = other.m_F; + m_ground_level = other.m_ground_level; + m_aabb.reset(new AABBImpl(*other.m_aabb)); return *this; +} + +EigenMesh3D::hit_result +EigenMesh3D::query_ray_hit(const Vec3d &s, const Vec3d &dir) const +{ + assert(is_approx(dir.norm(), 1.)); + igl::Hit hit; + hit.t = std::numeric_limits::infinity(); + + if (m_holes.empty()) { + m_aabb->intersect_ray(m_V, m_F, s, dir, hit); + hit_result ret(*this); + ret.m_t = double(hit.t); + ret.m_dir = dir; + ret.m_source = s; + if(!std::isinf(hit.t) && !std::isnan(hit.t)) + ret.m_normal = this->normal_by_face_id(hit.id); + + return ret; + } + else { + // If there are holes, the hit_results will be made by + // query_ray_hits (object) and filter_hits (holes): + return filter_hits(query_ray_hits(s, dir)); + } +} + +std::vector +EigenMesh3D::query_ray_hits(const Vec3d &s, const Vec3d &dir) const +{ + std::vector outs; + std::vector hits; + m_aabb->intersect_ray(m_V, m_F, s, dir, hits); + + // The sort is necessary, the hits are not always sorted. + std::sort(hits.begin(), hits.end(), + [](const igl::Hit& a, const igl::Hit& b) { return a.t < b.t; }); + + // Remove duplicates. They sometimes appear, for example when the ray is cast + // along an axis of a cube due to floating-point approximations in igl (?) + hits.erase(std::unique(hits.begin(), hits.end(), + [](const igl::Hit& a, const igl::Hit& b) + { return a.t == b.t; }), + hits.end()); + + // Convert the igl::Hit into hit_result + outs.reserve(hits.size()); + for (const igl::Hit& hit : hits) { + outs.emplace_back(EigenMesh3D::hit_result(*this)); + outs.back().m_t = double(hit.t); + outs.back().m_dir = dir; + outs.back().m_source = s; + if(!std::isinf(hit.t) && !std::isnan(hit.t)) + outs.back().m_normal = this->normal_by_face_id(hit.id); + } + + return outs; +} + +EigenMesh3D::hit_result EigenMesh3D::filter_hits( + const std::vector& object_hits) const +{ + assert(! m_holes.empty()); + hit_result out(*this); + + if (object_hits.empty()) + return out; + + const Vec3d& s = object_hits.front().source(); + const Vec3d& dir = object_hits.front().direction(); + + // A helper struct to save an intersetion with a hole + struct HoleHit { + HoleHit(float t_p, const Vec3d& normal_p, bool entry_p) : + t(t_p), normal(normal_p), entry(entry_p) {} + float t; + Vec3d normal; + bool entry; + }; + std::vector hole_isects; + hole_isects.reserve(m_holes.size()); + + auto sf = s.cast(); + auto dirf = dir.cast(); + + // Collect hits on all holes, preserve information about entry/exit + for (const sla::DrainHole& hole : m_holes) { + std::array, 2> isects; + if (hole.get_intersections(sf, dirf, isects)) { + // Ignore hole hits behind the source + if (isects[0].first > 0.f) hole_isects.emplace_back(isects[0].first, isects[0].second, true); + if (isects[1].first > 0.f) hole_isects.emplace_back(isects[1].first, isects[1].second, false); + } + } + + // Holes can intersect each other, sort the hits by t + std::sort(hole_isects.begin(), hole_isects.end(), + [](const HoleHit& a, const HoleHit& b) { return a.t < b.t; }); + + // Now inspect the intersections with object and holes, in the order of + // increasing distance. Keep track how deep are we nested in mesh/holes and + // pick the correct intersection. + // This needs to be done twice - first to find out how deep in the structure + // the source is, then to pick the correct intersection. + int hole_nested = 0; + int object_nested = 0; + for (int dry_run=1; dry_run>=0; --dry_run) { + hole_nested = -hole_nested; + object_nested = -object_nested; + + bool is_hole = false; + bool is_entry = false; + const HoleHit* next_hole_hit = hole_isects.empty() ? nullptr : &hole_isects.front(); + const hit_result* next_mesh_hit = &object_hits.front(); + + while (next_hole_hit || next_mesh_hit) { + if (next_hole_hit && next_mesh_hit) // still have hole and obj hits + is_hole = (next_hole_hit->t < next_mesh_hit->m_t); + else + is_hole = next_hole_hit; // one or the other ran out + + // Is this entry or exit hit? + is_entry = is_hole ? next_hole_hit->entry : ! next_mesh_hit->is_inside(); + + if (! dry_run) { + if (! is_hole && hole_nested == 0) { + // This is a valid object hit + return *next_mesh_hit; + } + if (is_hole && ! is_entry && object_nested != 0) { + // This holehit is the one we seek + out.m_t = next_hole_hit->t; + out.m_normal = next_hole_hit->normal; + out.m_source = s; + out.m_dir = dir; + return out; + } + } + + // Increase/decrease the counter + (is_hole ? hole_nested : object_nested) += (is_entry ? 1 : -1); + + // Advance the respective pointer + if (is_hole && next_hole_hit++ == &hole_isects.back()) + next_hole_hit = nullptr; + if (! is_hole && next_mesh_hit++ == &object_hits.back()) + next_mesh_hit = nullptr; + } + } + + // if we got here, the ray ended up in infinity + return out; +} + +#ifdef SLIC3R_SLA_NEEDS_WINDTREE +EigenMesh3D::si_result EigenMesh3D::signed_distance(const Vec3d &p) const { + double sign = 0; double sqdst = 0; int i = 0; Vec3d c; + igl::signed_distance_winding_number(*m_aabb, m_V, m_F, m_aabb->windtree, + p, sign, sqdst, i, c); + + return si_result(sign * std::sqrt(sqdst), i, c); +} + +bool EigenMesh3D::inside(const Vec3d &p) const { + return m_aabb->windtree.inside(p); +} +#endif /* SLIC3R_SLA_NEEDS_WINDTREE */ + +double EigenMesh3D::squared_distance(const Vec3d &p, int& i, Vec3d& c) const { + double sqdst = 0; + Eigen::Matrix pp = p; + Eigen::Matrix cc; + sqdst = m_aabb->squared_distance(m_V, m_F, pp, i, cc); + c = cc; + return sqdst; +} + +/* **************************************************************************** + * Misc functions + * ****************************************************************************/ + +namespace { + +bool point_on_edge(const Vec3d& p, const Vec3d& e1, const Vec3d& e2, + double eps = 0.05) +{ + using Line3D = Eigen::ParametrizedLine; + + auto line = Line3D::Through(e1, e2); + double d = line.distance(p); + return std::abs(d) < eps; +} + +template double distance(const Vec& pp1, const Vec& pp2) { + auto p = pp2 - pp1; + return std::sqrt(p.transpose() * p); +} + +} + +PointSet normals(const PointSet& points, + const EigenMesh3D& mesh, + double eps, + std::function thr, // throw on cancel + const std::vector& pt_indices) +{ + if (points.rows() == 0 || mesh.V().rows() == 0 || mesh.F().rows() == 0) + return {}; + + std::vector range = pt_indices; + if (range.empty()) { + range.resize(size_t(points.rows()), 0); + std::iota(range.begin(), range.end(), 0); + } + + PointSet ret(range.size(), 3); + + // for (size_t ridx = 0; ridx < range.size(); ++ridx) + ccr::enumerate( + range.begin(), range.end(), + [&ret, &mesh, &points, thr, eps](unsigned el, size_t ridx) { + thr(); + auto eidx = Eigen::Index(el); + int faceid = 0; + Vec3d p; + + mesh.squared_distance(points.row(eidx), faceid, p); + + auto trindex = mesh.F().row(faceid); + + const Vec3d &p1 = mesh.V().row(trindex(0)); + const Vec3d &p2 = mesh.V().row(trindex(1)); + const Vec3d &p3 = mesh.V().row(trindex(2)); + + // We should check if the point lies on an edge of the hosting + // triangle. If it does then all the other triangles using the + // same two points have to be searched and the final normal should + // be some kind of aggregation of the participating triangle + // normals. We should also consider the cases where the support + // point lies right on a vertex of its triangle. The procedure is + // the same, get the neighbor triangles and calculate an average + // normal. + + // mark the vertex indices of the edge. ia and ib marks and edge + // ic will mark a single vertex. + int ia = -1, ib = -1, ic = -1; + + if (std::abs(distance(p, p1)) < eps) { + ic = trindex(0); + } else if (std::abs(distance(p, p2)) < eps) { + ic = trindex(1); + } else if (std::abs(distance(p, p3)) < eps) { + ic = trindex(2); + } else if (point_on_edge(p, p1, p2, eps)) { + ia = trindex(0); + ib = trindex(1); + } else if (point_on_edge(p, p2, p3, eps)) { + ia = trindex(1); + ib = trindex(2); + } else if (point_on_edge(p, p1, p3, eps)) { + ia = trindex(0); + ib = trindex(2); + } + + // vector for the neigboring triangles including the detected one. + std::vector neigh; + if (ic >= 0) { // The point is right on a vertex of the triangle + for (int n = 0; n < mesh.F().rows(); ++n) { + thr(); + Vec3i ni = mesh.F().row(n); + if ((ni(X) == ic || ni(Y) == ic || ni(Z) == ic)) + neigh.emplace_back(ni); + } + } else if (ia >= 0 && ib >= 0) { // the point is on and edge + // now get all the neigboring triangles + for (int n = 0; n < mesh.F().rows(); ++n) { + thr(); + Vec3i ni = mesh.F().row(n); + if ((ni(X) == ia || ni(Y) == ia || ni(Z) == ia) && + (ni(X) == ib || ni(Y) == ib || ni(Z) == ib)) + neigh.emplace_back(ni); + } + } + + // Calculate the normals for the neighboring triangles + std::vector neighnorms; + neighnorms.reserve(neigh.size()); + for (const Vec3i &tri : neigh) { + const Vec3d & pt1 = mesh.V().row(tri(0)); + const Vec3d & pt2 = mesh.V().row(tri(1)); + const Vec3d & pt3 = mesh.V().row(tri(2)); + Eigen::Vector3d U = pt2 - pt1; + Eigen::Vector3d V = pt3 - pt1; + neighnorms.emplace_back(U.cross(V).normalized()); + } + + // Throw out duplicates. They would cause trouble with summing. We + // will use std::unique which works on sorted ranges. We will sort + // by the coefficient-wise sum of the normals. It should force the + // same elements to be consecutive. + std::sort(neighnorms.begin(), neighnorms.end(), + [](const Vec3d &v1, const Vec3d &v2) { + return v1.sum() < v2.sum(); + }); + + auto lend = std::unique(neighnorms.begin(), neighnorms.end(), + [](const Vec3d &n1, const Vec3d &n2) { + // Compare normals for equivalence. + // This is controvers stuff. + auto deq = [](double a, double b) { + return std::abs(a - b) < 1e-3; + }; + return deq(n1(X), n2(X)) && + deq(n1(Y), n2(Y)) && + deq(n1(Z), n2(Z)); + }); + + if (!neighnorms.empty()) { // there were neighbors to count with + // sum up the normals and then normalize the result again. + // This unification seems to be enough. + Vec3d sumnorm(0, 0, 0); + sumnorm = std::accumulate(neighnorms.begin(), lend, sumnorm); + sumnorm.normalize(); + ret.row(long(ridx)) = sumnorm; + } else { // point lies safely within its triangle + Eigen::Vector3d U = p2 - p1; + Eigen::Vector3d V = p3 - p1; + ret.row(long(ridx)) = U.cross(V).normalized(); + } + }); + + return ret; +} + +namespace bgi = boost::geometry::index; +using Index3D = bgi::rtree< PointIndexEl, bgi::rstar<16, 4> /* ? */ >; + +namespace { + +bool cmp_ptidx_elements(const PointIndexEl& e1, const PointIndexEl& e2) +{ + return e1.second < e2.second; +}; + +ClusteredPoints cluster(Index3D &sindex, + unsigned max_points, + std::function( + const Index3D &, const PointIndexEl &)> qfn) +{ + using Elems = std::vector; + + // Recursive function for visiting all the points in a given distance to + // each other + std::function group = + [&sindex, &group, max_points, qfn](Elems& pts, Elems& cluster) + { + for(auto& p : pts) { + std::vector tmp = qfn(sindex, p); + + std::sort(tmp.begin(), tmp.end(), cmp_ptidx_elements); + + Elems newpts; + std::set_difference(tmp.begin(), tmp.end(), + cluster.begin(), cluster.end(), + std::back_inserter(newpts), cmp_ptidx_elements); + + int c = max_points && newpts.size() + cluster.size() > max_points? + int(max_points - cluster.size()) : int(newpts.size()); + + cluster.insert(cluster.end(), newpts.begin(), newpts.begin() + c); + std::sort(cluster.begin(), cluster.end(), cmp_ptidx_elements); + + if(!newpts.empty() && (!max_points || cluster.size() < max_points)) + group(newpts, cluster); + } + }; + + std::vector clusters; + for(auto it = sindex.begin(); it != sindex.end();) { + Elems cluster = {}; + Elems pts = {*it}; + group(pts, cluster); + + for(auto& c : cluster) sindex.remove(c); + it = sindex.begin(); + + clusters.emplace_back(cluster); + } + + ClusteredPoints result; + for(auto& cluster : clusters) { + result.emplace_back(); + for(auto c : cluster) result.back().emplace_back(c.second); + } + + return result; +} + +std::vector distance_queryfn(const Index3D& sindex, + const PointIndexEl& p, + double dist, + unsigned max_points) +{ + std::vector tmp; tmp.reserve(max_points); + sindex.query( + bgi::nearest(p.first, max_points), + std::back_inserter(tmp) + ); + + for(auto it = tmp.begin(); it < tmp.end(); ++it) + if(distance(p.first, it->first) > dist) it = tmp.erase(it); + + return tmp; +} + +} // namespace + +// Clustering a set of points by the given criteria +ClusteredPoints cluster( + const std::vector& indices, + std::function pointfn, + double dist, + unsigned max_points) +{ + // A spatial index for querying the nearest points + Index3D sindex; + + // Build the index + for(auto idx : indices) sindex.insert( std::make_pair(pointfn(idx), idx)); + + return cluster(sindex, max_points, + [dist, max_points](const Index3D& sidx, const PointIndexEl& p) + { + return distance_queryfn(sidx, p, dist, max_points); + }); +} + +// Clustering a set of points by the given criteria +ClusteredPoints cluster( + const std::vector& indices, + std::function pointfn, + std::function predicate, + unsigned max_points) +{ + // A spatial index for querying the nearest points + Index3D sindex; + + // Build the index + for(auto idx : indices) sindex.insert( std::make_pair(pointfn(idx), idx)); + + return cluster(sindex, max_points, + [max_points, predicate](const Index3D& sidx, const PointIndexEl& p) + { + std::vector tmp; tmp.reserve(max_points); + sidx.query(bgi::satisfies([p, predicate](const PointIndexEl& e){ + return predicate(p, e); + }), std::back_inserter(tmp)); + return tmp; + }); +} + +ClusteredPoints cluster(const PointSet& pts, double dist, unsigned max_points) +{ + // A spatial index for querying the nearest points + Index3D sindex; + + // Build the index + for(Eigen::Index i = 0; i < pts.rows(); i++) + sindex.insert(std::make_pair(Vec3d(pts.row(i)), unsigned(i))); + + return cluster(sindex, max_points, + [dist, max_points](const Index3D& sidx, const PointIndexEl& p) + { + return distance_queryfn(sidx, p, dist, max_points); + }); +} + +} // namespace sla +} // namespace Slic3r diff --git a/src/libslic3r/SLA/Common.hpp b/src/libslic3r/SLA/Common.hpp new file mode 100644 index 0000000000..e1c5930e2a --- /dev/null +++ b/src/libslic3r/SLA/Common.hpp @@ -0,0 +1,32 @@ +#ifndef SLA_COMMON_HPP +#define SLA_COMMON_HPP + +#include +#include +#include +#include +#include + +//#include "SLASpatIndex.hpp" + +//#include +//#include + +// #define SLIC3R_SLA_NEEDS_WINDTREE + +namespace Slic3r { + +// Typedefs from Point.hpp +typedef Eigen::Matrix Vec3f; +typedef Eigen::Matrix Vec3d; +typedef Eigen::Matrix Vec4i; + +namespace sla { + +using PointSet = Eigen::MatrixXd; + +} // namespace sla +} // namespace Slic3r + + +#endif // SLASUPPORTTREE_HPP diff --git a/src/libslic3r/SLA/ConcaveHull.cpp b/src/libslic3r/SLA/ConcaveHull.cpp index dff0617216..d3c0d10224 100644 --- a/src/libslic3r/SLA/ConcaveHull.cpp +++ b/src/libslic3r/SLA/ConcaveHull.cpp @@ -1,7 +1,9 @@ -#include "ConcaveHull.hpp" +#include +#include + #include #include -#include "SLASpatIndex.hpp" + #include namespace Slic3r { @@ -40,9 +42,9 @@ Point ConcaveHull::centroid(const Points &pp) // As it shows, the current offset_ex in ClipperUtils hangs if used in jtRound // mode -ClipperLib::Paths fast_offset(const ClipperLib::Paths &paths, - coord_t delta, - ClipperLib::JoinType jointype) +static ClipperLib::Paths fast_offset(const ClipperLib::Paths &paths, + coord_t delta, + ClipperLib::JoinType jointype) { using ClipperLib::ClipperOffset; using ClipperLib::etClosedPolygon; @@ -73,7 +75,7 @@ Points ConcaveHull::calculate_centroids() const Points centroids = reserve_vector(m_polys.size()); std::transform(m_polys.begin(), m_polys.end(), std::back_inserter(centroids), - [this](const Polygon &poly) { return centroid(poly); }); + [](const Polygon &poly) { return centroid(poly); }); return centroids; } diff --git a/src/libslic3r/SLA/ConcaveHull.hpp b/src/libslic3r/SLA/ConcaveHull.hpp index 94e16d77cc..dccdafd18c 100644 --- a/src/libslic3r/SLA/ConcaveHull.hpp +++ b/src/libslic3r/SLA/ConcaveHull.hpp @@ -1,5 +1,5 @@ -#ifndef CONCAVEHULL_HPP -#define CONCAVEHULL_HPP +#ifndef SLA_CONCAVEHULL_HPP +#define SLA_CONCAVEHULL_HPP #include diff --git a/src/libslic3r/SLA/SLAConcurrency.hpp b/src/libslic3r/SLA/Concurrency.hpp similarity index 96% rename from src/libslic3r/SLA/SLAConcurrency.hpp rename to src/libslic3r/SLA/Concurrency.hpp index 4beb2aead1..8620c67b19 100644 --- a/src/libslic3r/SLA/SLAConcurrency.hpp +++ b/src/libslic3r/SLA/Concurrency.hpp @@ -1,5 +1,5 @@ -#ifndef SLACONCURRENCY_H -#define SLACONCURRENCY_H +#ifndef SLA_CONCURRENCY_H +#define SLA_CONCURRENCY_H #include #include diff --git a/src/libslic3r/SLA/Contour3D.cpp b/src/libslic3r/SLA/Contour3D.cpp new file mode 100644 index 0000000000..e39672b8bd --- /dev/null +++ b/src/libslic3r/SLA/Contour3D.cpp @@ -0,0 +1,149 @@ +#include +#include + +#include + +namespace Slic3r { namespace sla { + +Contour3D::Contour3D(const TriangleMesh &trmesh) +{ + points.reserve(trmesh.its.vertices.size()); + faces3.reserve(trmesh.its.indices.size()); + + for (auto &v : trmesh.its.vertices) + points.emplace_back(v.cast()); + + std::copy(trmesh.its.indices.begin(), trmesh.its.indices.end(), + std::back_inserter(faces3)); +} + +Contour3D::Contour3D(TriangleMesh &&trmesh) +{ + points.reserve(trmesh.its.vertices.size()); + + for (auto &v : trmesh.its.vertices) + points.emplace_back(v.cast()); + + faces3.swap(trmesh.its.indices); +} + +Contour3D::Contour3D(const EigenMesh3D &emesh) { + points.reserve(size_t(emesh.V().rows())); + faces3.reserve(size_t(emesh.F().rows())); + + for (int r = 0; r < emesh.V().rows(); r++) + points.emplace_back(emesh.V().row(r).cast()); + + for (int i = 0; i < emesh.F().rows(); i++) + faces3.emplace_back(emesh.F().row(i)); +} + +Contour3D &Contour3D::merge(const Contour3D &ctr) +{ + auto N = coord_t(points.size()); + auto N_f3 = faces3.size(); + auto N_f4 = faces4.size(); + + points.insert(points.end(), ctr.points.begin(), ctr.points.end()); + faces3.insert(faces3.end(), ctr.faces3.begin(), ctr.faces3.end()); + faces4.insert(faces4.end(), ctr.faces4.begin(), ctr.faces4.end()); + + for(size_t n = N_f3; n < faces3.size(); n++) { + auto& idx = faces3[n]; idx.x() += N; idx.y() += N; idx.z() += N; + } + + for(size_t n = N_f4; n < faces4.size(); n++) { + auto& idx = faces4[n]; for (int k = 0; k < 4; k++) idx(k) += N; + } + + return *this; +} + +Contour3D &Contour3D::merge(const Pointf3s &triangles) +{ + const size_t offs = points.size(); + points.insert(points.end(), triangles.begin(), triangles.end()); + faces3.reserve(faces3.size() + points.size() / 3); + + for(int i = int(offs); i < int(points.size()); i += 3) + faces3.emplace_back(i, i + 1, i + 2); + + return *this; +} + +void Contour3D::to_obj(std::ostream &stream) +{ + for(auto& p : points) + stream << "v " << p.transpose() << "\n"; + + for(auto& f : faces3) + stream << "f " << (f + Vec3i(1, 1, 1)).transpose() << "\n"; + + for(auto& f : faces4) + stream << "f " << (f + Vec4i(1, 1, 1, 1)).transpose() << "\n"; +} + +void Contour3D::from_obj(std::istream &stream) +{ + ObjParser::ObjData data; + ObjParser::objparse(stream, data); + + points.reserve(data.coordinates.size() / 4 + 1); + auto &coords = data.coordinates; + for (size_t i = 0; i < coords.size(); i += 4) + points.emplace_back(coords[i], coords[i + 1], coords[i + 2]); + + Vec3i triangle; + Vec4i quad; + size_t v = 0; + while(v < data.vertices.size()) { + size_t N = 0; + size_t i = v; + while (data.vertices[v++].coordIdx != -1) ++N; + + std::function setfn; + if (N < 3 || N > 4) continue; + else if (N == 3) setfn = [&triangle](int k, int f) { triangle(k) = f; }; + else setfn = [&quad](int k, int f) { quad(k) = f; }; + + for (size_t j = 0; j < N; ++j) + setfn(int(j), data.vertices[i + j].coordIdx); + } +} + +TriangleMesh to_triangle_mesh(const Contour3D &ctour) { + if (ctour.faces4.empty()) return {ctour.points, ctour.faces3}; + + std::vector triangles; + + triangles.reserve(ctour.faces3.size() + 2 * ctour.faces4.size()); + std::copy(ctour.faces3.begin(), ctour.faces3.end(), + std::back_inserter(triangles)); + + for (auto &quad : ctour.faces4) { + triangles.emplace_back(quad(0), quad(1), quad(2)); + triangles.emplace_back(quad(2), quad(3), quad(0)); + } + + return {ctour.points, std::move(triangles)}; +} + +TriangleMesh to_triangle_mesh(Contour3D &&ctour) { + if (ctour.faces4.empty()) + return {std::move(ctour.points), std::move(ctour.faces3)}; + + std::vector triangles; + + triangles.reserve(ctour.faces3.size() + 2 * ctour.faces4.size()); + std::copy(ctour.faces3.begin(), ctour.faces3.end(), + std::back_inserter(triangles)); + + for (auto &quad : ctour.faces4) { + triangles.emplace_back(quad(0), quad(1), quad(2)); + triangles.emplace_back(quad(2), quad(3), quad(0)); + } + + return {std::move(ctour.points), std::move(triangles)}; +} + +}} // namespace Slic3r::sla diff --git a/src/libslic3r/SLA/Contour3D.hpp b/src/libslic3r/SLA/Contour3D.hpp new file mode 100644 index 0000000000..295612f19b --- /dev/null +++ b/src/libslic3r/SLA/Contour3D.hpp @@ -0,0 +1,45 @@ +#ifndef SLA_CONTOUR3D_HPP +#define SLA_CONTOUR3D_HPP + +#include + +#include + +namespace Slic3r { namespace sla { + +class EigenMesh3D; + +/// Dumb vertex mesh consisting of triangles (or) quads. Capable of merging with +/// other meshes of this type and converting to and from other mesh formats. +struct Contour3D { + std::vector points; + std::vector faces3; + std::vector faces4; + + Contour3D() = default; + Contour3D(const TriangleMesh &trmesh); + Contour3D(TriangleMesh &&trmesh); + Contour3D(const EigenMesh3D &emesh); + + Contour3D& merge(const Contour3D& ctr); + Contour3D& merge(const Pointf3s& triangles); + + // Write the index triangle structure to OBJ file for debugging purposes. + void to_obj(std::ostream& stream); + void from_obj(std::istream &stream); + + inline bool empty() const + { + return points.empty() || (faces4.empty() && faces3.empty()); + } +}; + +/// Mesh from an existing contour. +TriangleMesh to_triangle_mesh(const Contour3D& ctour); + +/// Mesh from an evaporating 3D contour +TriangleMesh to_triangle_mesh(Contour3D&& ctour); + +}} // namespace Slic3r::sla + +#endif // CONTOUR3D_HPP diff --git a/src/libslic3r/SLA/EigenMesh3D.hpp b/src/libslic3r/SLA/EigenMesh3D.hpp new file mode 100644 index 0000000000..8d5b3b8dfd --- /dev/null +++ b/src/libslic3r/SLA/EigenMesh3D.hpp @@ -0,0 +1,156 @@ +#ifndef SLA_EIGENMESH3D_H +#define SLA_EIGENMESH3D_H + +#include +#include "libslic3r/SLA/Hollowing.hpp" + +namespace Slic3r { + +class TriangleMesh; + +namespace sla { + +struct Contour3D; + +/// An index-triangle structure for libIGL functions. Also serves as an +/// alternative (raw) input format for the SLASupportTree. +// Implemented in libslic3r/SLA/Common.cpp +class EigenMesh3D { + class AABBImpl; + + Eigen::MatrixXd m_V; + Eigen::MatrixXi m_F; + double m_ground_level = 0, m_gnd_offset = 0; + + std::unique_ptr m_aabb; + + // This holds a copy of holes in the mesh. Initialized externally + // by load_mesh setter. + std::vector m_holes; + +public: + + EigenMesh3D(const TriangleMesh&); + EigenMesh3D(const EigenMesh3D& other); + EigenMesh3D(const Contour3D &other); + EigenMesh3D& operator=(const EigenMesh3D&); + + ~EigenMesh3D(); + + inline double ground_level() const { return m_ground_level + m_gnd_offset; } + inline void ground_level_offset(double o) { m_gnd_offset = o; } + inline double ground_level_offset() const { return m_gnd_offset; } + + inline const Eigen::MatrixXd& V() const { return m_V; } + inline const Eigen::MatrixXi& F() const { return m_F; } + + // Result of a raycast + class hit_result { + // m_t holds a distance from m_source to the intersection. + double m_t = infty(); + const EigenMesh3D *m_mesh = nullptr; + Vec3d m_dir; + Vec3d m_source; + Vec3d m_normal; + friend class EigenMesh3D; + + // A valid object of this class can only be obtained from + // EigenMesh3D::query_ray_hit method. + explicit inline hit_result(const EigenMesh3D& em): m_mesh(&em) {} + public: + // This denotes no hit on the mesh. + static inline constexpr double infty() { return std::numeric_limits::infinity(); } + + explicit inline hit_result(double val = infty()) : m_t(val) {} + + inline double distance() const { return m_t; } + inline const Vec3d& direction() const { return m_dir; } + inline const Vec3d& source() const { return m_source; } + inline Vec3d position() const { return m_source + m_dir * m_t; } + inline bool is_valid() const { return m_mesh != nullptr; } + inline bool is_hit() const { return !std::isinf(m_t); } + + // Hit_result can decay into a double as the hit distance. + inline operator double() const { return distance(); } + + inline const Vec3d& normal() const { + assert(is_valid()); + return m_normal; + } + + inline bool is_inside() const { + return is_hit() && normal().dot(m_dir) > 0; + } + }; + + // Inform the object about location of holes + // creates internal copy of the vector + void load_holes(const std::vector& holes) { + m_holes = holes; + } + + // Casting a ray on the mesh, returns the distance where the hit occures. + hit_result query_ray_hit(const Vec3d &s, const Vec3d &dir) const; + + // Casts a ray on the mesh and returns all hits + std::vector query_ray_hits(const Vec3d &s, const Vec3d &dir) const; + + // Iterates over hits and holes and returns the true hit, possibly + // on the inside of a hole. + hit_result filter_hits(const std::vector& obj_hits) const; + + class si_result { + double m_value; + int m_fidx; + Vec3d m_p; + si_result(double val, int i, const Vec3d& c): + m_value(val), m_fidx(i), m_p(c) {} + friend class EigenMesh3D; + public: + + si_result() = delete; + + double value() const { return m_value; } + operator double() const { return m_value; } + const Vec3d& point_on_mesh() const { return m_p; } + int F_idx() const { return m_fidx; } + }; + +#ifdef SLIC3R_SLA_NEEDS_WINDTREE + // The signed distance from a point to the mesh. Outputs the distance, + // the index of the triangle and the closest point in mesh coordinate space. + si_result signed_distance(const Vec3d& p) const; + + bool inside(const Vec3d& p) const; +#endif /* SLIC3R_SLA_NEEDS_WINDTREE */ + + double squared_distance(const Vec3d& p, int& i, Vec3d& c) const; + inline double squared_distance(const Vec3d &p) const + { + int i; + Vec3d c; + return squared_distance(p, i, c); + } + + Vec3d normal_by_face_id(int face_id) const { + auto trindex = F().row(face_id); + const Vec3d& p1 = V().row(trindex(0)); + const Vec3d& p2 = V().row(trindex(1)); + const Vec3d& p3 = V().row(trindex(2)); + Eigen::Vector3d U = p2 - p1; + Eigen::Vector3d V = p3 - p1; + return U.cross(V).normalized(); + } +}; + +// Calculate the normals for the selected points (from 'points' set) on the +// mesh. This will call squared distance for each point. +PointSet normals(const PointSet& points, + const EigenMesh3D& convert_mesh, + double eps = 0.05, // min distance from edges + std::function throw_on_cancel = [](){}, + const std::vector& selected_points = {}); + +}} // namespace Slic3r::sla + +#endif // EIGENMESH3D_H diff --git a/src/libslic3r/SLA/Hollowing.cpp b/src/libslic3r/SLA/Hollowing.cpp new file mode 100644 index 0000000000..1ce0c4c679 --- /dev/null +++ b/src/libslic3r/SLA/Hollowing.cpp @@ -0,0 +1,283 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include + +//! macro used to mark string used at localization, +//! return same string +#define L(s) Slic3r::I18N::translate(s) + +namespace Slic3r { +namespace sla { + +template> +inline void _scale(S s, TriangleMesh &m) { m.scale(float(s)); } + +template> +inline void _scale(S s, Contour3D &m) { for (auto &p : m.points) p *= s; } + +static TriangleMesh _generate_interior(const TriangleMesh &mesh, + const JobController &ctl, + double min_thickness, + double voxel_scale, + double closing_dist) +{ + TriangleMesh imesh{mesh}; + + _scale(voxel_scale, imesh); + + double offset = voxel_scale * min_thickness; + double D = voxel_scale * closing_dist; + float out_range = 0.1f * float(offset); + float in_range = 1.1f * float(offset + D); + + if (ctl.stopcondition()) return {}; + else ctl.statuscb(0, L("Hollowing")); + + auto gridptr = mesh_to_grid(imesh, {}, out_range, in_range); + + assert(gridptr); + + if (!gridptr) { + BOOST_LOG_TRIVIAL(error) << "Returned OpenVDB grid is NULL"; + return {}; + } + + if (ctl.stopcondition()) return {}; + else ctl.statuscb(30, L("Hollowing")); + + if (closing_dist > .0) { + gridptr = redistance_grid(*gridptr, -(offset + D), double(in_range)); + } else { + D = -offset; + } + + if (ctl.stopcondition()) return {}; + else ctl.statuscb(70, L("Hollowing")); + + double iso_surface = D; + double adaptivity = 0.; + auto omesh = grid_to_mesh(*gridptr, iso_surface, adaptivity); + + _scale(1. / voxel_scale, omesh); + + if (ctl.stopcondition()) return {}; + else ctl.statuscb(100, L("Hollowing")); + + return omesh; +} + +std::unique_ptr generate_interior(const TriangleMesh & mesh, + const HollowingConfig &hc, + const JobController & ctl) +{ + static const double MIN_OVERSAMPL = 3.; + static const double MAX_OVERSAMPL = 8.; + + // I can't figure out how to increase the grid resolution through openvdb + // API so the model will be scaled up before conversion and the result + // scaled down. Voxels have a unit size. If I set voxelSize smaller, it + // scales the whole geometry down, and doesn't increase the number of + // voxels. + // + // max 8x upscale, min is native voxel size + auto voxel_scale = MIN_OVERSAMPL + (MAX_OVERSAMPL - MIN_OVERSAMPL) * hc.quality; + auto meshptr = std::make_unique( + _generate_interior(mesh, ctl, hc.min_thickness, voxel_scale, + hc.closing_distance)); + + if (meshptr) { + + // This flips the normals to be outward facing... + meshptr->require_shared_vertices(); + indexed_triangle_set its = std::move(meshptr->its); + + Slic3r::simplify_mesh(its); + + // flip normals back... + for (stl_triangle_vertex_indices &ind : its.indices) + std::swap(ind(0), ind(2)); + + *meshptr = Slic3r::TriangleMesh{its}; + } + + return meshptr; +} + +Contour3D DrainHole::to_mesh() const +{ + auto r = double(radius); + auto h = double(height); + sla::Contour3D hole = sla::cylinder(r, h); + Eigen::Quaterniond q; + q.setFromTwoVectors(Vec3d{0., 0., 1.}, normal.cast()); + for(auto& p : hole.points) p = q * p + pos.cast(); + return hole; +} + +bool DrainHole::operator==(const DrainHole &sp) const +{ + return (pos == sp.pos) && (normal == sp.normal) && + is_approx(radius, sp.radius) && + is_approx(height, sp.height); +} + +bool DrainHole::is_inside(const Vec3f& pt) const +{ + Eigen::Hyperplane plane(normal, pos); + float dist = plane.signedDistance(pt); + if (dist < float(EPSILON) || dist > height) + return false; + + Eigen::ParametrizedLine axis(pos, normal); + if ( axis.squaredDistance(pt) < pow(radius, 2.f)) + return true; + + return false; +} + + +// Given a line s+dir*t, find parameter t of intersections with the hole +// and the normal (points inside the hole). Outputs through out reference, +// returns true if two intersections were found. +bool DrainHole::get_intersections(const Vec3f& s, const Vec3f& dir, + std::array, 2>& out) + const +{ + assert(is_approx(normal.norm(), 1.f)); + const Eigen::ParametrizedLine ray(s, dir.normalized()); + + for (size_t i=0; i<2; ++i) + out[i] = std::make_pair(sla::EigenMesh3D::hit_result::infty(), Vec3d::Zero()); + + const float sqr_radius = pow(radius, 2.f); + + // first check a bounding sphere of the hole: + Vec3f center = pos+normal*height/2.f; + float sqr_dist_limit = pow(height/2.f, 2.f) + sqr_radius ; + if (ray.squaredDistance(center) > sqr_dist_limit) + return false; + + // The line intersects the bounding sphere, look for intersections with + // bases of the cylinder. + + size_t found = 0; // counts how many intersections were found + Eigen::Hyperplane base; + if (! is_approx(ray.direction().dot(normal), 0.f)) { + for (size_t i=1; i<=1; --i) { + Vec3f cylinder_center = pos+i*height*normal; + if (i == 0) { + // The hole base can be identical to mesh surface if it is flat + // let's better move the base outward a bit + cylinder_center -= EPSILON*normal; + } + base = Eigen::Hyperplane(normal, cylinder_center); + Vec3f intersection = ray.intersectionPoint(base); + // Only accept the point if it is inside the cylinder base. + if ((cylinder_center-intersection).squaredNorm() < sqr_radius) { + out[found].first = ray.intersectionParameter(base); + out[found].second = (i==0 ? 1. : -1.) * normal.cast(); + ++found; + } + } + } + else + { + // In case the line was perpendicular to the cylinder axis, previous + // block was skipped, but base will later be assumed to be valid. + base = Eigen::Hyperplane(normal, pos-EPSILON*normal); + } + + // In case there is still an intersection to be found, check the wall + if (found != 2 && ! is_approx(std::abs(ray.direction().dot(normal)), 1.f)) { + // Project the ray onto the base plane + Vec3f proj_origin = base.projection(ray.origin()); + Vec3f proj_dir = base.projection(ray.origin()+ray.direction())-proj_origin; + // save how the parameter scales and normalize the projected direction + float par_scale = proj_dir.norm(); + proj_dir = proj_dir/par_scale; + Eigen::ParametrizedLine projected_ray(proj_origin, proj_dir); + // Calculate point on the secant that's closest to the center + // and its distance to the circle along the projected line + Vec3f closest = projected_ray.projection(pos); + float dist = sqrt((sqr_radius - (closest-pos).squaredNorm())); + // Unproject both intersections on the original line and check + // they are on the cylinder and not past it: + for (int i=-1; i<=1 && found !=2; i+=2) { + Vec3f isect = closest + i*dist * projected_ray.direction(); + Vec3f to_isect = isect-proj_origin; + float par = to_isect.norm() / par_scale; + if (to_isect.normalized().dot(proj_dir.normalized()) < 0.f) + par *= -1.f; + Vec3d hit_normal = (pos-isect).normalized().cast(); + isect = ray.pointAt(par); + // check that the intersection is between the base planes: + float vert_dist = base.signedDistance(isect); + if (vert_dist > 0.f && vert_dist < height) { + out[found].first = par; + out[found].second = hit_normal; + ++found; + } + } + } + + // If only one intersection was found, it is some corner case, + // no intersection will be returned: + if (found != 2) + return false; + + // Sort the intersections: + if (out[0].first > out[1].first) + std::swap(out[0], out[1]); + + return true; +} + +void cut_drainholes(std::vector & obj_slices, + const std::vector &slicegrid, + float closing_radius, + const sla::DrainHoles & holes, + std::function thr) +{ + TriangleMesh mesh; + for (const sla::DrainHole &holept : holes) { + auto r = double(holept.radius); + auto h = double(holept.height); + sla::Contour3D hole = sla::cylinder(r, h); + Eigen::Quaterniond q; + q.setFromTwoVectors(Vec3d{0., 0., 1.}, holept.normal.cast()); + for(auto& p : hole.points) p = q * p + holept.pos.cast(); + mesh.merge(sla::to_triangle_mesh(hole)); + } + + if (mesh.empty()) return; + + mesh.require_shared_vertices(); + + TriangleMeshSlicer slicer(&mesh); + + std::vector hole_slices; + slicer.slice(slicegrid, closing_radius, &hole_slices, thr); + + if (obj_slices.size() != hole_slices.size()) + BOOST_LOG_TRIVIAL(warning) + << "Sliced object and drain-holes layer count does not match!"; + + size_t until = std::min(obj_slices.size(), hole_slices.size()); + + for (size_t i = 0; i < until; ++i) + obj_slices[i] = diff_ex(obj_slices[i], hole_slices[i]); +} + +}} // namespace Slic3r::sla diff --git a/src/libslic3r/SLA/Hollowing.hpp b/src/libslic3r/SLA/Hollowing.hpp new file mode 100644 index 0000000000..b3375ed1ae --- /dev/null +++ b/src/libslic3r/SLA/Hollowing.hpp @@ -0,0 +1,70 @@ +#ifndef SLA_HOLLOWING_HPP +#define SLA_HOLLOWING_HPP + +#include +#include +#include +#include + +namespace Slic3r { + +class TriangleMesh; + +namespace sla { + +struct HollowingConfig +{ + double min_thickness = 2.; + double quality = 0.5; + double closing_distance = 0.5; + bool enabled = true; +}; + +struct DrainHole +{ + Vec3f pos; + Vec3f normal; + float radius; + float height; + + DrainHole() + : pos(Vec3f::Zero()), normal(Vec3f::UnitZ()), radius(5.f), height(10.f) + {} + + DrainHole(Vec3f p, Vec3f n, float r, float h) + : pos(p), normal(n), radius(r), height(h) + {} + + bool operator==(const DrainHole &sp) const; + + bool operator!=(const DrainHole &sp) const { return !(sp == (*this)); } + + bool is_inside(const Vec3f& pt) const; + + bool get_intersections(const Vec3f& s, const Vec3f& dir, + std::array, 2>& out) const; + + Contour3D to_mesh() const; + + template inline void serialize(Archive &ar) + { + ar(pos, normal, radius, height); + } +}; + +using DrainHoles = std::vector; + +std::unique_ptr generate_interior(const TriangleMesh &mesh, + const HollowingConfig & = {}, + const JobController &ctl = {}); + +void cut_drainholes(std::vector & obj_slices, + const std::vector &slicegrid, + float closing_radius, + const sla::DrainHoles & holes, + std::function thr); + +} +} + +#endif // HOLLOWINGFILTER_H diff --git a/src/libslic3r/SLA/JobController.hpp b/src/libslic3r/SLA/JobController.hpp new file mode 100644 index 0000000000..3baa3d12d1 --- /dev/null +++ b/src/libslic3r/SLA/JobController.hpp @@ -0,0 +1,31 @@ +#ifndef SLA_JOBCONTROLLER_HPP +#define SLA_JOBCONTROLLER_HPP + +#include + +namespace Slic3r { namespace sla { + +/// A Control structure for the support calculation. Consists of the status +/// indicator callback and the stop condition predicate. +struct JobController +{ + using StatusFn = std::function; + using StopCond = std::function; + using CancelFn = std::function; + + // This will signal the status of the calculation to the front-end + StatusFn statuscb = [](unsigned, const std::string&){}; + + // Returns true if the calculation should be aborted. + StopCond stopcondition = [](){ return false; }; + + // Similar to cancel callback. This should check the stop condition and + // if true, throw an appropriate exception. (TriangleMeshSlicer needs this) + // consider it a hard abort. stopcondition is permits the algorithm to + // terminate itself + CancelFn cancelfn = [](){}; +}; + +}} // namespace Slic3r::sla + +#endif // JOBCONTROLLER_HPP diff --git a/src/libslic3r/SLA/SLAPad.cpp b/src/libslic3r/SLA/Pad.cpp similarity index 98% rename from src/libslic3r/SLA/SLAPad.cpp rename to src/libslic3r/SLA/Pad.cpp index 6f0db8e6c7..742f0db1bd 100644 --- a/src/libslic3r/SLA/SLAPad.cpp +++ b/src/libslic3r/SLA/Pad.cpp @@ -1,10 +1,12 @@ -#include "SLAPad.hpp" -#include "SLABoilerPlate.hpp" -#include "SLASpatIndex.hpp" +#include +#include +#include +#include +#include + #include "ConcaveHull.hpp" #include "boost/log/trivial.hpp" -#include "SLABoostAdapter.hpp" #include "ClipperUtils.hpp" #include "Tesselate.hpp" #include "MTUtils.hpp" @@ -69,7 +71,7 @@ Contour3D walls( // Shorthand for the vertex arrays auto& upts = upper.points, &lpts = lower.points; - auto& rpts = ret.points; auto& ind = ret.indices; + auto& rpts = ret.points; auto& ind = ret.faces3; // If the Z levels are flipped, or the offset difference is negative, we // will interpret that as the triangles normals should be inverted. @@ -676,7 +678,7 @@ void create_pad(const ExPolygons &sup_blueprint, ThrowOnCancel thr) { Contour3D t = create_pad_geometry(sup_blueprint, model_blueprint, cfg, thr); - out.merge(mesh(std::move(t))); + out.merge(to_triangle_mesh(std::move(t))); } std::string PadConfig::validate() const diff --git a/src/libslic3r/SLA/SLAPad.hpp b/src/libslic3r/SLA/Pad.hpp similarity index 98% rename from src/libslic3r/SLA/SLAPad.hpp rename to src/libslic3r/SLA/Pad.hpp index 4abcdd281e..61eec2dd1e 100644 --- a/src/libslic3r/SLA/SLAPad.hpp +++ b/src/libslic3r/SLA/Pad.hpp @@ -1,5 +1,5 @@ -#ifndef SLABASEPOOL_HPP -#define SLABASEPOOL_HPP +#ifndef SLA_PAD_HPP +#define SLA_PAD_HPP #include #include diff --git a/src/libslic3r/SLA/SLARaster.cpp b/src/libslic3r/SLA/Raster.cpp similarity index 99% rename from src/libslic3r/SLA/SLARaster.cpp rename to src/libslic3r/SLA/Raster.cpp index 091cadd231..9b7f1db7d9 100644 --- a/src/libslic3r/SLA/SLARaster.cpp +++ b/src/libslic3r/SLA/Raster.cpp @@ -3,7 +3,7 @@ #include -#include "SLARaster.hpp" +#include #include "libslic3r/ExPolygon.hpp" #include "libslic3r/MTUtils.hpp" #include diff --git a/src/libslic3r/SLA/SLARaster.hpp b/src/libslic3r/SLA/Raster.hpp similarity index 98% rename from src/libslic3r/SLA/SLARaster.hpp rename to src/libslic3r/SLA/Raster.hpp index b3d73536bb..4d76b32909 100644 --- a/src/libslic3r/SLA/SLARaster.hpp +++ b/src/libslic3r/SLA/Raster.hpp @@ -1,5 +1,5 @@ -#ifndef SLARASTER_HPP -#define SLARASTER_HPP +#ifndef SLA_RASTER_HPP +#define SLA_RASTER_HPP #include #include diff --git a/src/libslic3r/SLA/SLARasterWriter.cpp b/src/libslic3r/SLA/RasterWriter.cpp similarity index 96% rename from src/libslic3r/SLA/SLARasterWriter.cpp rename to src/libslic3r/SLA/RasterWriter.cpp index 6ac86827ef..238120dda0 100644 --- a/src/libslic3r/SLA/SLARasterWriter.cpp +++ b/src/libslic3r/SLA/RasterWriter.cpp @@ -1,6 +1,8 @@ -#include "SLARasterWriter.hpp" -#include "libslic3r/Zipper.hpp" -#include "libslic3r/Time.hpp" +#include + +#include "libslic3r/PrintConfig.hpp" +#include +#include #include "ExPolygon.hpp" #include diff --git a/src/libslic3r/SLA/SLARasterWriter.hpp b/src/libslic3r/SLA/RasterWriter.hpp similarity index 95% rename from src/libslic3r/SLA/SLARasterWriter.hpp rename to src/libslic3r/SLA/RasterWriter.hpp index 93a315c821..a472e44525 100644 --- a/src/libslic3r/SLA/SLARasterWriter.hpp +++ b/src/libslic3r/SLA/RasterWriter.hpp @@ -1,5 +1,5 @@ -#ifndef SLARASTERWRITER_HPP -#define SLARASTERWRITER_HPP +#ifndef SLA_RASTERWRITER_HPP +#define SLA_RASTERWRITER_HPP // For png export of the sliced model #include @@ -9,12 +9,14 @@ #include #include -#include "libslic3r/PrintConfig.hpp" +#include +#include -#include "SLARaster.hpp" -#include "libslic3r/Zipper.hpp" +namespace Slic3r { -namespace Slic3r { namespace sla { +class DynamicPrintConfig; + +namespace sla { // API to write the zipped sla output layers and metadata. // Implementation uses PNG raster output. @@ -116,7 +118,7 @@ public: void save(Zipper &zipper, const std::string &prjname = ""); void set_statistics(const PrintStatistics &statistics); - + void set_config(const DynamicPrintConfig &cfg); }; diff --git a/src/libslic3r/SLA/SLARotfinder.cpp b/src/libslic3r/SLA/Rotfinder.cpp similarity index 97% rename from src/libslic3r/SLA/SLARotfinder.cpp rename to src/libslic3r/SLA/Rotfinder.cpp index 2e64059d68..9ec9837e2a 100644 --- a/src/libslic3r/SLA/SLARotfinder.cpp +++ b/src/libslic3r/SLA/Rotfinder.cpp @@ -2,9 +2,9 @@ #include #include -#include "SLABoilerPlate.hpp" -#include "SLARotfinder.hpp" -#include "SLASupportTree.hpp" +#include +#include +#include #include "Model.hpp" namespace Slic3r { diff --git a/src/libslic3r/SLA/SLARotfinder.hpp b/src/libslic3r/SLA/Rotfinder.hpp similarity index 95% rename from src/libslic3r/SLA/SLARotfinder.hpp rename to src/libslic3r/SLA/Rotfinder.hpp index b8cec859e2..4469f9731d 100644 --- a/src/libslic3r/SLA/SLARotfinder.hpp +++ b/src/libslic3r/SLA/Rotfinder.hpp @@ -1,5 +1,5 @@ -#ifndef SLAROTFINDER_HPP -#define SLAROTFINDER_HPP +#ifndef SLA_ROTFINDER_HPP +#define SLA_ROTFINDER_HPP #include #include diff --git a/src/libslic3r/SLA/SLABoilerPlate.hpp b/src/libslic3r/SLA/SLABoilerPlate.hpp deleted file mode 100644 index d7ce26bb2b..0000000000 --- a/src/libslic3r/SLA/SLABoilerPlate.hpp +++ /dev/null @@ -1,103 +0,0 @@ -#ifndef SLABOILERPLATE_HPP -#define SLABOILERPLATE_HPP - -#include -#include -#include - -#include -#include - -#include "SLACommon.hpp" -#include "SLASpatIndex.hpp" - -namespace Slic3r { -namespace sla { - -/// Intermediate struct for a 3D mesh -struct Contour3D { - Pointf3s points; - std::vector indices; - - Contour3D& merge(const Contour3D& ctr) - { - auto s3 = coord_t(points.size()); - auto s = indices.size(); - - points.insert(points.end(), ctr.points.begin(), ctr.points.end()); - indices.insert(indices.end(), ctr.indices.begin(), ctr.indices.end()); - - for(size_t n = s; n < indices.size(); n++) { - auto& idx = indices[n]; idx.x() += s3; idx.y() += s3; idx.z() += s3; - } - - return *this; - } - - Contour3D& merge(const Pointf3s& triangles) - { - const size_t offs = points.size(); - points.insert(points.end(), triangles.begin(), triangles.end()); - indices.reserve(indices.size() + points.size() / 3); - - for(int i = int(offs); i < int(points.size()); i += 3) - indices.emplace_back(i, i + 1, i + 2); - - return *this; - } - - // Write the index triangle structure to OBJ file for debugging purposes. - void to_obj(std::ostream& stream) - { - for(auto& p : points) { - stream << "v " << p.transpose() << "\n"; - } - - for(auto& f : indices) { - stream << "f " << (f + Vec3i(1, 1, 1)).transpose() << "\n"; - } - } -}; - -using ClusterEl = std::vector; -using ClusteredPoints = std::vector; - -// Clustering a set of points by the given distance. -ClusteredPoints cluster(const std::vector& indices, - std::function pointfn, - double dist, - unsigned max_points); - -ClusteredPoints cluster(const PointSet& points, - double dist, - unsigned max_points); - -ClusteredPoints cluster( - const std::vector& indices, - std::function pointfn, - std::function predicate, - unsigned max_points); - - -// Calculate the normals for the selected points (from 'points' set) on the -// mesh. This will call squared distance for each point. -PointSet normals(const PointSet& points, - const EigenMesh3D& mesh, - double eps = 0.05, // min distance from edges - std::function throw_on_cancel = [](){}, - const std::vector& selected_points = {}); - -/// Mesh from an existing contour. -inline TriangleMesh mesh(const Contour3D& ctour) { - return {ctour.points, ctour.indices}; -} - -/// Mesh from an evaporating 3D contour -inline TriangleMesh mesh(Contour3D&& ctour) { - return {std::move(ctour.points), std::move(ctour.indices)}; -} - -} -} - -#endif // SLABOILERPLATE_HPP diff --git a/src/libslic3r/SLA/SLACommon.hpp b/src/libslic3r/SLA/SLACommon.hpp deleted file mode 100644 index 97b4596761..0000000000 --- a/src/libslic3r/SLA/SLACommon.hpp +++ /dev/null @@ -1,187 +0,0 @@ -#ifndef SLACOMMON_HPP -#define SLACOMMON_HPP - -#include -#include -#include - -// #define SLIC3R_SLA_NEEDS_WINDTREE - -namespace Slic3r { - -// Typedefs from Point.hpp -typedef Eigen::Matrix Vec3f; -typedef Eigen::Matrix Vec3d; - -class TriangleMesh; - -namespace sla { - -// An enum to keep track of where the current points on the ModelObject came from. -enum class PointsStatus { - NoPoints, // No points were generated so far. - Generating, // The autogeneration algorithm triggered, but not yet finished. - AutoGenerated, // Points were autogenerated (i.e. copied from the backend). - UserModified // User has done some edits. -}; - -struct SupportPoint -{ - Vec3f pos; - float head_front_radius; - bool is_new_island; - - SupportPoint() - : pos(Vec3f::Zero()), head_front_radius(0.f), is_new_island(false) - {} - - SupportPoint(float pos_x, - float pos_y, - float pos_z, - float head_radius, - bool new_island) - : pos(pos_x, pos_y, pos_z) - , head_front_radius(head_radius) - , is_new_island(new_island) - {} - - SupportPoint(Vec3f position, float head_radius, bool new_island) - : pos(position) - , head_front_radius(head_radius) - , is_new_island(new_island) - {} - - SupportPoint(Eigen::Matrix data) - : pos(data(0), data(1), data(2)) - , head_front_radius(data(3)) - , is_new_island(data(4) != 0.f) - {} - - bool operator==(const SupportPoint &sp) const - { - return (pos == sp.pos) && head_front_radius == sp.head_front_radius && - is_new_island == sp.is_new_island; - } - bool operator!=(const SupportPoint &sp) const { return !(sp == (*this)); } - - template void serialize(Archive &ar) - { - ar(pos, head_front_radius, is_new_island); - } -}; - -using SupportPoints = std::vector; - -/// An index-triangle structure for libIGL functions. Also serves as an -/// alternative (raw) input format for the SLASupportTree -class EigenMesh3D { - class AABBImpl; - - Eigen::MatrixXd m_V; - Eigen::MatrixXi m_F; - double m_ground_level = 0, m_gnd_offset = 0; - - std::unique_ptr m_aabb; -public: - - EigenMesh3D(const TriangleMesh&); - EigenMesh3D(const EigenMesh3D& other); - EigenMesh3D& operator=(const EigenMesh3D&); - - ~EigenMesh3D(); - - inline double ground_level() const { return m_ground_level + m_gnd_offset; } - inline void ground_level_offset(double o) { m_gnd_offset = o; } - inline double ground_level_offset() const { return m_gnd_offset; } - - inline const Eigen::MatrixXd& V() const { return m_V; } - inline const Eigen::MatrixXi& F() const { return m_F; } - - // Result of a raycast - class hit_result { - double m_t = std::nan(""); - int m_face_id = -1; - const EigenMesh3D *m_mesh = nullptr; - Vec3d m_dir; - Vec3d m_source; - friend class EigenMesh3D; - - // A valid object of this class can only be obtained from - // EigenMesh3D::query_ray_hit method. - explicit inline hit_result(const EigenMesh3D& em): m_mesh(&em) {} - public: - - // This can create a placeholder object which is invalid (not created - // by a query_ray_hit call) but the distance can be preset to - // a specific value for distinguishing the placeholder. - inline hit_result(double val = std::nan("")): m_t(val) {} - - inline double distance() const { return m_t; } - inline const Vec3d& direction() const { return m_dir; } - inline Vec3d position() const { return m_source + m_dir * m_t; } - inline int face() const { return m_face_id; } - inline bool is_valid() const { return m_mesh != nullptr; } - - // Hit_result can decay into a double as the hit distance. - inline operator double() const { return distance(); } - - inline Vec3d normal() const { - if(m_face_id < 0 || !is_valid()) return {}; - auto trindex = m_mesh->m_F.row(m_face_id); - const Vec3d& p1 = m_mesh->V().row(trindex(0)); - const Vec3d& p2 = m_mesh->V().row(trindex(1)); - const Vec3d& p3 = m_mesh->V().row(trindex(2)); - Eigen::Vector3d U = p2 - p1; - Eigen::Vector3d V = p3 - p1; - return U.cross(V).normalized(); - } - - inline bool is_inside() { - return m_face_id >= 0 && normal().dot(m_dir) > 0; - } - }; - - // Casting a ray on the mesh, returns the distance where the hit occures. - hit_result query_ray_hit(const Vec3d &s, const Vec3d &dir) const; - - class si_result { - double m_value; - int m_fidx; - Vec3d m_p; - si_result(double val, int i, const Vec3d& c): - m_value(val), m_fidx(i), m_p(c) {} - friend class EigenMesh3D; - public: - - si_result() = delete; - - double value() const { return m_value; } - operator double() const { return m_value; } - const Vec3d& point_on_mesh() const { return m_p; } - int F_idx() const { return m_fidx; } - }; - -#ifdef SLIC3R_SLA_NEEDS_WINDTREE - // The signed distance from a point to the mesh. Outputs the distance, - // the index of the triangle and the closest point in mesh coordinate space. - si_result signed_distance(const Vec3d& p) const; - - bool inside(const Vec3d& p) const; -#endif /* SLIC3R_SLA_NEEDS_WINDTREE */ - - double squared_distance(const Vec3d& p, int& i, Vec3d& c) const; - inline double squared_distance(const Vec3d &p) const - { - int i; - Vec3d c; - return squared_distance(p, i, c); - } -}; - -using PointSet = Eigen::MatrixXd; - -} // namespace sla -} // namespace Slic3r - - -#endif // SLASUPPORTTREE_HPP diff --git a/src/libslic3r/SLA/SLASpatIndex.hpp b/src/libslic3r/SLA/SpatIndex.hpp similarity index 97% rename from src/libslic3r/SLA/SLASpatIndex.hpp rename to src/libslic3r/SLA/SpatIndex.hpp index 20b6fcd589..2955cdcdf6 100644 --- a/src/libslic3r/SLA/SLASpatIndex.hpp +++ b/src/libslic3r/SLA/SpatIndex.hpp @@ -1,5 +1,5 @@ -#ifndef SPATINDEX_HPP -#define SPATINDEX_HPP +#ifndef SLA_SPATINDEX_HPP +#define SLA_SPATINDEX_HPP #include #include diff --git a/src/libslic3r/SLA/SupportPoint.hpp b/src/libslic3r/SLA/SupportPoint.hpp new file mode 100644 index 0000000000..202a614c32 --- /dev/null +++ b/src/libslic3r/SLA/SupportPoint.hpp @@ -0,0 +1,69 @@ +#ifndef SLA_SUPPORTPOINT_HPP +#define SLA_SUPPORTPOINT_HPP + +#include +#include +#include + +namespace Slic3r { namespace sla { + +// An enum to keep track of where the current points on the ModelObject came from. +enum class PointsStatus { + NoPoints, // No points were generated so far. + Generating, // The autogeneration algorithm triggered, but not yet finished. + AutoGenerated, // Points were autogenerated (i.e. copied from the backend). + UserModified // User has done some edits. +}; + +struct SupportPoint +{ + Vec3f pos; + float head_front_radius; + bool is_new_island; + + SupportPoint() + : pos(Vec3f::Zero()), head_front_radius(0.f), is_new_island(false) + {} + + SupportPoint(float pos_x, + float pos_y, + float pos_z, + float head_radius, + bool new_island) + : pos(pos_x, pos_y, pos_z) + , head_front_radius(head_radius) + , is_new_island(new_island) + {} + + SupportPoint(Vec3f position, float head_radius, bool new_island) + : pos(position) + , head_front_radius(head_radius) + , is_new_island(new_island) + {} + + SupportPoint(Eigen::Matrix data) + : pos(data(0), data(1), data(2)) + , head_front_radius(data(3)) + , is_new_island(data(4) != 0.f) + {} + + bool operator==(const SupportPoint &sp) const + { + float rdiff = std::abs(head_front_radius - sp.head_front_radius); + return (pos == sp.pos) && rdiff < float(EPSILON) && + is_new_island == sp.is_new_island; + } + + bool operator!=(const SupportPoint &sp) const { return !(sp == (*this)); } + + template void serialize(Archive &ar) + { + ar(pos, head_front_radius, is_new_island); + } +}; + +using SupportPoints = std::vector; + +}} // namespace Slic3r::sla + +#endif // SUPPORTPOINT_HPP diff --git a/src/libslic3r/SLA/SLAAutoSupports.cpp b/src/libslic3r/SLA/SupportPointGenerator.cpp similarity index 89% rename from src/libslic3r/SLA/SLAAutoSupports.cpp rename to src/libslic3r/SLA/SupportPointGenerator.cpp index 65f5901431..78c2ced356 100644 --- a/src/libslic3r/SLA/SLAAutoSupports.cpp +++ b/src/libslic3r/SLA/SupportPointGenerator.cpp @@ -3,7 +3,7 @@ #include -#include "SLAAutoSupports.hpp" +#include "SupportPointGenerator.hpp" #include "Model.hpp" #include "ExPolygon.hpp" #include "SVG.hpp" @@ -18,7 +18,7 @@ namespace Slic3r { namespace sla { -/*float SLAAutoSupports::approximate_geodesic_distance(const Vec3d& p1, const Vec3d& p2, Vec3d& n1, Vec3d& n2) +/*float SupportPointGenerator::approximate_geodesic_distance(const Vec3d& p1, const Vec3d& p2, Vec3d& n1, Vec3d& n2) { n1.normalize(); n2.normalize(); @@ -36,7 +36,7 @@ namespace sla { } -float SLAAutoSupports::get_required_density(float angle) const +float SupportPointGenerator::get_required_density(float angle) const { // calculation would be density_0 * cos(angle). To provide one more degree of freedom, we will scale the angle // to get the user-set density for 45 deg. So it ends up as density_0 * cos(K * angle). @@ -44,27 +44,45 @@ float SLAAutoSupports::get_required_density(float angle) const return std::max(0.f, float(m_config.density_at_horizontal * cos(K*angle))); } -float SLAAutoSupports::distance_limit(float angle) const +float SupportPointGenerator::distance_limit(float angle) const { return 1./(2.4*get_required_density(angle)); }*/ -SLAAutoSupports::SLAAutoSupports(const sla::EigenMesh3D & emesh, - const std::vector &slices, - const std::vector & heights, - const Config & config, - std::function throw_on_cancel, - std::function statusfn) +SupportPointGenerator::SupportPointGenerator( + const sla::EigenMesh3D &emesh, + const std::vector &slices, + const std::vector & heights, + const Config & config, + std::function throw_on_cancel, + std::function statusfn) + : SupportPointGenerator(emesh, config, throw_on_cancel, statusfn) +{ + std::random_device rd; + m_rng.seed(rd()); + execute(slices, heights); +} + +SupportPointGenerator::SupportPointGenerator( + const EigenMesh3D &emesh, + const SupportPointGenerator::Config &config, + std::function throw_on_cancel, + std::function statusfn) : m_config(config) , m_emesh(emesh) , m_throw_on_cancel(throw_on_cancel) , m_statusfn(statusfn) +{ +} + +void SupportPointGenerator::execute(const std::vector &slices, + const std::vector & heights) { process(slices, heights); project_onto_mesh(m_output); } -void SLAAutoSupports::project_onto_mesh(std::vector& points) const +void SupportPointGenerator::project_onto_mesh(std::vector& points) const { // The function makes sure that all the points are really exactly placed on the mesh. @@ -77,36 +95,29 @@ void SLAAutoSupports::project_onto_mesh(std::vector& points) m_throw_on_cancel(); Vec3f& p = points[point_id].pos; // Project the point upward and downward and choose the closer intersection with the mesh. - //bool up = igl::ray_mesh_intersect(p.cast(), Vec3f(0., 0., 1.), m_V, m_F, hit_up); - //bool down = igl::ray_mesh_intersect(p.cast(), Vec3f(0., 0., -1.), m_V, m_F, hit_down); - sla::EigenMesh3D::hit_result hit_up = m_emesh.query_ray_hit(p.cast(), Vec3d(0., 0., 1.)); sla::EigenMesh3D::hit_result hit_down = m_emesh.query_ray_hit(p.cast(), Vec3d(0., 0., -1.)); - bool up = hit_up.face() != -1; - bool down = hit_down.face() != -1; + bool up = hit_up.is_hit(); + bool down = hit_down.is_hit(); if (!up && !down) continue; sla::EigenMesh3D::hit_result& hit = (!down || (hit_up.distance() < hit_down.distance())) ? hit_up : hit_down; - //int fid = hit.face(); - //Vec3f bc(1-hit.u-hit.v, hit.u, hit.v); - //p = (bc(0) * m_V.row(m_F(fid, 0)) + bc(1) * m_V.row(m_F(fid, 1)) + bc(2)*m_V.row(m_F(fid, 2))).cast(); - p = p + (hit.distance() * hit.direction()).cast(); } }); } -static std::vector make_layers( +static std::vector make_layers( const std::vector& slices, const std::vector& heights, std::function throw_on_cancel) { assert(slices.size() == heights.size()); // Allocate empty layers. - std::vector layers; + std::vector layers; layers.reserve(slices.size()); for (size_t i = 0; i < slices.size(); ++ i) layers.emplace_back(i, heights[i]); @@ -122,7 +133,7 @@ static std::vector make_layers( if ((layer_id % 8) == 0) // Don't call the following function too often as it flushes CPU write caches due to synchronization primitves. throw_on_cancel(); - SLAAutoSupports::MyLayer &layer = layers[layer_id]; + SupportPointGenerator::MyLayer &layer = layers[layer_id]; const ExPolygons &islands = slices[layer_id]; //FIXME WTF? const float height = (layer_id>2 ? heights[layer_id-3] : heights[0]-(heights[1]-heights[0])); @@ -143,8 +154,8 @@ static std::vector make_layers( if ((layer_id % 2) == 0) // Don't call the following function too often as it flushes CPU write caches due to synchronization primitves. throw_on_cancel(); - SLAAutoSupports::MyLayer &layer_above = layers[layer_id]; - SLAAutoSupports::MyLayer &layer_below = layers[layer_id - 1]; + SupportPointGenerator::MyLayer &layer_above = layers[layer_id]; + SupportPointGenerator::MyLayer &layer_below = layers[layer_id - 1]; //FIXME WTF? const float layer_height = (layer_id!=0 ? heights[layer_id]-heights[layer_id-1] : heights[0]); const float safe_angle = 5.f * (float(M_PI)/180.f); // smaller number - less supports @@ -152,8 +163,8 @@ static std::vector make_layers( const float slope_angle = 75.f * (float(M_PI)/180.f); // smaller number - less supports const float slope_offset = float(scale_(layer_height / std::tan(slope_angle))); //FIXME This has a quadratic time complexity, it will be excessively slow for many tiny islands. - for (SLAAutoSupports::Structure &top : layer_above.islands) { - for (SLAAutoSupports::Structure &bottom : layer_below.islands) { + for (SupportPointGenerator::Structure &top : layer_above.islands) { + for (SupportPointGenerator::Structure &bottom : layer_below.islands) { float overlap_area = top.overlap_area(bottom); if (overlap_area > 0) { top.islands_below.emplace_back(&bottom, overlap_area); @@ -191,13 +202,13 @@ static std::vector make_layers( return layers; } -void SLAAutoSupports::process(const std::vector& slices, const std::vector& heights) +void SupportPointGenerator::process(const std::vector& slices, const std::vector& heights) { -#ifdef SLA_AUTOSUPPORTS_DEBUG +#ifdef SLA_SUPPORTPOINTGEN_DEBUG std::vector> islands; -#endif /* SLA_AUTOSUPPORTS_DEBUG */ +#endif /* SLA_SUPPORTPOINTGEN_DEBUG */ - std::vector layers = make_layers(slices, heights, m_throw_on_cancel); + std::vector layers = make_layers(slices, heights, m_throw_on_cancel); PointGrid3D point_grid; point_grid.cell_size = Vec3f(10.f, 10.f, 10.f); @@ -206,8 +217,8 @@ void SLAAutoSupports::process(const std::vector& slices, const std:: double status = 0; for (unsigned int layer_id = 0; layer_id < layers.size(); ++ layer_id) { - SLAAutoSupports::MyLayer *layer_top = &layers[layer_id]; - SLAAutoSupports::MyLayer *layer_bottom = (layer_id > 0) ? &layers[layer_id - 1] : nullptr; + SupportPointGenerator::MyLayer *layer_top = &layers[layer_id]; + SupportPointGenerator::MyLayer *layer_bottom = (layer_id > 0) ? &layers[layer_id - 1] : nullptr; std::vector support_force_bottom; if (layer_bottom != nullptr) { support_force_bottom.assign(layer_bottom->islands.size(), 0.f); @@ -263,13 +274,13 @@ void SLAAutoSupports::process(const std::vector& slices, const std:: status += increment; m_statusfn(int(std::round(status))); -#ifdef SLA_AUTOSUPPORTS_DEBUG +#ifdef SLA_SUPPORTPOINTGEN_DEBUG /*std::string layer_num_str = std::string((i<10 ? "0" : "")) + std::string((i<100 ? "0" : "")) + std::to_string(i); output_expolygons(expolys_top, "top" + layer_num_str + ".svg"); output_expolygons(diff, "diff" + layer_num_str + ".svg"); if (!islands.empty()) output_expolygons(islands, "islands" + layer_num_str + ".svg");*/ -#endif /* SLA_AUTOSUPPORTS_DEBUG */ +#endif /* SLA_SUPPORTPOINTGEN_DEBUG */ } } @@ -449,7 +460,7 @@ static inline std::vector poisson_disk_from_samples(const std::vector raw_samples = sample_expolygon_with_boundary(islands, samples_per_mm2, 5.f / poisson_radius, rng); + + std::vector raw_samples = sample_expolygon_with_boundary(islands, samples_per_mm2, 5.f / poisson_radius, m_rng); std::vector poisson_samples; for (size_t iter = 0; iter < 4; ++ iter) { poisson_samples = poisson_disk_from_samples(raw_samples, poisson_radius, @@ -488,7 +498,7 @@ void SLAAutoSupports::uniformly_cover(const ExPolygons& islands, Structure& stru min_spacing = std::max(m_config.minimal_distance, min_spacing * coeff); } -#ifdef SLA_AUTOSUPPORTS_DEBUG +#ifdef SLA_SUPPORTPOINTGEN_DEBUG { static int irun = 0; Slic3r::SVG svg(debug_out_path("SLA_supports-uniformly_cover-%d.svg", irun ++), get_extents(islands)); @@ -503,7 +513,7 @@ void SLAAutoSupports::uniformly_cover(const ExPolygons& islands, Structure& stru // assert(! poisson_samples.empty()); if (poisson_samples_target < poisson_samples.size()) { - std::shuffle(poisson_samples.begin(), poisson_samples.end(), rng); + std::shuffle(poisson_samples.begin(), poisson_samples.end(), m_rng); poisson_samples.erase(poisson_samples.begin() + poisson_samples_target, poisson_samples.end()); } for (const Vec2f &pt : poisson_samples) { @@ -528,8 +538,8 @@ void remove_bottom_points(std::vector &pts, double gnd_lvl, double pts.erase(endit, pts.end()); } -#ifdef SLA_AUTOSUPPORTS_DEBUG -void SLAAutoSupports::output_structures(const std::vector& structures) +#ifdef SLA_SUPPORTPOINTGEN_DEBUG +void SupportPointGenerator::output_structures(const std::vector& structures) { for (unsigned int i=0 ; i& structures } } -void SLAAutoSupports::output_expolygons(const ExPolygons& expolys, const std::string &filename) +void SupportPointGenerator::output_expolygons(const ExPolygons& expolys, const std::string &filename) { BoundingBox bb(Point(-30000000, -30000000), Point(30000000, 30000000)); Slic3r::SVG svg_cummulative(filename, bb); diff --git a/src/libslic3r/SLA/SLAAutoSupports.hpp b/src/libslic3r/SLA/SupportPointGenerator.hpp similarity index 66% rename from src/libslic3r/SLA/SLAAutoSupports.hpp rename to src/libslic3r/SLA/SupportPointGenerator.hpp index d2f50f0a46..738fc57303 100644 --- a/src/libslic3r/SLA/SLAAutoSupports.hpp +++ b/src/libslic3r/SLA/SupportPointGenerator.hpp @@ -1,43 +1,50 @@ -#ifndef SLAAUTOSUPPORTS_HPP_ -#define SLAAUTOSUPPORTS_HPP_ +#ifndef SLA_SUPPORTPOINTGENERATOR_HPP +#define SLA_SUPPORTPOINTGENERATOR_HPP + +#include + +#include +#include +#include #include #include #include -#include #include -// #define SLA_AUTOSUPPORTS_DEBUG +// #define SLA_SUPPORTPOINTGEN_DEBUG -namespace Slic3r { -namespace sla { +namespace Slic3r { namespace sla { -class SLAAutoSupports { +class SupportPointGenerator { public: struct Config { - float density_relative {1.f}; - float minimal_distance {1.f}; - float head_diameter {0.4f}; - /////////////// - inline float support_force() const { return 7.7f / density_relative; } // a force one point can support (arbitrary force unit) - inline float tear_pressure() const { return 1.f; } // pressure that the display exerts (the force unit per mm2) - }; - - SLAAutoSupports(const sla::EigenMesh3D& emesh, const std::vector& slices, - const std::vector& heights, const Config& config, std::function throw_on_cancel, std::function statusfn); + float density_relative {1.f}; + float minimal_distance {1.f}; + float head_diameter {0.4f}; + /////////////// + inline float support_force() const { return 7.7f / density_relative; } // a force one point can support (arbitrary force unit) + inline float tear_pressure() const { return 1.f; } // pressure that the display exerts (the force unit per mm2) + }; + + SupportPointGenerator(const EigenMesh3D& emesh, const std::vector& slices, + const std::vector& heights, const Config& config, std::function throw_on_cancel, std::function statusfn); + + SupportPointGenerator(const EigenMesh3D& emesh, const Config& config, std::function throw_on_cancel, std::function statusfn); + + const std::vector& output() const { return m_output; } + std::vector& output() { return m_output; } + + struct MyLayer; - const std::vector& output() { return m_output; } - - struct MyLayer; - struct Structure { Structure(MyLayer &layer, const ExPolygon& poly, const BoundingBox &bbox, const Vec2f ¢roid, float area, float h) : layer(&layer), polygon(&poly), bbox(bbox), centroid(centroid), area(area), height(h) -#ifdef SLA_AUTOSUPPORTS_DEBUG +#ifdef SLA_SUPPORTPOINTGEN_DEBUG , unique_id(std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch())) -#endif /* SLA_AUTOSUPPORTS_DEBUG */ - {} +#endif /* SLA_SUPPORTPOINTGEN_DEBUG */ + {} MyLayer *layer; const ExPolygon* polygon = nullptr; const BoundingBox bbox; @@ -49,24 +56,24 @@ public: float supports_force_this_layer = 0.f; float supports_force_inherited = 0.f; float supports_force_total() const { return this->supports_force_this_layer + this->supports_force_inherited; } -#ifdef SLA_AUTOSUPPORTS_DEBUG +#ifdef SLA_SUPPORTPOINTGEN_DEBUG std::chrono::milliseconds unique_id; -#endif /* SLA_AUTOSUPPORTS_DEBUG */ - +#endif /* SLA_SUPPORTPOINTGEN_DEBUG */ + struct Link { - Link(Structure *island, float overlap_area) : island(island), overlap_area(overlap_area) {} + Link(Structure *island, float overlap_area) : island(island), overlap_area(overlap_area) {} Structure *island; float overlap_area; }; #ifdef NDEBUG - // In release mode, use the optimized container. + // In release mode, use the optimized container. boost::container::small_vector islands_above; boost::container::small_vector islands_below; #else - // In debug mode, use the standard vector, which is well handled by debugger visualizer. - std::vector islands_above; - std::vector islands_below; + // In debug mode, use the standard vector, which is well handled by debugger visualizer. + std::vector islands_above; + std::vector islands_below; #endif // Overhangs, that are dangling considerably. ExPolygons dangling_areas; @@ -74,16 +81,16 @@ public: ExPolygons overhangs; // Overhangs, where the surface must slope. ExPolygons overhangs_slopes; - float overhangs_area; - + float overhangs_area = 0.f; + bool overlaps(const Structure &rhs) const { return this->bbox.overlap(rhs.bbox) && (this->polygon->overlaps(*rhs.polygon) || rhs.polygon->overlaps(*this->polygon)); } float overlap_area(const Structure &rhs) const { double out = 0.; if (this->bbox.overlap(rhs.bbox)) { - Polygons polys = intersection(to_polygons(*this->polygon), to_polygons(*rhs.polygon), false); - for (const Polygon &poly : polys) + Polygons polys = intersection(to_polygons(*this->polygon), to_polygons(*rhs.polygon), false); + for (const Polygon &poly : polys) out += poly.area(); } return float(out); @@ -96,13 +103,13 @@ public: } Polygons polygons_below() const { size_t cnt = 0; - for (const Link &below : this->islands_below) + for (const Link &below : this->islands_below) cnt += 1 + below.island->polygon->holes.size(); Polygons out; out.reserve(cnt); - for (const Link &below : this->islands_below) { + for (const Link &below : this->islands_below) { out.emplace_back(below.island->polygon->contour); - append(out, below.island->polygon->holes); + append(out, below.island->polygon->holes); } return out; } @@ -116,19 +123,19 @@ public: // Positive deficit of the supports. If negative, this area is well supported. If positive, more supports need to be added. float support_force_deficit(const float tear_pressure) const { return this->area * tear_pressure - this->supports_force_total(); } }; - + struct MyLayer { - MyLayer(const size_t layer_id, coordf_t print_z) : layer_id(layer_id), print_z(print_z) {} + MyLayer(const size_t layer_id, coordf_t print_z) : layer_id(layer_id), print_z(print_z) {} size_t layer_id; coordf_t print_z; std::vector islands; }; - + struct RichSupportPoint { Vec3f position; Structure *island; }; - + struct PointGrid3D { struct GridHash { std::size_t operator()(const Vec3i &cell_id) const { @@ -136,23 +143,23 @@ public: } }; typedef std::unordered_multimap Grid; - + Vec3f cell_size; Grid grid; - + Vec3i cell_id(const Vec3f &pos) { return Vec3i(int(floor(pos.x() / cell_size.x())), int(floor(pos.y() / cell_size.y())), int(floor(pos.z() / cell_size.z()))); } - + void insert(const Vec2f &pos, Structure *island) { RichSupportPoint pt; - pt.position = Vec3f(pos.x(), pos.y(), float(island->layer->print_z)); + pt.position = Vec3f(pos.x(), pos.y(), float(island->layer->print_z)); pt.island = island; grid.emplace(cell_id(pt.position), pt); } - + bool collides_with(const Vec2f &pos, Structure *island, float radius) { Vec3f pos3d(pos.x(), pos.y(), float(island->layer->print_z)); Vec3i cell = cell_id(pos3d); @@ -170,41 +177,45 @@ public: } return false; } - + private: bool collides_with(const Vec3f &pos, float radius, Grid::const_iterator it_begin, Grid::const_iterator it_end) { for (Grid::const_iterator it = it_begin; it != it_end; ++ it) { - float dist2 = (it->second.position - pos).squaredNorm(); + float dist2 = (it->second.position - pos).squaredNorm(); if (dist2 < radius * radius) return true; } return false; } }; - + + void execute(const std::vector &slices, + const std::vector & heights); + + void seed(std::mt19937::result_type s) { m_rng.seed(s); } private: - std::vector m_output; - - SLAAutoSupports::Config m_config; - + std::vector m_output; + + SupportPointGenerator::Config m_config; + void process(const std::vector& slices, const std::vector& heights); void uniformly_cover(const ExPolygons& islands, Structure& structure, PointGrid3D &grid3d, bool is_new_island = false, bool just_one = false); - void project_onto_mesh(std::vector& points) const; + void project_onto_mesh(std::vector& points) const; -#ifdef SLA_AUTOSUPPORTS_DEBUG +#ifdef SLA_SUPPORTPOINTGEN_DEBUG static void output_expolygons(const ExPolygons& expolys, const std::string &filename); static void output_structures(const std::vector &structures); -#endif // SLA_AUTOSUPPORTS_DEBUG - - const sla::EigenMesh3D& m_emesh; +#endif // SLA_SUPPORTPOINTGEN_DEBUG + + const EigenMesh3D& m_emesh; std::function m_throw_on_cancel; std::function m_statusfn; + + std::mt19937 m_rng; }; void remove_bottom_points(std::vector &pts, double gnd_lvl, double tolerance); -} // namespace sla -} // namespace Slic3r +}} // namespace Slic3r::sla - -#endif // SLAAUTOSUPPORTS_HPP_ +#endif // SUPPORTPOINTGENERATOR_HPP diff --git a/src/libslic3r/SLA/SLASupportTree.cpp b/src/libslic3r/SLA/SupportTree.cpp similarity index 95% rename from src/libslic3r/SLA/SLASupportTree.cpp rename to src/libslic3r/SLA/SupportTree.cpp index fea8bf731c..3024883edd 100644 --- a/src/libslic3r/SLA/SLASupportTree.cpp +++ b/src/libslic3r/SLA/SupportTree.cpp @@ -4,10 +4,10 @@ */ #include -#include "SLASupportTree.hpp" -#include "SLABoilerPlate.hpp" -#include "SLASpatIndex.hpp" -#include "SLASupportTreeBuilder.hpp" +#include +#include +#include +#include #include #include diff --git a/src/libslic3r/SLA/SLASupportTree.hpp b/src/libslic3r/SLA/SupportTree.hpp similarity index 83% rename from src/libslic3r/SLA/SLASupportTree.hpp rename to src/libslic3r/SLA/SupportTree.hpp index 322b29251e..acf6c10f5e 100644 --- a/src/libslic3r/SLA/SLASupportTree.hpp +++ b/src/libslic3r/SLA/SupportTree.hpp @@ -1,12 +1,15 @@ -#ifndef SLASUPPORTTREE_HPP -#define SLASUPPORTTREE_HPP +#ifndef SLA_SUPPORTTREE_HPP +#define SLA_SUPPORTTREE_HPP #include #include #include -#include "SLACommon.hpp" -#include "SLAPad.hpp" +#include +#include +#include +#include +#include namespace Slic3r { @@ -105,27 +108,6 @@ struct SupportConfig enum class MeshType { Support, Pad }; -/// A Control structure for the support calculation. Consists of the status -/// indicator callback and the stop condition predicate. -struct JobController -{ - using StatusFn = std::function; - using StopCond = std::function; - using CancelFn = std::function; - - // This will signal the status of the calculation to the front-end - StatusFn statuscb = [](unsigned, const std::string&){}; - - // Returns true if the calculation should be aborted. - StopCond stopcondition = [](){ return false; }; - - // Similar to cancel callback. This should check the stop condition and - // if true, throw an appropriate exception. (TriangleMeshSlicer needs this) - // consider it a hard abort. stopcondition is permits the algorithm to - // terminate itself - CancelFn cancelfn = [](){}; -}; - struct SupportableMesh { EigenMesh3D emesh; diff --git a/src/libslic3r/SLA/SLASupportTreeBuilder.cpp b/src/libslic3r/SLA/SupportTreeBuilder.cpp similarity index 96% rename from src/libslic3r/SLA/SLASupportTreeBuilder.cpp rename to src/libslic3r/SLA/SupportTreeBuilder.cpp index 2e0310ed8d..d385e98a29 100644 --- a/src/libslic3r/SLA/SLASupportTreeBuilder.cpp +++ b/src/libslic3r/SLA/SupportTreeBuilder.cpp @@ -1,5 +1,6 @@ -#include "SLASupportTreeBuilder.hpp" -#include "SLASupportTreeBuildsteps.hpp" +#include +#include +#include namespace Slic3r { namespace sla { @@ -12,7 +13,7 @@ Contour3D sphere(double rho, Portion portion, double fa) { if(rho <= 1e-6 && rho >= -1e-6) return ret; auto& vertices = ret.points; - auto& facets = ret.indices; + auto& facets = ret.faces3; // Algorithm: // Add points one-by-one to the sphere grid and form facets using relative @@ -102,7 +103,7 @@ Contour3D cylinder(double r, double h, size_t ssteps, const Vec3d &sp) auto steps = int(ssteps); auto& points = ret.points; - auto& indices = ret.indices; + auto& indices = ret.faces3; points.reserve(2*ssteps); double a = 2*PI/steps; @@ -211,8 +212,8 @@ Head::Head(double r_big_mm, coord_t i1s1 = coord_t(idx1), i1s2 = coord_t(idx2); coord_t i2s1 = i1s1 + 1, i2s2 = i1s2 + 1; - mesh.indices.emplace_back(i1s1, i2s1, i2s2); - mesh.indices.emplace_back(i1s1, i2s2, i1s2); + mesh.faces3.emplace_back(i1s1, i2s1, i2s2); + mesh.faces3.emplace_back(i1s1, i2s2, i1s2); } auto i1s1 = coord_t(s1.points.size()) - coord_t(steps); @@ -220,8 +221,8 @@ Head::Head(double r_big_mm, auto i1s2 = coord_t(s1.points.size()); auto i2s2 = coord_t(s1.points.size()) + coord_t(steps) - 1; - mesh.indices.emplace_back(i2s2, i2s1, i1s1); - mesh.indices.emplace_back(i1s2, i2s2, i1s1); + mesh.faces3.emplace_back(i2s2, i2s1, i1s1); + mesh.faces3.emplace_back(i1s2, i2s2, i1s1); // To simplify further processing, we translate the mesh so that the // last vertex of the pointing sphere (the pinpoint) will be at (0,0,0) @@ -240,7 +241,7 @@ Pillar::Pillar(const Vec3d &jp, const Vec3d &endp, double radius, size_t st): // move the data. Contour3D body = cylinder(radius, height, st, endp); mesh.points.swap(body.points); - mesh.indices.swap(body.indices); + mesh.faces3.swap(body.faces3); } } @@ -275,7 +276,7 @@ Pillar &Pillar::add_base(double baseheight, double radius) base.points.emplace_back(endpt); base.points.emplace_back(ep); - auto& indices = base.indices; + auto& indices = base.faces3; auto hcenter = int(base.points.size() - 1); auto lcenter = int(base.points.size() - 2); auto offs = int(steps); @@ -466,7 +467,7 @@ const TriangleMesh &SupportTreeBuilder::merged_mesh() const return m_meshcache; } - m_meshcache = mesh(merged); + m_meshcache = to_triangle_mesh(merged); // The mesh will be passed by const-pointer to TriangleMeshSlicer, // which will need this. diff --git a/src/libslic3r/SLA/SLASupportTreeBuilder.hpp b/src/libslic3r/SLA/SupportTreeBuilder.hpp similarity index 97% rename from src/libslic3r/SLA/SLASupportTreeBuilder.hpp rename to src/libslic3r/SLA/SupportTreeBuilder.hpp index c0d9f04c0f..90cf417c83 100644 --- a/src/libslic3r/SLA/SLASupportTreeBuilder.hpp +++ b/src/libslic3r/SLA/SupportTreeBuilder.hpp @@ -1,10 +1,11 @@ -#ifndef SUPPORTTREEBUILDER_HPP -#define SUPPORTTREEBUILDER_HPP +#ifndef SLA_SUPPORTTREEBUILDER_HPP +#define SLA_SUPPORTTREEBUILDER_HPP -#include "SLAConcurrency.hpp" -#include "SLABoilerPlate.hpp" -#include "SLASupportTree.hpp" -#include "SLAPad.hpp" +#include +#include +#include +#include +#include #include namespace Slic3r { @@ -73,7 +74,7 @@ Contour3D sphere(double rho, Portion portion = make_portion(0.0, 2.0*PI), // h: Height // ssteps: how many edges will create the base circle // sp: starting point -Contour3D cylinder(double r, double h, size_t ssteps, const Vec3d &sp = {0,0,0}); +Contour3D cylinder(double r, double h, size_t ssteps = 45, const Vec3d &sp = {0,0,0}); const constexpr long ID_UNSET = -1; diff --git a/src/libslic3r/SLA/SLASupportTreeBuildsteps.cpp b/src/libslic3r/SLA/SupportTreeBuildsteps.cpp similarity index 82% rename from src/libslic3r/SLA/SLASupportTreeBuildsteps.cpp rename to src/libslic3r/SLA/SupportTreeBuildsteps.cpp index 392a963e12..45fef58cd0 100644 --- a/src/libslic3r/SLA/SLASupportTreeBuildsteps.cpp +++ b/src/libslic3r/SLA/SupportTreeBuildsteps.cpp @@ -1,4 +1,4 @@ -#include "SLASupportTreeBuildsteps.hpp" +#include #include #include @@ -7,8 +7,16 @@ namespace Slic3r { namespace sla { +static const Vec3d DOWN = {0.0, 0.0, -1.0}; + +using libnest2d::opt::initvals; +using libnest2d::opt::bound; +using libnest2d::opt::StopCriteria; +using libnest2d::opt::GeneticOptimizer; +using libnest2d::opt::SubplexOptimizer; + SupportTreeBuildsteps::SupportTreeBuildsteps(SupportTreeBuilder & builder, - const SupportableMesh &sm) + const SupportableMesh &sm) : m_cfg(sm.cfg) , m_mesh(sm.emesh) , m_support_pts(sm.pts) @@ -30,7 +38,7 @@ SupportTreeBuildsteps::SupportTreeBuildsteps(SupportTreeBuilder & builder, } bool SupportTreeBuildsteps::execute(SupportTreeBuilder & builder, - const SupportableMesh &sm) + const SupportableMesh &sm) { if(sm.pts.empty()) return false; @@ -560,9 +568,7 @@ void SupportTreeBuildsteps::create_ground_pillar(const Vec3d &jp, double radius, long head_id) { - // People were killed for this number (seriously) - static const double SQR2 = std::sqrt(2.0); - static const Vec3d DOWN = {0.0, 0.0, -1.0}; + const double SLOPE = 1. / std::cos(m_cfg.bridge_slope); double gndlvl = m_builder.ground_level; Vec3d endp = {jp(X), jp(Y), gndlvl}; @@ -573,38 +579,47 @@ void SupportTreeBuildsteps::create_ground_pillar(const Vec3d &jp, bool can_add_base = true; bool normal_mode = true; + // If in zero elevation mode and the pillar is too close to the model body, + // the support pillar can not be placed in the gap between the model and + // the pad, and the pillar bases must not touch the model body either. + // To solve this, a corrector bridge is inserted between the starting point + // (jp) and the new pillar. if (m_cfg.object_elevation_mm < EPSILON && (dist = std::sqrt(m_mesh.squared_distance(endp))) < min_dist) { // Get the distance from the mesh. This can be later optimized // to get the distance in 2D plane because we are dealing with // the ground level only. + + normal_mode = false; + + // The min distance needed to move away from the model in XY plane. + double current_d = min_dist - dist; + double current_bride_d = SLOPE * current_d; + + // get a suitable direction for the corrector bridge. It is the + // original sourcedir's azimuth but the polar angle is saturated to the + // configured bridge slope. + auto [polar, azimuth] = dir_to_spheric(sourcedir); + polar = PI - m_cfg.bridge_slope; + auto dir = spheric_to_dir(polar, azimuth).normalized(); - normal_mode = false; - double mind = min_dist - dist; - double azimuth = std::atan2(sourcedir(Y), sourcedir(X)); - double sinpolar = std::sin(PI - m_cfg.bridge_slope); - double cospolar = std::cos(PI - m_cfg.bridge_slope); - double cosazm = std::cos(azimuth); - double sinazm = std::sin(azimuth); - - auto dir = Vec3d(cosazm * sinpolar, sinazm * sinpolar, cospolar) - .normalized(); - - using namespace libnest2d::opt; StopCriteria scr; scr.stop_score = min_dist; SubplexOptimizer solver(scr); + // Search for a distance along the corrector bridge to move the endpoint + // sufficiently away form the model body. The first few optimization + // cycles should succeed here. auto result = solver.optimize_max( [this, dir, jp, gndlvl](double mv) { - Vec3d endpt = jp + SQR2 * mv * dir; + Vec3d endpt = jp + mv * dir; endpt(Z) = gndlvl; return std::sqrt(m_mesh.squared_distance(endpt)); }, - initvals(mind), bound(0.0, 2 * min_dist)); + initvals(current_bride_d), + bound(0.0, m_cfg.max_bridge_length_mm - current_bride_d)); - mind = std::get<0>(result.optimum); - endp = jp + SQR2 * mind * dir; + endp = jp + std::get<0>(result.optimum) * dir; Vec3d pgnd = {endp(X), endp(Y), gndlvl}; can_add_base = result.score > min_dist; @@ -623,7 +638,7 @@ void SupportTreeBuildsteps::create_ground_pillar(const Vec3d &jp, else { // If the new endpoint is below ground, do not make a pillar if (endp(Z) < gndlvl) - endp = endp - SQR2 * (gndlvl - endp(Z)) * dir; // back off + endp = endp - SLOPE * (gndlvl - endp(Z)) * dir; // back off else { auto hit = bridge_mesh_intersect(endp, DOWN, radius); @@ -685,11 +700,6 @@ void SupportTreeBuildsteps::filter() // not be enough space for the pinhead. Filtering is applied for // these reasons. - using libnest2d::opt::bound; - using libnest2d::opt::initvals; - using libnest2d::opt::GeneticOptimizer; - using libnest2d::opt::StopCriteria; - ccr::SpinningMutex mutex; auto addfn = [&mutex](PtIndices &container, unsigned val) { std::lock_guard lk(mutex); @@ -708,10 +718,7 @@ void SupportTreeBuildsteps::filter() // (Quaternion::FromTwoVectors) and apply the rotation to the // arrow head. - double z = n(2); - double r = 1.0; // for normalized vector - double polar = std::acos(z / r); - double azimuth = std::atan2(n(1), n(0)); + auto [polar, azimuth] = dir_to_spheric(n); // skip if the tilt is not sane if(polar >= PI - m_cfg.normal_cutoff_angle) { @@ -729,9 +736,7 @@ void SupportTreeBuildsteps::filter() double pin_r = double(m_support_pts[fidx].head_front_radius); // Reassemble the now corrected normal - auto nn = Vec3d(std::cos(azimuth) * std::sin(polar), - std::sin(azimuth) * std::sin(polar), - std::cos(polar)).normalized(); + auto nn = spheric_to_dir(polar, azimuth).normalized(); // check available distance EigenMesh3D::hit_result t @@ -757,9 +762,7 @@ void SupportTreeBuildsteps::filter() auto oresult = solver.optimize_max( [this, pin_r, w, hp](double plr, double azm) { - auto dir = Vec3d(std::cos(azm) * std::sin(plr), - std::sin(azm) * std::sin(plr), - std::cos(plr)).normalized(); + auto dir = spheric_to_dir(plr, azm).normalized(); double score = pinhead_mesh_intersect( hp, dir, pin_r, m_cfg.head_back_radius_mm, w); @@ -767,18 +770,15 @@ void SupportTreeBuildsteps::filter() return score; }, initvals(polar, azimuth), // start with what we have - bound(3 * PI / 4, - PI), // Must not exceed the tilt limit + bound(3 * PI / 4, PI), // Must not exceed the tilt limit bound(-PI, PI) // azimuth can be a full search ); if(oresult.score > w) { polar = std::get<0>(oresult.optimum); azimuth = std::get<1>(oresult.optimum); - nn = Vec3d(std::cos(azimuth) * std::sin(polar), - std::sin(azimuth) * std::sin(polar), - std::cos(polar)).normalized(); - t = oresult.score; + nn = spheric_to_dir(polar, azimuth).normalized(); + t = EigenMesh3D::hit_result(oresult.score); } } @@ -837,16 +837,17 @@ void SupportTreeBuildsteps::classify() m_thr(); auto& head = m_builder.head(i); - Vec3d n(0, 0, -1); double r = head.r_back_mm; Vec3d headjp = head.junction_point(); // collision check - auto hit = bridge_mesh_intersect(headjp, n, r); + auto hit = bridge_mesh_intersect(headjp, DOWN, r); if(std::isinf(hit.distance())) ground_head_indices.emplace_back(i); else if(m_cfg.ground_facing_only) head.invalidate(); - else m_iheads_onmodel.emplace_back(std::make_pair(i, hit)); + else m_iheads_onmodel.emplace_back(i); + + m_head_to_ground_scans[i] = hit; } // We want to search for clusters of points that are far enough @@ -893,13 +894,14 @@ void SupportTreeBuildsteps::routing_to_ground() // get the current cluster centroid auto & thr = m_thr; const auto &points = m_points; - long lcid = cluster_centroid( + + long lcid = cluster_centroid( cl, [&points](size_t idx) { return points.row(long(idx)); }, [thr](const Vec3d &p1, const Vec3d &p2) { thr(); return distance(Vec2d(p1(X), p1(Y)), Vec2d(p2(X), p2(Y))); }); - + assert(lcid >= 0); unsigned hid = cl[size_t(lcid)]; // Head ID @@ -944,192 +946,138 @@ void SupportTreeBuildsteps::routing_to_ground() } } +bool SupportTreeBuildsteps::connect_to_ground(Head &head, const Vec3d &dir) +{ + auto hjp = head.junction_point(); + double r = head.r_back_mm; + double t = bridge_mesh_intersect(hjp, dir, head.r_back_mm); + double d = 0, tdown = 0; + t = std::min(t, m_cfg.max_bridge_length_mm); + + while (d < t && !std::isinf(tdown = bridge_mesh_intersect(hjp + d * dir, DOWN, r))) + d += r; + + if(!std::isinf(tdown)) return false; + + Vec3d endp = hjp + d * dir; + m_builder.add_bridge(head.id, endp); + m_builder.add_junction(endp, head.r_back_mm); + + this->create_ground_pillar(endp, dir, head.r_back_mm); + + return true; +} + +bool SupportTreeBuildsteps::connect_to_ground(Head &head) +{ + if (connect_to_ground(head, head.dir)) return true; + + // Optimize bridge direction: + // Straight path failed so we will try to search for a suitable + // direction out of the cavity. + auto [polar, azimuth] = dir_to_spheric(head.dir); + + StopCriteria stc; + stc.max_iterations = m_cfg.optimizer_max_iterations; + stc.relative_score_difference = m_cfg.optimizer_rel_score_diff; + stc.stop_score = 1e6; + GeneticOptimizer solver(stc); + solver.seed(0); // we want deterministic behavior + + double r_back = head.r_back_mm; + Vec3d hjp = head.junction_point(); + auto oresult = solver.optimize_max( + [this, hjp, r_back](double plr, double azm) { + Vec3d n = spheric_to_dir(plr, azm).normalized(); + return bridge_mesh_intersect(hjp, n, r_back); + }, + initvals(polar, azimuth), // let's start with what we have + bound(3*PI/4, PI), // Must not exceed the slope limit + bound(-PI, PI) // azimuth can be a full range search + ); + + Vec3d bridgedir = spheric_to_dir(oresult.optimum).normalized(); + return connect_to_ground(head, bridgedir); +} + +bool SupportTreeBuildsteps::connect_to_model_body(Head &head) +{ + if (head.id <= ID_UNSET) return false; + + auto it = m_head_to_ground_scans.find(unsigned(head.id)); + if (it == m_head_to_ground_scans.end()) return false; + + auto &hit = it->second; + Vec3d hjp = head.junction_point(); + double zangle = std::asin(hit.direction()(Z)); + zangle = std::max(zangle, PI/4); + double h = std::sin(zangle) * head.fullwidth(); + + // The width of the tail head that we would like to have... + h = std::min(hit.distance() - head.r_back_mm, h); + + if(h <= 0.) return false; + + Vec3d endp{hjp(X), hjp(Y), hjp(Z) - hit.distance() + h}; + auto center_hit = m_mesh.query_ray_hit(hjp, DOWN); + + double hitdiff = center_hit.distance() - hit.distance(); + Vec3d hitp = std::abs(hitdiff) < 2*head.r_back_mm? + center_hit.position() : hit.position(); + + head.transform(); + + long pillar_id = m_builder.add_pillar(head.id, endp, head.r_back_mm); + Pillar &pill = m_builder.pillar(pillar_id); + + Vec3d taildir = endp - hitp; + double dist = distance(endp, hitp) + m_cfg.head_penetration_mm; + double w = dist - 2 * head.r_pin_mm - head.r_back_mm; + + if (w < 0.) { + BOOST_LOG_TRIVIAL(error) << "Pinhead width is negative!"; + w = 0.; + } + + Head tailhead(head.r_back_mm, head.r_pin_mm, w, + m_cfg.head_penetration_mm, taildir, hitp); + + tailhead.transform(); + pill.base = tailhead.mesh; + + m_pillar_index.guarded_insert(pill.endpoint(), pill.id); + + return true; +} + void SupportTreeBuildsteps::routing_to_model() { // We need to check if there is an easy way out to the bed surface. // If it can be routed there with a bridge shorter than // min_bridge_distance. - - // First we want to index the available pillars. The best is to connect - // these points to the available pillars - - auto routedown = [this](Head& head, const Vec3d& dir, double dist) - { - head.transform(); - Vec3d endp = head.junction_point() + dist * dir; - m_builder.add_bridge(head.id, endp); - m_builder.add_junction(endp, head.r_back_mm); - - this->create_ground_pillar(endp, dir, head.r_back_mm); - }; - - std::vector modelpillars; - ccr::SpinningMutex mutex; - auto onmodelfn = - [this, routedown, &modelpillars, &mutex] - (const std::pair &el, size_t) - { + ccr::enumerate(m_iheads_onmodel.begin(), m_iheads_onmodel.end(), + [this] (const unsigned idx, size_t) { m_thr(); - unsigned idx = el.first; - EigenMesh3D::hit_result hit = el.second; auto& head = m_builder.head(idx); - Vec3d hjp = head.junction_point(); - // ///////////////////////////////////////////////////////////////// // Search nearby pillar - // ///////////////////////////////////////////////////////////////// - if(search_pillar_and_connect(head)) { head.transform(); return; } - // ///////////////////////////////////////////////////////////////// - // Try straight path - // ///////////////////////////////////////////////////////////////// - // Cannot connect to nearby pillar. We will try to search for // a route to the ground. + if(connect_to_ground(head)) { head.transform(); return; } - double t = bridge_mesh_intersect(hjp, head.dir, head.r_back_mm); - double d = 0, tdown = 0; - Vec3d dirdown(0.0, 0.0, -1.0); - - t = std::min(t, m_cfg.max_bridge_length_mm); - - while(d < t && !std::isinf(tdown = bridge_mesh_intersect( - hjp + d*head.dir, - dirdown, head.r_back_mm))) { - d += head.r_back_mm; - } - - if(std::isinf(tdown)) { // we heave found a route to the ground - routedown(head, head.dir, d); return; - } - - // ///////////////////////////////////////////////////////////////// - // Optimize bridge direction - // ///////////////////////////////////////////////////////////////// - - // Straight path failed so we will try to search for a suitable - // direction out of the cavity. - - // Get the spherical representation of the normal. its easier to - // work with. - double z = head.dir(Z); - double r = 1.0; // for normalized vector - double polar = std::acos(z / r); - double azimuth = std::atan2(head.dir(Y), head.dir(X)); - - using libnest2d::opt::bound; - using libnest2d::opt::initvals; - using libnest2d::opt::GeneticOptimizer; - using libnest2d::opt::StopCriteria; - - StopCriteria stc; - stc.max_iterations = m_cfg.optimizer_max_iterations; - stc.relative_score_difference = m_cfg.optimizer_rel_score_diff; - stc.stop_score = 1e6; - GeneticOptimizer solver(stc); - solver.seed(0); // we want deterministic behavior - - double r_back = head.r_back_mm; - - auto oresult = solver.optimize_max( - [this, hjp, r_back](double plr, double azm) - { - Vec3d n = Vec3d(std::cos(azm) * std::sin(plr), - std::sin(azm) * std::sin(plr), - std::cos(plr)).normalized(); - return bridge_mesh_intersect(hjp, n, r_back); - }, - initvals(polar, azimuth), // let's start with what we have - bound(3*PI/4, PI), // Must not exceed the slope limit - bound(-PI, PI) // azimuth can be a full range search - ); - - d = 0; t = oresult.score; - - polar = std::get<0>(oresult.optimum); - azimuth = std::get<1>(oresult.optimum); - Vec3d bridgedir = Vec3d(std::cos(azimuth) * std::sin(polar), - std::sin(azimuth) * std::sin(polar), - std::cos(polar)).normalized(); - - t = std::min(t, m_cfg.max_bridge_length_mm); - - while(d < t && !std::isinf(tdown = bridge_mesh_intersect( - hjp + d*bridgedir, - dirdown, - head.r_back_mm))) { - d += head.r_back_mm; - } - - if(std::isinf(tdown)) { // we heave found a route to the ground - routedown(head, bridgedir, d); return; - } - - // ///////////////////////////////////////////////////////////////// - // Route to model body - // ///////////////////////////////////////////////////////////////// - - double zangle = std::asin(hit.direction()(Z)); - zangle = std::max(zangle, PI/4); - double h = std::sin(zangle) * head.fullwidth(); - - // The width of the tail head that we would like to have... - h = std::min(hit.distance() - head.r_back_mm, h); - - if(h > 0) { - Vec3d endp{hjp(X), hjp(Y), hjp(Z) - hit.distance() + h}; - auto center_hit = m_mesh.query_ray_hit(hjp, dirdown); - - double hitdiff = center_hit.distance() - hit.distance(); - Vec3d hitp = std::abs(hitdiff) < 2*head.r_back_mm? - center_hit.position() : hit.position(); - - head.transform(); - - long pillar_id = m_builder.add_pillar(head.id, endp, head.r_back_mm); - Pillar &pill = m_builder.pillar(pillar_id); - - Vec3d taildir = endp - hitp; - double dist = distance(endp, hitp) + m_cfg.head_penetration_mm; - double w = dist - 2 * head.r_pin_mm - head.r_back_mm; - - if (w < 0.) { - BOOST_LOG_TRIVIAL(error) << "Pinhead width is negative!"; - w = 0.; - } - - Head tailhead(head.r_back_mm, - head.r_pin_mm, - w, - m_cfg.head_penetration_mm, - taildir, - hitp); - - tailhead.transform(); - pill.base = tailhead.mesh; - - // Experimental: add the pillar to the index for cascading - std::lock_guard lk(mutex); - modelpillars.emplace_back(unsigned(pill.id)); - return; - } + // No route to the ground, so connect to the model body as a last resort + if (connect_to_model_body(head)) { return; } // We have failed to route this head. BOOST_LOG_TRIVIAL(warning) - << "Failed to route model facing support point." - << " ID: " << idx; + << "Failed to route model facing support point. ID: " << idx; + head.invalidate(); - }; - - ccr::enumerate(m_iheads_onmodel.begin(), m_iheads_onmodel.end(), onmodelfn); - - for(auto pillid : modelpillars) { - auto& pillar = m_builder.pillar(pillid); - m_pillar_index.insert(pillar.endpoint(), pillid); - } + }); } void SupportTreeBuildsteps::interconnect_pillars() @@ -1280,7 +1228,8 @@ void SupportTreeBuildsteps::interconnect_pillars() spts[n] = s; // Check the path vertically down - auto hr = bridge_mesh_intersect(s, {0, 0, -1}, pillar().r); + Vec3d check_from = s + Vec3d{0., 0., pillar().r}; + auto hr = bridge_mesh_intersect(check_from, DOWN, pillar().r); Vec3d gndsp{s(X), s(Y), gnd}; // If the path is clear, check for pillar base collisions @@ -1360,12 +1309,11 @@ void SupportTreeBuildsteps::routing_headless() Vec3d n = m_support_nmls.row(i); // mesh outward normal Vec3d sp = sph - n * HWIDTH_MM; // stick head start point - Vec3d dir = {0, 0, -1}; Vec3d sj = sp + R * n; // stick start point // This is only for checking - double idist = bridge_mesh_intersect(sph, dir, R, true); - double realdist = ray_mesh_intersect(sj, dir); + double idist = bridge_mesh_intersect(sph, DOWN, R, true); + double realdist = ray_mesh_intersect(sj, DOWN); double dist = realdist; if (std::isinf(dist)) dist = sph(Z) - m_builder.ground_level; @@ -1378,7 +1326,7 @@ void SupportTreeBuildsteps::routing_headless() } bool use_endball = !std::isinf(realdist); - Vec3d ej = sj + (dist + HWIDTH_MM) * dir; + Vec3d ej = sj + (dist + HWIDTH_MM) * DOWN ; m_builder.add_compact_bridge(sp, ej, n, R, use_endball); } } diff --git a/src/libslic3r/SLA/SLASupportTreeBuildsteps.hpp b/src/libslic3r/SLA/SupportTreeBuildsteps.hpp similarity index 88% rename from src/libslic3r/SLA/SLASupportTreeBuildsteps.hpp rename to src/libslic3r/SLA/SupportTreeBuildsteps.hpp index e953cc1361..9533049b60 100644 --- a/src/libslic3r/SLA/SLASupportTreeBuildsteps.hpp +++ b/src/libslic3r/SLA/SupportTreeBuildsteps.hpp @@ -3,7 +3,8 @@ #include -#include "SLASupportTreeBuilder.hpp" +#include +#include namespace Slic3r { namespace sla { @@ -19,6 +20,32 @@ inline Vec2d to_vec2(const Vec3d& v3) { return {v3(X), v3(Y)}; } +inline std::pair dir_to_spheric(const Vec3d &n, double norm = 1.) +{ + double z = n.z(); + double r = norm; + double polar = std::acos(z / r); + double azimuth = std::atan2(n(1), n(0)); + return {polar, azimuth}; +} + +inline Vec3d spheric_to_dir(double polar, double azimuth) +{ + return {std::cos(azimuth) * std::sin(polar), + std::sin(azimuth) * std::sin(polar), std::cos(polar)}; +} + +inline Vec3d spheric_to_dir(const std::tuple &v) +{ + auto [plr, azm] = v; + return spheric_to_dir(plr, azm); +} + +inline Vec3d spheric_to_dir(const std::pair &v) +{ + return spheric_to_dir(v.first, v.second); +} + // This function returns the position of the centroid in the input 'clust' // vector of point indices. template @@ -150,10 +177,10 @@ class SupportTreeBuildsteps { using PtIndices = std::vector; PtIndices m_iheads; // support points with pinhead + PtIndices m_iheads_onmodel; PtIndices m_iheadless; // headless support points - - // supp. pts. connecting to model: point index and the ray hit data - std::vector> m_iheads_onmodel; + + std::map m_head_to_ground_scans; // normals for support points from model faces. PointSet m_support_nmls; @@ -222,15 +249,29 @@ class SupportTreeBuildsteps { // For connecting a head to a nearby pillar. bool connect_to_nearpillar(const Head& head, long nearpillar_id); - + + // Find route for a head to the ground. Inserts additional bridge from the + // head to the pillar if cannot create pillar directly. + // The optional dir parameter is the direction of the bridge which is the + // direction of the pinhead if omitted. + bool connect_to_ground(Head& head, const Vec3d &dir); + inline bool connect_to_ground(Head& head); + + bool connect_to_model_body(Head &head); + bool search_pillar_and_connect(const Head& head); - + // This is a proxy function for pillar creation which will mind the gap // between the pad and the model bottom in zero elevation mode. + // jp is the starting junction point which needs to be routed down. + // sourcedir is the allowed direction of an optional bridge between the + // jp junction and the final pillar. void create_ground_pillar(const Vec3d &jp, const Vec3d &sourcedir, double radius, long head_id = ID_UNSET); + + public: SupportTreeBuildsteps(SupportTreeBuilder & builder, const SupportableMesh &sm); diff --git a/src/libslic3r/SLA/SLASupportTreeIGL.cpp b/src/libslic3r/SLA/SupportTreeIGL.cpp similarity index 91% rename from src/libslic3r/SLA/SLASupportTreeIGL.cpp rename to src/libslic3r/SLA/SupportTreeIGL.cpp index 05f8b19842..ea2be6be11 100644 --- a/src/libslic3r/SLA/SLASupportTreeIGL.cpp +++ b/src/libslic3r/SLA/SupportTreeIGL.cpp @@ -1,6 +1,6 @@ #include #include "SLA/SLASupportTree.hpp" -#include "SLA/SLABoilerPlate.hpp" +#include "SLA/SLACommon.hpp" #include "SLA/SLASpatIndex.hpp" // Workaround: IGL signed_distance.h will define PI in the igl namespace. @@ -228,6 +228,26 @@ EigenMesh3D::EigenMesh3D(const EigenMesh3D &other): m_V(other.m_V), m_F(other.m_F), m_ground_level(other.m_ground_level), m_aabb( new AABBImpl(*other.m_aabb) ) {} +EigenMesh3D::EigenMesh3D(const Contour3D &other) +{ + m_V.resize(Eigen::Index(other.points.size()), 3); + m_F.resize(Eigen::Index(other.faces3.size() + 2 * other.faces4.size()), 3); + + for (Eigen::Index i = 0; i < Eigen::Index(other.points.size()); ++i) + m_V.row(i) = other.points[size_t(i)]; + + for (Eigen::Index i = 0; i < Eigen::Index(other.faces3.size()); ++i) + m_F.row(i) = other.faces3[size_t(i)]; + + size_t N = other.faces3.size() + 2 * other.faces4.size(); + for (size_t i = other.faces3.size(); i < N; i += 2) { + size_t quad_idx = (i - other.faces3.size()) / 2; + auto & quad = other.faces4[quad_idx]; + m_F.row(Eigen::Index(i)) = Vec3i{quad(0), quad(1), quad(2)}; + m_F.row(Eigen::Index(i + 1)) = Vec3i{quad(2), quad(3), quad(0)}; + } +} + EigenMesh3D &EigenMesh3D::operator=(const EigenMesh3D &other) { m_V = other.m_V; @@ -252,6 +272,31 @@ EigenMesh3D::query_ray_hit(const Vec3d &s, const Vec3d &dir) const return ret; } +std::vector +EigenMesh3D::query_ray_hits(const Vec3d &s, const Vec3d &dir) const +{ + std::vector outs; + std::vector hits; + m_aabb->intersect_ray(m_V, m_F, s, dir, hits); + + // The sort is necessary, the hits are not always sorted. + std::sort(hits.begin(), hits.end(), + [](const igl::Hit& a, const igl::Hit& b) { return a.t < b.t; }); + + // Convert the igl::Hit into hit_result + outs.reserve(hits.size()); + for (const igl::Hit& hit : hits) { + outs.emplace_back(EigenMesh3D::hit_result(*this)); + outs.back().m_t = double(hit.t); + outs.back().m_dir = dir; + outs.back().m_source = s; + if(!std::isinf(hit.t) && !std::isnan(hit.t)) + outs.back().m_face_id = hit.id; + } + + return outs; +} + #ifdef SLIC3R_SLA_NEEDS_WINDTREE EigenMesh3D::si_result EigenMesh3D::signed_distance(const Vec3d &p) const { double sign = 0; double sqdst = 0; int i = 0; Vec3d c; diff --git a/src/libslic3r/SLAPrint.cpp b/src/libslic3r/SLAPrint.cpp index d9f8b7b6db..4d34c09c7b 100644 --- a/src/libslic3r/SLAPrint.cpp +++ b/src/libslic3r/SLAPrint.cpp @@ -1,7 +1,6 @@ #include "SLAPrint.hpp" -#include "SLA/SLASupportTree.hpp" -#include "SLA/SLAPad.hpp" -#include "SLA/SLAAutoSupports.hpp" +#include "SLAPrintSteps.hpp" + #include "ClipperUtils.hpp" #include "Geometry.hpp" #include "MTUtils.hpp" @@ -13,9 +12,6 @@ #include #include -// For geometry algorithms with native Clipper types (no copies and conversions) -#include - // #define SLAPRINT_DO_BENCHMARK #ifdef SLAPRINT_DO_BENCHMARK @@ -32,65 +28,86 @@ namespace Slic3r { -class SLAPrintObject::SupportData : public sla::SupportableMesh + +bool is_zero_elevation(const SLAPrintObjectConfig &c) { -public: - sla::SupportTree::UPtr support_tree_ptr; // the supports - std::vector support_slices; // sliced supports + return c.pad_enable.getBool() && c.pad_around_object.getBool(); +} + +// Compile the argument for support creation from the static print config. +sla::SupportConfig make_support_cfg(const SLAPrintObjectConfig& c) +{ + sla::SupportConfig scfg; - inline SupportData(const TriangleMesh &t): sla::SupportableMesh{t, {}, {}} {} + scfg.enabled = c.supports_enable.getBool(); + scfg.head_front_radius_mm = 0.5*c.support_head_front_diameter.getFloat(); + scfg.head_back_radius_mm = 0.5*c.support_pillar_diameter.getFloat(); + scfg.head_penetration_mm = c.support_head_penetration.getFloat(); + scfg.head_width_mm = c.support_head_width.getFloat(); + scfg.object_elevation_mm = is_zero_elevation(c) ? + 0. : c.support_object_elevation.getFloat(); + scfg.bridge_slope = c.support_critical_angle.getFloat() * PI / 180.0 ; + scfg.max_bridge_length_mm = c.support_max_bridge_length.getFloat(); + scfg.max_pillar_link_distance_mm = c.support_max_pillar_link_distance.getFloat(); + switch(c.support_pillar_connection_mode.getInt()) { + case slapcmZigZag: + scfg.pillar_connection_mode = sla::PillarConnectionMode::zigzag; break; + case slapcmCross: + scfg.pillar_connection_mode = sla::PillarConnectionMode::cross; break; + case slapcmDynamic: + scfg.pillar_connection_mode = sla::PillarConnectionMode::dynamic; break; + } + scfg.ground_facing_only = c.support_buildplate_only.getBool(); + scfg.pillar_widening_factor = c.support_pillar_widening_factor.getFloat(); + scfg.base_radius_mm = 0.5*c.support_base_diameter.getFloat(); + scfg.base_height_mm = c.support_base_height.getFloat(); + scfg.pillar_base_safety_distance_mm = + c.support_base_safety_distance.getFloat() < EPSILON ? + scfg.safety_distance_mm : c.support_base_safety_distance.getFloat(); - sla::SupportTree::UPtr &create_support_tree(const sla::JobController &ctl) - { - support_tree_ptr = sla::SupportTree::create(*this, ctl); - return support_tree_ptr; + return scfg; +} + +sla::PadConfig::EmbedObject builtin_pad_cfg(const SLAPrintObjectConfig& c) +{ + sla::PadConfig::EmbedObject ret; + + ret.enabled = is_zero_elevation(c); + + if(ret.enabled) { + ret.everywhere = c.pad_around_object_everywhere.getBool(); + ret.object_gap_mm = c.pad_object_gap.getFloat(); + ret.stick_width_mm = c.pad_object_connector_width.getFloat(); + ret.stick_stride_mm = c.pad_object_connector_stride.getFloat(); + ret.stick_penetration_mm = c.pad_object_connector_penetration + .getFloat(); } -}; + + return ret; +} -namespace { - -// should add up to 100 (%) -const std::array OBJ_STEP_LEVELS = +sla::PadConfig make_pad_cfg(const SLAPrintObjectConfig& c) { - 30, // slaposObjectSlice, - 20, // slaposSupportPoints, - 10, // slaposSupportTree, - 10, // slaposPad, - 30, // slaposSliceSupports, -}; + sla::PadConfig pcfg; + + pcfg.wall_thickness_mm = c.pad_wall_thickness.getFloat(); + pcfg.wall_slope = c.pad_wall_slope.getFloat() * PI / 180.0; + + pcfg.max_merge_dist_mm = c.pad_max_merge_distance.getFloat(); + pcfg.wall_height_mm = c.pad_wall_height.getFloat(); + pcfg.brim_size_mm = c.pad_brim_size.getFloat(); + + // set builtin pad implicitly ON + pcfg.embed_object = builtin_pad_cfg(c); + + return pcfg; +} -// Object step to status label. The labels are localized at the time of calling, thus supporting language switching. -std::string OBJ_STEP_LABELS(size_t idx) +bool validate_pad(const TriangleMesh &pad, const sla::PadConfig &pcfg) { - switch (idx) { - case slaposObjectSlice: return L("Slicing model"); - case slaposSupportPoints: return L("Generating support points"); - case slaposSupportTree: return L("Generating support tree"); - case slaposPad: return L("Generating pad"); - case slaposSliceSupports: return L("Slicing supports"); - default:; - } - assert(false); return "Out of bounds!"; -}; - -// Should also add up to 100 (%) -const std::array PRINT_STEP_LEVELS = -{ - 10, // slapsMergeSlicesAndEval - 90, // slapsRasterize -}; - -// Print step to status label. The labels are localized at the time of calling, thus supporting language switching. -std::string PRINT_STEP_LABELS(size_t idx) -{ - switch (idx) { - case slapsMergeSlicesAndEval: return L("Merging slices and calculating statistics"); - case slapsRasterize: return L("Rasterizing layers"); - default:; - } - assert(false); return "Out of bounds!"; -}; - + // An empty pad can only be created if embed_object mode is enabled + // and the pad is not forced everywhere + return !pad.empty() || (pcfg.embed_object.enabled && !pcfg.embed_object.everywhere); } void SLAPrint::clear() @@ -397,6 +414,13 @@ SLAPrint::ApplyStatus SLAPrint::apply(const Model &model, DynamicPrintConfig con model_object.sla_support_points = model_object_new.sla_support_points; } model_object.sla_points_status = model_object_new.sla_points_status; + + // Invalidate hollowing if drain holes have changed + if (model_object.sla_drain_holes != model_object_new.sla_drain_holes) + { + model_object.sla_drain_holes = model_object_new.sla_drain_holes; + update_apply_status(it_print_object_status->print_object->invalidate_step(slaposHollowing)); + } // Copy the ModelObject name, input_file and instances. The instances will compared against PrintObject instances in the next step. model_object.name = model_object_new.name; @@ -576,87 +600,6 @@ std::string SLAPrint::output_filename(const std::string &filename_base) const return this->PrintBase::output_filename(m_print_config.output_filename_format.value, ".sl1", filename_base, &config); } -namespace { - -bool is_zero_elevation(const SLAPrintObjectConfig &c) { - return c.pad_enable.getBool() && c.pad_around_object.getBool(); -} - -// Compile the argument for support creation from the static print config. -sla::SupportConfig make_support_cfg(const SLAPrintObjectConfig& c) { - sla::SupportConfig scfg; - - scfg.enabled = c.supports_enable.getBool(); - scfg.head_front_radius_mm = 0.5*c.support_head_front_diameter.getFloat(); - scfg.head_back_radius_mm = 0.5*c.support_pillar_diameter.getFloat(); - scfg.head_penetration_mm = c.support_head_penetration.getFloat(); - scfg.head_width_mm = c.support_head_width.getFloat(); - scfg.object_elevation_mm = is_zero_elevation(c) ? - 0. : c.support_object_elevation.getFloat(); - scfg.bridge_slope = c.support_critical_angle.getFloat() * PI / 180.0 ; - scfg.max_bridge_length_mm = c.support_max_bridge_length.getFloat(); - scfg.max_pillar_link_distance_mm = c.support_max_pillar_link_distance.getFloat(); - switch(c.support_pillar_connection_mode.getInt()) { - case slapcmZigZag: - scfg.pillar_connection_mode = sla::PillarConnectionMode::zigzag; break; - case slapcmCross: - scfg.pillar_connection_mode = sla::PillarConnectionMode::cross; break; - case slapcmDynamic: - scfg.pillar_connection_mode = sla::PillarConnectionMode::dynamic; break; - } - scfg.ground_facing_only = c.support_buildplate_only.getBool(); - scfg.pillar_widening_factor = c.support_pillar_widening_factor.getFloat(); - scfg.base_radius_mm = 0.5*c.support_base_diameter.getFloat(); - scfg.base_height_mm = c.support_base_height.getFloat(); - scfg.pillar_base_safety_distance_mm = - c.support_base_safety_distance.getFloat() < EPSILON ? - scfg.safety_distance_mm : c.support_base_safety_distance.getFloat(); - - return scfg; -} - -sla::PadConfig::EmbedObject builtin_pad_cfg(const SLAPrintObjectConfig& c) { - sla::PadConfig::EmbedObject ret; - - ret.enabled = is_zero_elevation(c); - - if(ret.enabled) { - ret.everywhere = c.pad_around_object_everywhere.getBool(); - ret.object_gap_mm = c.pad_object_gap.getFloat(); - ret.stick_width_mm = c.pad_object_connector_width.getFloat(); - ret.stick_stride_mm = c.pad_object_connector_stride.getFloat(); - ret.stick_penetration_mm = c.pad_object_connector_penetration - .getFloat(); - } - - return ret; -} - -sla::PadConfig make_pad_cfg(const SLAPrintObjectConfig& c) { - sla::PadConfig pcfg; - - pcfg.wall_thickness_mm = c.pad_wall_thickness.getFloat(); - pcfg.wall_slope = c.pad_wall_slope.getFloat() * PI / 180.0; - - pcfg.max_merge_dist_mm = c.pad_max_merge_distance.getFloat(); - pcfg.wall_height_mm = c.pad_wall_height.getFloat(); - pcfg.brim_size_mm = c.pad_brim_size.getFloat(); - - // set builtin pad implicitly ON - pcfg.embed_object = builtin_pad_cfg(c); - - return pcfg; -} - -bool validate_pad(const TriangleMesh &pad, const sla::PadConfig &pcfg) -{ - // An empty pad can only be created if embed_object mode is enabled - // and the pad is not forced everywhere - return !pad.empty() || (pcfg.embed_object.enabled && !pcfg.embed_object.everywhere); -} - -} - std::string SLAPrint::validate() const { for(SLAPrintObject * po : m_objects) { @@ -726,751 +669,16 @@ bool SLAPrint::invalidate_step(SLAPrintStep step) void SLAPrint::process() { - using namespace sla; - using ExPolygon = Slic3r::ExPolygon; - if(m_objects.empty()) return; // Assumption: at this point the print objects should be populated only with // the model objects we have to process and the instances are also filtered - - // shortcut to initial layer height - double ilhd = m_material_config.initial_layer_height.getFloat(); - auto ilh = float(ilhd); - - coord_t ilhs = scaled(ilhd); - const size_t objcount = m_objects.size(); - - static const unsigned min_objstatus = 0; // where the per object operations start - static const unsigned max_objstatus = 50; // where the per object operations end - - // the coefficient that multiplies the per object status values which - // are set up for <0, 100>. They need to be scaled into the whole process - const double ostepd = (max_objstatus - min_objstatus) / (objcount * 100.0); - - // The slicing will be performed on an imaginary 1D grid which starts from - // the bottom of the bounding box created around the supported model. So - // the first layer which is usually thicker will be part of the supports - // not the model geometry. Exception is when the model is not in the air - // (elevation is zero) and no pad creation was requested. In this case the - // model geometry starts on the ground level and the initial layer is part - // of it. In any case, the model and the supports have to be sliced in the - // same imaginary grid (the height vector argument to TriangleMeshSlicer). - - // Slicing the model object. This method is oversimplified and needs to - // be compared with the fff slicing algorithm for verification - auto slice_model = [this, ilhs, ilh](SLAPrintObject& po) { - const TriangleMesh& mesh = po.transformed_mesh(); - - // We need to prepare the slice index... - - double lhd = m_objects.front()->m_config.layer_height.getFloat(); - float lh = float(lhd); - coord_t lhs = scaled(lhd); - auto && bb3d = mesh.bounding_box(); - double minZ = bb3d.min(Z) - po.get_elevation(); - double maxZ = bb3d.max(Z); - auto minZf = float(minZ); - coord_t minZs = scaled(minZ); - coord_t maxZs = scaled(maxZ); - - po.m_slice_index.clear(); - - size_t cap = size_t(1 + (maxZs - minZs - ilhs) / lhs); - po.m_slice_index.reserve(cap); - - po.m_slice_index.emplace_back(minZs + ilhs, minZf + ilh / 2.f, ilh); - - for(coord_t h = minZs + ilhs + lhs; h <= maxZs; h += lhs) - po.m_slice_index.emplace_back(h, unscaled(h) - lh / 2.f, lh); - - // Just get the first record that is from the model: - auto slindex_it = - po.closest_slice_record(po.m_slice_index, float(bb3d.min(Z))); - - if(slindex_it == po.m_slice_index.end()) - //TRN To be shown at the status bar on SLA slicing error. - throw std::runtime_error( - L("Slicing had to be stopped due to an internal error: " - "Inconsistent slice index.")); - - po.m_model_height_levels.clear(); - po.m_model_height_levels.reserve(po.m_slice_index.size()); - for(auto it = slindex_it; it != po.m_slice_index.end(); ++it) - po.m_model_height_levels.emplace_back(it->slice_level()); - - TriangleMeshSlicer slicer(&mesh); - - po.m_model_slices.clear(); - slicer.slice(po.m_model_height_levels, - float(po.config().slice_closing_radius.value), - &po.m_model_slices, - [this](){ throw_if_canceled(); }); - - auto mit = slindex_it; - double doffs = m_printer_config.absolute_correction.getFloat(); - coord_t clpr_offs = scaled(doffs); - for(size_t id = 0; - id < po.m_model_slices.size() && mit != po.m_slice_index.end(); - id++) - { - // We apply the printer correction offset here. - if(clpr_offs != 0) - po.m_model_slices[id] = - offset_ex(po.m_model_slices[id], float(clpr_offs)); - - mit->set_model_slice_idx(po, id); ++mit; - } - - if(po.m_config.supports_enable.getBool() || - po.m_config.pad_enable.getBool()) - { - po.m_supportdata.reset( - new SLAPrintObject::SupportData(po.transformed_mesh()) ); - } - }; - - // In this step we check the slices, identify island and cover them with - // support points. Then we sprinkle the rest of the mesh. - auto support_points = [this, ostepd](SLAPrintObject& po) { - // If supports are disabled, we can skip the model scan. - if(!po.m_config.supports_enable.getBool()) return; - - if (!po.m_supportdata) - po.m_supportdata.reset( - new SLAPrintObject::SupportData(po.transformed_mesh())); - - const ModelObject& mo = *po.m_model_object; - - BOOST_LOG_TRIVIAL(debug) << "Support point count " - << mo.sla_support_points.size(); - - // Unless the user modified the points or we already did the calculation, we will do - // the autoplacement. Otherwise we will just blindly copy the frontend data - // into the backend cache. - if (mo.sla_points_status != sla::PointsStatus::UserModified) { - - // Hypothetical use of the slice index: - // auto bb = po.transformed_mesh().bounding_box(); - // auto range = po.get_slice_records(bb.min(Z)); - // std::vector heights; heights.reserve(range.size()); - // for(auto& record : range) heights.emplace_back(record.slice_level()); - - // calculate heights of slices (slices are calculated already) - const std::vector& heights = po.m_model_height_levels; - - this->throw_if_canceled(); - SLAAutoSupports::Config config; - const SLAPrintObjectConfig& cfg = po.config(); - - // the density config value is in percents: - config.density_relative = float(cfg.support_points_density_relative / 100.f); - config.minimal_distance = float(cfg.support_points_minimal_distance); - config.head_diameter = float(cfg.support_head_front_diameter); - - // scaling for the sub operations - double d = ostepd * OBJ_STEP_LEVELS[slaposSupportPoints] / 100.0; - double init = m_report_status.status(); - - auto statuscb = [this, d, init](unsigned st) - { - double current = init + st * d; - if(std::round(m_report_status.status()) < std::round(current)) - m_report_status(*this, current, - OBJ_STEP_LABELS(slaposSupportPoints)); - - }; - - // Construction of this object does the calculation. - this->throw_if_canceled(); - SLAAutoSupports auto_supports(po.m_supportdata->emesh, - po.get_model_slices(), - heights, - config, - [this]() { throw_if_canceled(); }, - statuscb); - - // Now let's extract the result. - const std::vector& points = auto_supports.output(); - this->throw_if_canceled(); - po.m_supportdata->pts = points; - - BOOST_LOG_TRIVIAL(debug) << "Automatic support points: " - << po.m_supportdata->pts.size(); - - // Using RELOAD_SLA_SUPPORT_POINTS to tell the Plater to pass - // the update status to GLGizmoSlaSupports - m_report_status(*this, - -1, - L("Generating support points"), - SlicingStatus::RELOAD_SLA_SUPPORT_POINTS); - } - else { - // There are either some points on the front-end, or the user - // removed them on purpose. No calculation will be done. - po.m_supportdata->pts = po.transformed_support_points(); - } - - // If the zero elevation mode is engaged, we have to filter out all the - // points that are on the bottom of the object - if (is_zero_elevation(po.config())) { - double tolerance = po.config().pad_enable.getBool() - ? po.m_config.pad_wall_thickness.getFloat() - : po.m_config.support_base_height.getFloat(); - - remove_bottom_points(po.m_supportdata->pts, - po.m_supportdata->emesh.ground_level(), - tolerance); - } - }; - - // In this step we create the supports - auto support_tree = [this, ostepd](SLAPrintObject& po) - { - if(!po.m_supportdata) return; - - sla::PadConfig pcfg = make_pad_cfg(po.m_config); - - if (pcfg.embed_object) - po.m_supportdata->emesh.ground_level_offset(pcfg.wall_thickness_mm); - - po.m_supportdata->cfg = make_support_cfg(po.m_config); - - // scaling for the sub operations - double d = ostepd * OBJ_STEP_LEVELS[slaposSupportTree] / 100.0; - double init = m_report_status.status(); - JobController ctl; - - ctl.statuscb = [this, d, init](unsigned st, const std::string &logmsg) { - double current = init + st * d; - if (std::round(m_report_status.status()) < std::round(current)) - m_report_status(*this, current, - OBJ_STEP_LABELS(slaposSupportTree), - SlicingStatus::DEFAULT, logmsg); - }; - ctl.stopcondition = [this]() { return canceled(); }; - ctl.cancelfn = [this]() { throw_if_canceled(); }; - - po.m_supportdata->create_support_tree(ctl); - - if (!po.m_config.supports_enable.getBool()) return; - - throw_if_canceled(); - - // Create the unified mesh - auto rc = SlicingStatus::RELOAD_SCENE; - - // This is to prevent "Done." being displayed during merged_mesh() - m_report_status(*this, -1, L("Visualizing supports")); - - BOOST_LOG_TRIVIAL(debug) << "Processed support point count " - << po.m_supportdata->pts.size(); - - // Check the mesh for later troubleshooting. - if(po.support_mesh().empty()) - BOOST_LOG_TRIVIAL(warning) << "Support mesh is empty"; - - m_report_status(*this, -1, L("Visualizing supports"), rc); - }; - - // This step generates the sla base pad - auto generate_pad = [this](SLAPrintObject& po) { - // this step can only go after the support tree has been created - // and before the supports had been sliced. (or the slicing has to be - // repeated) - - if(po.m_config.pad_enable.getBool()) - { - // Get the distilled pad configuration from the config - sla::PadConfig pcfg = make_pad_cfg(po.m_config); - - ExPolygons bp; // This will store the base plate of the pad. - double pad_h = pcfg.full_height(); - const TriangleMesh &trmesh = po.transformed_mesh(); - - // This call can get pretty time consuming - auto thrfn = [this](){ throw_if_canceled(); }; - - if (!po.m_config.supports_enable.getBool() || pcfg.embed_object) { - // No support (thus no elevation) or zero elevation mode - // we sometimes call it "builtin pad" is enabled so we will - // get a sample from the bottom of the mesh and use it for pad - // creation. - sla::pad_blueprint(trmesh, bp, float(pad_h), - float(po.m_config.layer_height.getFloat()), - thrfn); - } - - po.m_supportdata->support_tree_ptr->add_pad(bp, pcfg); - auto &pad_mesh = po.m_supportdata->support_tree_ptr->retrieve_mesh(MeshType::Pad); - - if (!validate_pad(pad_mesh, pcfg)) - throw std::runtime_error( - L("No pad can be generated for this model with the " - "current configuration")); - - } else if(po.m_supportdata && po.m_supportdata->support_tree_ptr) { - po.m_supportdata->support_tree_ptr->remove_pad(); - } - - po.throw_if_canceled(); - auto rc = SlicingStatus::RELOAD_SCENE; - m_report_status(*this, -1, L("Visualizing supports"), rc); - }; - - // Slicing the support geometries similarly to the model slicing procedure. - // If the pad had been added previously (see step "base_pool" than it will - // be part of the slices) - auto slice_supports = [this](SLAPrintObject& po) { - auto& sd = po.m_supportdata; - - if(sd) sd->support_slices.clear(); - - // Don't bother if no supports and no pad is present. - if (!po.m_config.supports_enable.getBool() && - !po.m_config.pad_enable.getBool()) - return; - - if(sd && sd->support_tree_ptr) { - - std::vector heights; heights.reserve(po.m_slice_index.size()); - - for(auto& rec : po.m_slice_index) { - heights.emplace_back(rec.slice_level()); - } - - sd->support_slices = sd->support_tree_ptr->slice( - heights, float(po.config().slice_closing_radius.value)); - } - - double doffs = m_printer_config.absolute_correction.getFloat(); - coord_t clpr_offs = scaled(doffs); - for(size_t i = 0; - i < sd->support_slices.size() && i < po.m_slice_index.size(); - ++i) - { - // We apply the printer correction offset here. - if(clpr_offs != 0) - sd->support_slices[i] = - offset_ex(sd->support_slices[i], float(clpr_offs)); - - po.m_slice_index[i].set_support_slice_idx(po, i); - } - - // Using RELOAD_SLA_PREVIEW to tell the Plater to pass the update - // status to the 3D preview to load the SLA slices. - m_report_status(*this, -2, "", SlicingStatus::RELOAD_SLA_PREVIEW); - }; - - // Merging the slices from all the print objects into one slice grid and - // calculating print statistics from the merge result. - auto merge_slices_and_eval_stats = [this, ilhs]() { - - // clear the rasterizer input - m_printer_input.clear(); - - size_t mx = 0; - for(SLAPrintObject * o : m_objects) { - if(auto m = o->get_slice_index().size() > mx) mx = m; - } - - m_printer_input.reserve(mx); - - auto eps = coord_t(SCALED_EPSILON); - - for(SLAPrintObject * o : m_objects) { - coord_t gndlvl = o->get_slice_index().front().print_level() - ilhs; - - for(const SliceRecord& slicerecord : o->get_slice_index()) { - coord_t lvlid = slicerecord.print_level() - gndlvl; - - // Neat trick to round the layer levels to the grid. - lvlid = eps * (lvlid / eps); - - auto it = std::lower_bound(m_printer_input.begin(), - m_printer_input.end(), - PrintLayer(lvlid)); - - if(it == m_printer_input.end() || it->level() != lvlid) - it = m_printer_input.insert(it, PrintLayer(lvlid)); - - - it->add(slicerecord); - } - } - - m_print_statistics.clear(); - - using ClipperPoint = ClipperLib::IntPoint; - using ClipperPolygon = ClipperLib::Polygon; // see clipper_polygon.hpp in libnest2d - using ClipperPolygons = std::vector; - namespace sl = libnest2d::shapelike; // For algorithms - - // Set up custom union and diff functions for clipper polygons - auto polyunion = [] (const ClipperPolygons& subjects) - { - ClipperLib::Clipper clipper; - - bool closed = true; - - for(auto& path : subjects) { - clipper.AddPath(path.Contour, ClipperLib::ptSubject, closed); - clipper.AddPaths(path.Holes, ClipperLib::ptSubject, closed); - } - - auto mode = ClipperLib::pftPositive; - - return libnest2d::clipper_execute(clipper, ClipperLib::ctUnion, mode, mode); - }; - - auto polydiff = [](const ClipperPolygons& subjects, const ClipperPolygons& clips) - { - ClipperLib::Clipper clipper; - - bool closed = true; - - for(auto& path : subjects) { - clipper.AddPath(path.Contour, ClipperLib::ptSubject, closed); - clipper.AddPaths(path.Holes, ClipperLib::ptSubject, closed); - } - - for(auto& path : clips) { - clipper.AddPath(path.Contour, ClipperLib::ptClip, closed); - clipper.AddPaths(path.Holes, ClipperLib::ptClip, closed); - } - - auto mode = ClipperLib::pftPositive; - - return libnest2d::clipper_execute(clipper, ClipperLib::ctDifference, mode, mode); - }; - - // libnest calculates positive area for clockwise polygons, Slic3r is in counter-clockwise - auto areafn = [](const ClipperPolygon& poly) { return - sl::area(poly); }; - - const double area_fill = m_printer_config.area_fill.getFloat()*0.01;// 0.5 (50%); - const double fast_tilt = m_printer_config.fast_tilt_time.getFloat();// 5.0; - const double slow_tilt = m_printer_config.slow_tilt_time.getFloat();// 8.0; - - const double init_exp_time = m_material_config.initial_exposure_time.getFloat(); - const double exp_time = m_material_config.exposure_time.getFloat(); - - const int fade_layers_cnt = m_default_object_config.faded_layers.getInt();// 10 // [3;20] - - const auto width = scaled(m_printer_config.display_width.getFloat()); - const auto height = scaled(m_printer_config.display_height.getFloat()); - const double display_area = width*height; - - // get polygons for all instances in the object - auto get_all_polygons = - [](const ExPolygons& input_polygons, - const std::vector& instances, - bool is_lefthanded) - { - ClipperPolygons polygons; - polygons.reserve(input_polygons.size() * instances.size()); - - for (const ExPolygon& polygon : input_polygons) { - if(polygon.contour.empty()) continue; - - for (size_t i = 0; i < instances.size(); ++i) - { - ClipperPolygon poly; - - // We need to reverse if is_lefthanded is true but - bool needreverse = is_lefthanded; - - // should be a move - poly.Contour.reserve(polygon.contour.size() + 1); - - auto& cntr = polygon.contour.points; - if(needreverse) - for(auto it = cntr.rbegin(); it != cntr.rend(); ++it) - poly.Contour.emplace_back(it->x(), it->y()); - else - for(auto& p : cntr) - poly.Contour.emplace_back(p.x(), p.y()); - - for(auto& h : polygon.holes) { - poly.Holes.emplace_back(); - auto& hole = poly.Holes.back(); - hole.reserve(h.points.size() + 1); - - if(needreverse) - for(auto it = h.points.rbegin(); it != h.points.rend(); ++it) - hole.emplace_back(it->x(), it->y()); - else - for(auto& p : h.points) - hole.emplace_back(p.x(), p.y()); - } - - if(is_lefthanded) { - for(auto& p : poly.Contour) p.X = -p.X; - for(auto& h : poly.Holes) for(auto& p : h) p.X = -p.X; - } - - sl::rotate(poly, double(instances[i].rotation)); - sl::translate(poly, ClipperPoint{instances[i].shift(X), - instances[i].shift(Y)}); - - polygons.emplace_back(std::move(poly)); - } - } - return polygons; - }; - - double supports_volume(0.0); - double models_volume(0.0); - - double estim_time(0.0); - - size_t slow_layers = 0; - size_t fast_layers = 0; - - const double delta_fade_time = (init_exp_time - exp_time) / (fade_layers_cnt + 1); - double fade_layer_time = init_exp_time; - - SpinMutex mutex; - using Lock = std::lock_guard; - - // Going to parallel: - auto printlayerfn = [this, - // functions and read only vars - get_all_polygons, polyunion, polydiff, areafn, - area_fill, display_area, exp_time, init_exp_time, fast_tilt, slow_tilt, delta_fade_time, - - // write vars - &mutex, &models_volume, &supports_volume, &estim_time, &slow_layers, - &fast_layers, &fade_layer_time](size_t sliced_layer_cnt) - { - PrintLayer& layer = m_printer_input[sliced_layer_cnt]; - - // vector of slice record references - auto& slicerecord_references = layer.slices(); - - if(slicerecord_references.empty()) return; - - // Layer height should match for all object slices for a given level. - const auto l_height = double(slicerecord_references.front().get().layer_height()); - - // Calculation of the consumed material - - ClipperPolygons model_polygons; - ClipperPolygons supports_polygons; - - size_t c = std::accumulate(layer.slices().begin(), - layer.slices().end(), - size_t(0), - [](size_t a, const SliceRecord &sr) { - return a + sr.get_slice(soModel) - .size(); - }); - - model_polygons.reserve(c); - - c = std::accumulate(layer.slices().begin(), - layer.slices().end(), - size_t(0), - [](size_t a, const SliceRecord &sr) { - return a + sr.get_slice(soModel).size(); - }); - - supports_polygons.reserve(c); - - for(const SliceRecord& record : layer.slices()) { - const SLAPrintObject *po = record.print_obj(); - - const ExPolygons &modelslices = record.get_slice(soModel); - - bool is_lefth = record.print_obj()->is_left_handed(); - if (!modelslices.empty()) { - ClipperPolygons v = get_all_polygons(modelslices, po->instances(), is_lefth); - for(ClipperPolygon& p_tmp : v) model_polygons.emplace_back(std::move(p_tmp)); - } - - const ExPolygons &supportslices = record.get_slice(soSupport); - - if (!supportslices.empty()) { - ClipperPolygons v = get_all_polygons(supportslices, po->instances(), is_lefth); - for(ClipperPolygon& p_tmp : v) supports_polygons.emplace_back(std::move(p_tmp)); - } - } - - model_polygons = polyunion(model_polygons); - double layer_model_area = 0; - for (const ClipperPolygon& polygon : model_polygons) - layer_model_area += areafn(polygon); - - if (layer_model_area < 0 || layer_model_area > 0) { - Lock lck(mutex); models_volume += layer_model_area * l_height; - } - - if(!supports_polygons.empty()) { - if(model_polygons.empty()) supports_polygons = polyunion(supports_polygons); - else supports_polygons = polydiff(supports_polygons, model_polygons); - // allegedly, union of subject is done withing the diff according to the pftPositive polyFillType - } - - double layer_support_area = 0; - for (const ClipperPolygon& polygon : supports_polygons) - layer_support_area += areafn(polygon); - - if (layer_support_area < 0 || layer_support_area > 0) { - Lock lck(mutex); supports_volume += layer_support_area * l_height; - } - - // Here we can save the expensively calculated polygons for printing - ClipperPolygons trslices; - trslices.reserve(model_polygons.size() + supports_polygons.size()); - for(ClipperPolygon& poly : model_polygons) trslices.emplace_back(std::move(poly)); - for(ClipperPolygon& poly : supports_polygons) trslices.emplace_back(std::move(poly)); - - layer.transformed_slices(polyunion(trslices)); - - // Calculation of the slow and fast layers to the future controlling those values on FW - - const bool is_fast_layer = (layer_model_area + layer_support_area) <= display_area*area_fill; - const double tilt_time = is_fast_layer ? fast_tilt : slow_tilt; - - { Lock lck(mutex); - if (is_fast_layer) - fast_layers++; - else - slow_layers++; - - - // Calculation of the printing time - - if (sliced_layer_cnt < 3) - estim_time += init_exp_time; - else if (fade_layer_time > exp_time) - { - fade_layer_time -= delta_fade_time; - estim_time += fade_layer_time; - } - else - estim_time += exp_time; - - estim_time += tilt_time; - } - }; - - // sequential version for debugging: - // for(size_t i = 0; i < m_printer_input.size(); ++i) printlayerfn(i); - tbb::parallel_for(0, m_printer_input.size(), printlayerfn); - - auto SCALING2 = SCALING_FACTOR * SCALING_FACTOR; - m_print_statistics.support_used_material = supports_volume * SCALING2; - m_print_statistics.objects_used_material = models_volume * SCALING2; - - // Estimated printing time - // A layers count o the highest object - if (m_printer_input.size() == 0) - m_print_statistics.estimated_print_time = std::nan(""); - else - m_print_statistics.estimated_print_time = estim_time; - - m_print_statistics.fast_layers_count = fast_layers; - m_print_statistics.slow_layers_count = slow_layers; - -#if ENABLE_THUMBNAIL_GENERATOR - // second argument set to -3 to differentiate it from the same call made into slice_supports() - m_report_status(*this, -3, "", SlicingStatus::RELOAD_SLA_PREVIEW); -#else - m_report_status(*this, -2, "", SlicingStatus::RELOAD_SLA_PREVIEW); -#endif // ENABLE_THUMBNAIL_GENERATOR - }; - - // Rasterizing the model objects, and their supports - auto rasterize = [this]() { - if(canceled()) return; - - // Set up the printer, allocate space for all the layers - sla::RasterWriter &printer = init_printer(); - - auto lvlcnt = unsigned(m_printer_input.size()); - printer.layers(lvlcnt); - - // coefficient to map the rasterization state (0-99) to the allocated - // portion (slot) of the process state - double sd = (100 - max_objstatus) / 100.0; - - // slot is the portion of 100% that is realted to rasterization - unsigned slot = PRINT_STEP_LEVELS[slapsRasterize]; - - // pst: previous state - double pst = m_report_status.status(); - - double increment = (slot * sd) / m_printer_input.size(); - double dstatus = m_report_status.status(); - - SpinMutex slck; - - // procedure to process one height level. This will run in parallel - auto lvlfn = - [this, &slck, &printer, increment, &dstatus, &pst] - (unsigned level_id) - { - if(canceled()) return; - - PrintLayer& printlayer = m_printer_input[level_id]; - - // Switch to the appropriate layer in the printer - printer.begin_layer(level_id); - - for(const ClipperLib::Polygon& poly : printlayer.transformed_slices()) - printer.draw_polygon(poly, level_id); - - // Finish the layer for later saving it. - printer.finish_layer(level_id); - - // Status indication guarded with the spinlock - { - std::lock_guard lck(slck); - dstatus += increment; - double st = std::round(dstatus); - if(st > pst) { - m_report_status(*this, st, - PRINT_STEP_LABELS(slapsRasterize)); - pst = st; - } - } - }; - - // last minute escape - if(canceled()) return; - - // Sequential version (for testing) - // for(unsigned l = 0; l < lvlcnt; ++l) lvlfn(l); - - // Print all the layers in parallel - tbb::parallel_for(0, lvlcnt, lvlfn); - - // Set statistics values to the printer - sla::RasterWriter::PrintStatistics stats; - stats.used_material = (m_print_statistics.objects_used_material + - m_print_statistics.support_used_material) / - 1000; - - int num_fade = m_default_object_config.faded_layers.getInt(); - stats.num_fade = num_fade >= 0 ? size_t(num_fade) : size_t(0); - stats.num_fast = m_print_statistics.fast_layers_count; - stats.num_slow = m_print_statistics.slow_layers_count; - stats.estimated_print_time_s = m_print_statistics.estimated_print_time; - - m_printer->set_statistics(stats); - }; - - using slaposFn = std::function; - using slapsFn = std::function; - - slaposFn pobj_program[] = - { - slice_model, support_points, support_tree, generate_pad, slice_supports - }; + + Steps printsteps{this}; // We want to first process all objects... std::vector level1_obj_steps = { - slaposObjectSlice, slaposSupportPoints, slaposSupportTree, slaposPad + slaposHollowing, slaposObjectSlice, slaposSupportPoints, slaposSupportTree, slaposPad }; // and then slice all supports to allow preview to be displayed ASAP @@ -1478,10 +686,9 @@ void SLAPrint::process() slaposSliceSupports }; - slapsFn print_program[] = { merge_slices_and_eval_stats, rasterize }; SLAPrintStep print_steps[] = { slapsMergeSlicesAndEval, slapsRasterize }; - - double st = min_objstatus; + + double st = Steps::min_objstatus; BOOST_LOG_TRIVIAL(info) << "Start slicing process."; @@ -1496,10 +703,10 @@ void SLAPrint::process() std::array step_times {}; auto apply_steps_on_objects = - [this, &st, ostepd, &pobj_program, &step_times, &bench] + [this, &st, &printsteps, &step_times, &bench] (const std::vector &steps) { - unsigned incr = 0; + double incr = 0; for (SLAPrintObject *po : m_objects) { for (SLAPrintObjectStep step : steps) { @@ -1509,19 +716,19 @@ void SLAPrint::process() // throws the canceled signal. throw_if_canceled(); - st += incr * ostepd; + st += incr; if (po->m_stepmask[step] && po->set_started(step)) { - m_report_status(*this, st, OBJ_STEP_LABELS(step)); + m_report_status(*this, st, printsteps.label(step)); bench.start(); - pobj_program[step](*po); + printsteps.execute(step, *po); bench.stop(); step_times[step] += bench.getElapsedSec(); throw_if_canceled(); po->set_done(step); } - incr = OBJ_STEP_LEVELS[step]; + incr = printsteps.progressrange(step); } } }; @@ -1531,23 +738,22 @@ void SLAPrint::process() // this would disable the rasterization step // std::fill(m_stepmask.begin(), m_stepmask.end(), false); - - double pstd = (100 - max_objstatus) / 100.0; - st = max_objstatus; + + st = Steps::max_objstatus; for(SLAPrintStep currentstep : print_steps) { throw_if_canceled(); if (m_stepmask[currentstep] && set_started(currentstep)) { - m_report_status(*this, st, PRINT_STEP_LABELS(currentstep)); + m_report_status(*this, st, printsteps.label(currentstep)); bench.start(); - print_program[currentstep](); + printsteps.execute(currentstep); bench.stop(); step_times[slaposCount + currentstep] += bench.getElapsedSec(); throw_if_canceled(); set_done(currentstep); } - st += PRINT_STEP_LEVELS[currentstep] * pstd; + st += printsteps.progressrange(currentstep); } // If everything vent well @@ -1556,10 +762,10 @@ void SLAPrint::process() #ifdef SLAPRINT_DO_BENCHMARK std::string csvbenchstr; for (size_t i = 0; i < size_t(slaposCount); ++i) - csvbenchstr += OBJ_STEP_LABELS(i) + ";"; + csvbenchstr += printsteps.label(SLAPrintObjectStep(i)) + ";"; for (size_t i = 0; i < size_t(slapsCount); ++i) - csvbenchstr += PRINT_STEP_LABELS(i) + ";"; + csvbenchstr += printsteps.label(SLAPrintStep(i)) + ";"; csvbenchstr += "\n"; for (double t : step_times) csvbenchstr += std::to_string(t) + ";"; @@ -1711,7 +917,14 @@ bool SLAPrintObject::invalidate_state_by_config_options(const std::vector steps; bool invalidated = false; for (const t_config_option_key &opt_key : opt_keys) { - if ( opt_key == "layer_height" + if ( opt_key == "hollowing_enable" + || opt_key == "hollowing_min_thickness" + || opt_key == "hollowing_quality" + || opt_key == "hollowing_closing_distance" + ) { + steps.emplace_back(slaposHollowing); + } else if ( + opt_key == "layer_height" || opt_key == "faded_layers" || opt_key == "pad_enable" || opt_key == "pad_wall_thickness" @@ -1769,8 +982,14 @@ bool SLAPrintObject::invalidate_step(SLAPrintObjectStep step) { bool invalidated = Inherited::invalidate_step(step); // propagate to dependent steps - if (step == slaposObjectSlice) { + if (step == slaposHollowing) { invalidated |= this->invalidate_all_steps(); + } else if (step == slaposObjectSlice) { + invalidated |= this->invalidate_steps({ slaposDrillHolesIfHollowed, slaposSupportPoints, slaposSupportTree, slaposPad, slaposSliceSupports }); + invalidated |= m_print->invalidate_step(slapsMergeSlicesAndEval); + } else if (step == slaposDrillHolesIfHollowed) { + invalidated |= this->invalidate_steps({ slaposSupportPoints, slaposSupportTree, slaposPad, slaposSliceSupports }); + invalidated |= m_print->invalidate_step(slapsMergeSlicesAndEval); } else if (step == slaposSupportPoints) { invalidated |= this->invalidate_steps({ slaposSupportTree, slaposPad, slaposSliceSupports }); invalidated |= m_print->invalidate_step(slapsMergeSlicesAndEval); @@ -1925,6 +1144,14 @@ const TriangleMesh& SLAPrintObject::pad_mesh() const return EMPTY_MESH; } +const TriangleMesh &SLAPrintObject::hollowed_interior_mesh() const +{ + if (m_hollowing_data && m_config.hollowing_enable.getBool()) + return m_hollowing_data->interior; + + return EMPTY_MESH; +} + const TriangleMesh &SLAPrintObject::transformed_mesh() const { // we need to transform the raw mesh... // currently all the instances share the same x and y rotation and scaling @@ -1938,21 +1165,29 @@ const TriangleMesh &SLAPrintObject::transformed_mesh() const { return m_transformed_rmesh.get(); } -std::vector SLAPrintObject::transformed_support_points() const +sla::SupportPoints SLAPrintObject::transformed_support_points() const { assert(m_model_object != nullptr); - std::vector& spts = m_model_object->sla_support_points; - - // this could be cached as well - std::vector ret; - ret.reserve(spts.size()); - - for(sla::SupportPoint& sp : spts) { - Vec3f transformed_pos = trafo().cast() * sp.pos; - ret.emplace_back(transformed_pos, sp.head_front_radius, sp.is_new_island); + auto spts = m_model_object->sla_support_points; + auto tr = trafo().cast(); + for (sla::SupportPoint& suppt : spts) { + suppt.pos = tr * suppt.pos; } + + return spts; +} - return ret; +sla::DrainHoles SLAPrintObject::transformed_drainhole_points() const +{ + assert(m_model_object != nullptr); + auto pts = m_model_object->sla_drain_holes; + auto tr = trafo().cast(); + for (sla::DrainHole &hl : pts) { + hl.pos = tr * hl.pos; + hl.normal = tr * hl.normal - tr.translation(); + } + + return pts; } DynamicConfig SLAPrintStatistics::config() const diff --git a/src/libslic3r/SLAPrint.hpp b/src/libslic3r/SLAPrint.hpp index 8f386d407f..1b8e18a2a3 100644 --- a/src/libslic3r/SLAPrint.hpp +++ b/src/libslic3r/SLAPrint.hpp @@ -3,8 +3,8 @@ #include #include "PrintBase.hpp" -//#include "PrintExport.hpp" -#include "SLA/SLARasterWriter.hpp" +#include "SLA/RasterWriter.hpp" +#include "SLA/SupportTree.hpp" #include "Point.hpp" #include "MTUtils.hpp" #include "Zipper.hpp" @@ -19,7 +19,9 @@ enum SLAPrintStep : unsigned int { }; enum SLAPrintObjectStep : unsigned int { + slaposHollowing, slaposObjectSlice, + slaposDrillHolesIfHollowed, slaposSupportPoints, slaposSupportTree, slaposPad, @@ -73,13 +75,17 @@ public: // Support mesh is only valid if this->is_step_done(slaposSupportTree) is true. const TriangleMesh& support_mesh() const; // Get a pad mesh centered around origin in XY, and with zero rotation around Z applied. - // Support mesh is only valid if this->is_step_done(slaposBasePool) is true. + // Support mesh is only valid if this->is_step_done(slaposPad) is true. const TriangleMesh& pad_mesh() const; + + // Ready after this->is_step_done(slaposHollowing) is true + const TriangleMesh& hollowed_interior_mesh() const; // This will return the transformed mesh which is cached const TriangleMesh& transformed_mesh() const; - std::vector transformed_support_points() const; + sla::SupportPoints transformed_support_points() const; + sla::DrainHoles transformed_drainhole_points() const; // Get the needed Z elevation for the model geometry if supports should be // displayed. This Z offset should also be applied to the support @@ -287,9 +293,35 @@ private: // Caching the transformed (m_trafo) raw mesh of the object mutable CachedObject m_transformed_rmesh; - - class SupportData; + + class SupportData : public sla::SupportableMesh + { + public: + sla::SupportTree::UPtr support_tree_ptr; // the supports + std::vector support_slices; // sliced supports + + inline SupportData(const TriangleMesh &t) + : sla::SupportableMesh{t, {}, {}} + {} + + sla::SupportTree::UPtr &create_support_tree(const sla::JobController &ctl) + { + support_tree_ptr = sla::SupportTree::create(*this, ctl); + return support_tree_ptr; + } + }; + std::unique_ptr m_supportdata; + + class HollowingData + { + public: + + TriangleMesh interior; + // std::vector + }; + + std::unique_ptr m_hollowing_data; }; using PrintObjects = std::vector; @@ -339,7 +371,9 @@ class SLAPrint : public PrintBaseWithState { private: // Prevents erroneous use by other classes. typedef PrintBaseWithState Inherited; - + + class Steps; // See SLAPrintSteps.cpp + public: SLAPrint(): m_stepmask(slapsCount, true) {} @@ -401,8 +435,8 @@ public: template void transformed_slices(Container&& c) { m_transformed_slices = std::forward(c); } - - friend void SLAPrint::process(); + + friend class SLAPrint::Steps; public: @@ -478,6 +512,19 @@ private: friend SLAPrintObject; }; +// Helper functions: + +bool is_zero_elevation(const SLAPrintObjectConfig &c); + +sla::SupportConfig make_support_cfg(const SLAPrintObjectConfig& c); + +sla::PadConfig::EmbedObject builtin_pad_cfg(const SLAPrintObjectConfig& c); + +sla::PadConfig make_pad_cfg(const SLAPrintObjectConfig& c); + +bool validate_pad(const TriangleMesh &pad, const sla::PadConfig &pcfg); + + } // namespace Slic3r #endif /* slic3r_SLAPrint_hpp_ */ diff --git a/src/libslic3r/SLAPrintSteps.cpp b/src/libslic3r/SLAPrintSteps.cpp new file mode 100644 index 0000000000..0ae0e66a44 --- /dev/null +++ b/src/libslic3r/SLAPrintSteps.cpp @@ -0,0 +1,862 @@ +#include + + +// Need the cylinder method for the the drainholes in hollowing step +#include + +#include +#include +#include + +#include + +// For geometry algorithms with native Clipper types (no copies and conversions) +#include + +#include + +#include "I18N.hpp" + +//! macro used to mark string used at localization, +//! return same string +#define L(s) Slic3r::I18N::translate(s) + +namespace Slic3r { + +namespace { + +const std::array OBJ_STEP_LEVELS = { + 5, // slaposHollowing, + 20, // slaposObjectSlice, + 5, // slaposDrillHolesIfHollowed + 20, // slaposSupportPoints, + 10, // slaposSupportTree, + 10, // slaposPad, + 30, // slaposSliceSupports, +}; + +std::string OBJ_STEP_LABELS(size_t idx) +{ + switch (idx) { + case slaposHollowing: return L("Hollowing out the model"); + case slaposObjectSlice: return L("Slicing model"); + case slaposDrillHolesIfHollowed: return L("Drilling holes into hollowed model."); + case slaposSupportPoints: return L("Generating support points"); + case slaposSupportTree: return L("Generating support tree"); + case slaposPad: return L("Generating pad"); + case slaposSliceSupports: return L("Slicing supports"); + default:; + } + assert(false); + return "Out of bounds!"; +}; + +const std::array PRINT_STEP_LEVELS = { + 10, // slapsMergeSlicesAndEval + 90, // slapsRasterize +}; + +std::string PRINT_STEP_LABELS(size_t idx) +{ + switch (idx) { + case slapsMergeSlicesAndEval: return L("Merging slices and calculating statistics"); + case slapsRasterize: return L("Rasterizing layers"); + default:; + } + assert(false); return "Out of bounds!"; +}; + +} + +SLAPrint::Steps::Steps(SLAPrint *print) + : m_print{print} + , objcount{m_print->m_objects.size()} + , ilhd{m_print->m_material_config.initial_layer_height.getFloat()} + , ilh{float(ilhd)} + , ilhs{scaled(ilhd)} + , objectstep_scale{(max_objstatus - min_objstatus) / (objcount * 100.0)} +{} + +void SLAPrint::Steps::hollow_model(SLAPrintObject &po) +{ + if (!po.m_config.hollowing_enable.getBool()) { + BOOST_LOG_TRIVIAL(info) << "Skipping hollowing step!"; + po.m_hollowing_data.reset(); + return; + } else { + BOOST_LOG_TRIVIAL(info) << "Performing hollowing step!"; + } + + if (!po.m_hollowing_data) + po.m_hollowing_data.reset(new SLAPrintObject::HollowingData()); + + double thickness = po.m_config.hollowing_min_thickness.getFloat(); + double quality = po.m_config.hollowing_quality.getFloat(); + double closing_d = po.m_config.hollowing_closing_distance.getFloat(); + sla::HollowingConfig hlwcfg{thickness, quality, closing_d}; + auto meshptr = generate_interior(po.transformed_mesh(), hlwcfg); + if (meshptr) po.m_hollowing_data->interior = *meshptr; + + if (po.m_hollowing_data->interior.empty()) + BOOST_LOG_TRIVIAL(warning) << "Hollowed interior is empty!"; +} + +// The slicing will be performed on an imaginary 1D grid which starts from +// the bottom of the bounding box created around the supported model. So +// the first layer which is usually thicker will be part of the supports +// not the model geometry. Exception is when the model is not in the air +// (elevation is zero) and no pad creation was requested. In this case the +// model geometry starts on the ground level and the initial layer is part +// of it. In any case, the model and the supports have to be sliced in the +// same imaginary grid (the height vector argument to TriangleMeshSlicer). +void SLAPrint::Steps::slice_model(SLAPrintObject &po) +{ + TriangleMesh hollowed_mesh; + + bool is_hollowing = po.m_config.hollowing_enable.getBool() && po.m_hollowing_data; + + if (is_hollowing) { + hollowed_mesh = po.transformed_mesh(); + hollowed_mesh.merge(po.m_hollowing_data->interior); + hollowed_mesh.require_shared_vertices(); + } + + const TriangleMesh &mesh = is_hollowing ? hollowed_mesh : po.transformed_mesh(); + + // We need to prepare the slice index... + + double lhd = m_print->m_objects.front()->m_config.layer_height.getFloat(); + float lh = float(lhd); + coord_t lhs = scaled(lhd); + auto && bb3d = mesh.bounding_box(); + double minZ = bb3d.min(Z) - po.get_elevation(); + double maxZ = bb3d.max(Z); + auto minZf = float(minZ); + coord_t minZs = scaled(minZ); + coord_t maxZs = scaled(maxZ); + + po.m_slice_index.clear(); + + size_t cap = size_t(1 + (maxZs - minZs - ilhs) / lhs); + po.m_slice_index.reserve(cap); + + po.m_slice_index.emplace_back(minZs + ilhs, minZf + ilh / 2.f, ilh); + + for(coord_t h = minZs + ilhs + lhs; h <= maxZs; h += lhs) + po.m_slice_index.emplace_back(h, unscaled(h) - lh / 2.f, lh); + + // Just get the first record that is from the model: + auto slindex_it = + po.closest_slice_record(po.m_slice_index, float(bb3d.min(Z))); + + if(slindex_it == po.m_slice_index.end()) + //TRN To be shown at the status bar on SLA slicing error. + throw std::runtime_error( + L("Slicing had to be stopped due to an internal error: " + "Inconsistent slice index.")); + + po.m_model_height_levels.clear(); + po.m_model_height_levels.reserve(po.m_slice_index.size()); + for(auto it = slindex_it; it != po.m_slice_index.end(); ++it) + po.m_model_height_levels.emplace_back(it->slice_level()); + + TriangleMeshSlicer slicer(&mesh); + + po.m_model_slices.clear(); + float closing_r = float(po.config().slice_closing_radius.value); + auto thr = [this]() { m_print->throw_if_canceled(); }; + auto &slice_grid = po.m_model_height_levels; + slicer.slice(slice_grid, closing_r, &po.m_model_slices, thr); + + sla::DrainHoles drainholes = po.transformed_drainhole_points(); + cut_drainholes(po.m_model_slices, slice_grid, closing_r, drainholes, thr); + + auto mit = slindex_it; + double doffs = m_print->m_printer_config.absolute_correction.getFloat(); + coord_t clpr_offs = scaled(doffs); + for(size_t id = 0; + id < po.m_model_slices.size() && mit != po.m_slice_index.end(); + id++) + { + // We apply the printer correction offset here. + if(clpr_offs != 0) + po.m_model_slices[id] = + offset_ex(po.m_model_slices[id], float(clpr_offs)); + + mit->set_model_slice_idx(po, id); ++mit; + } + + if(po.m_config.supports_enable.getBool() || po.m_config.pad_enable.getBool()) + { + po.m_supportdata.reset(new SLAPrintObject::SupportData(mesh)); + } +} + +// In this step we check the slices, identify island and cover them with +// support points. Then we sprinkle the rest of the mesh. +void SLAPrint::Steps::support_points(SLAPrintObject &po) +{ + // If supports are disabled, we can skip the model scan. + if(!po.m_config.supports_enable.getBool()) return; + + bool is_hollowing = po.m_config.hollowing_enable.getBool() && po.m_hollowing_data; + + TriangleMesh hollowed_mesh; + if (is_hollowing) { + hollowed_mesh = po.transformed_mesh(); + hollowed_mesh.merge(po.m_hollowing_data->interior); + hollowed_mesh.require_shared_vertices(); + } + + const TriangleMesh &mesh = is_hollowing ? hollowed_mesh : po.transformed_mesh(); + + if (!po.m_supportdata) + po.m_supportdata.reset(new SLAPrintObject::SupportData(mesh)); + + const ModelObject& mo = *po.m_model_object; + + BOOST_LOG_TRIVIAL(debug) << "Support point count " + << mo.sla_support_points.size(); + + // Unless the user modified the points or we already did the calculation, + // we will do the autoplacement. Otherwise we will just blindly copy the + // frontend data into the backend cache. + if (mo.sla_points_status != sla::PointsStatus::UserModified) { + + // calculate heights of slices (slices are calculated already) + const std::vector& heights = po.m_model_height_levels; + + // Tell the mesh where drain holes are. Although the points are + // calculated on slices, the algorithm then raycasts the points + // so they actually lie on the mesh. + po.m_supportdata->emesh.load_holes(po.transformed_drainhole_points()); + + throw_if_canceled(); + sla::SupportPointGenerator::Config config; + const SLAPrintObjectConfig& cfg = po.config(); + + // the density config value is in percents: + config.density_relative = float(cfg.support_points_density_relative / 100.f); + config.minimal_distance = float(cfg.support_points_minimal_distance); + config.head_diameter = float(cfg.support_head_front_diameter); + + // scaling for the sub operations + double d = objectstep_scale * OBJ_STEP_LEVELS[slaposSupportPoints] / 100.0; + double init = current_status(); + + auto statuscb = [this, d, init](unsigned st) + { + double current = init + st * d; + if(std::round(current_status()) < std::round(current)) + report_status(current, OBJ_STEP_LABELS(slaposSupportPoints)); + }; + + // Construction of this object does the calculation. + throw_if_canceled(); + sla::SupportPointGenerator auto_supports( + po.m_supportdata->emesh, po.get_model_slices(), heights, config, + [this]() { throw_if_canceled(); }, statuscb); + + // Now let's extract the result. + const std::vector& points = auto_supports.output(); + throw_if_canceled(); + po.m_supportdata->pts = points; + + BOOST_LOG_TRIVIAL(debug) << "Automatic support points: " + << po.m_supportdata->pts.size(); + + // Using RELOAD_SLA_SUPPORT_POINTS to tell the Plater to pass + // the update status to GLGizmoSlaSupports + report_status(-1, L("Generating support points"), + SlicingStatus::RELOAD_SLA_SUPPORT_POINTS); + } else { + // There are either some points on the front-end, or the user + // removed them on purpose. No calculation will be done. + po.m_supportdata->pts = po.transformed_support_points(); + } + + // If the zero elevation mode is engaged, we have to filter out all the + // points that are on the bottom of the object + if (is_zero_elevation(po.config())) { + double tolerance = po.config().pad_enable.getBool() ? + po.m_config.pad_wall_thickness.getFloat() : + po.m_config.support_base_height.getFloat(); + + remove_bottom_points(po.m_supportdata->pts, + po.m_supportdata->emesh.ground_level(), + tolerance); + } +} + +void SLAPrint::Steps::support_tree(SLAPrintObject &po) +{ + if(!po.m_supportdata) return; + + sla::PadConfig pcfg = make_pad_cfg(po.m_config); + + if (pcfg.embed_object) + po.m_supportdata->emesh.ground_level_offset(pcfg.wall_thickness_mm); + + po.m_supportdata->cfg = make_support_cfg(po.m_config); + po.m_supportdata->emesh.load_holes(po.transformed_drainhole_points()); + + // scaling for the sub operations + double d = objectstep_scale * OBJ_STEP_LEVELS[slaposSupportTree] / 100.0; + double init = current_status(); + sla::JobController ctl; + + ctl.statuscb = [this, d, init](unsigned st, const std::string &logmsg) { + double current = init + st * d; + if (std::round(current_status()) < std::round(current)) + report_status(current, OBJ_STEP_LABELS(slaposSupportTree), + SlicingStatus::DEFAULT, logmsg); + }; + ctl.stopcondition = [this]() { return canceled(); }; + ctl.cancelfn = [this]() { throw_if_canceled(); }; + + po.m_supportdata->create_support_tree(ctl); + + if (!po.m_config.supports_enable.getBool()) return; + + throw_if_canceled(); + + // Create the unified mesh + auto rc = SlicingStatus::RELOAD_SCENE; + + // This is to prevent "Done." being displayed during merged_mesh() + report_status(-1, L("Visualizing supports")); + + BOOST_LOG_TRIVIAL(debug) << "Processed support point count " + << po.m_supportdata->pts.size(); + + // Check the mesh for later troubleshooting. + if(po.support_mesh().empty()) + BOOST_LOG_TRIVIAL(warning) << "Support mesh is empty"; + + report_status(-1, L("Visualizing supports"), rc); +} + +void SLAPrint::Steps::generate_pad(SLAPrintObject &po) { + // this step can only go after the support tree has been created + // and before the supports had been sliced. (or the slicing has to be + // repeated) + + if(po.m_config.pad_enable.getBool()) { + // Get the distilled pad configuration from the config + sla::PadConfig pcfg = make_pad_cfg(po.m_config); + + ExPolygons bp; // This will store the base plate of the pad. + double pad_h = pcfg.full_height(); + const TriangleMesh &trmesh = po.transformed_mesh(); + + if (!po.m_config.supports_enable.getBool() || pcfg.embed_object) { + // No support (thus no elevation) or zero elevation mode + // we sometimes call it "builtin pad" is enabled so we will + // get a sample from the bottom of the mesh and use it for pad + // creation. + sla::pad_blueprint(trmesh, bp, float(pad_h), + float(po.m_config.layer_height.getFloat()), + [this](){ throw_if_canceled(); }); + } + + po.m_supportdata->support_tree_ptr->add_pad(bp, pcfg); + auto &pad_mesh = po.m_supportdata->support_tree_ptr->retrieve_mesh(sla::MeshType::Pad); + + if (!validate_pad(pad_mesh, pcfg)) + throw std::runtime_error( + L("No pad can be generated for this model with the " + "current configuration")); + + } else if(po.m_supportdata && po.m_supportdata->support_tree_ptr) { + po.m_supportdata->support_tree_ptr->remove_pad(); + } + + throw_if_canceled(); + report_status(-1, L("Visualizing supports"), SlicingStatus::RELOAD_SCENE); +} + +// Slicing the support geometries similarly to the model slicing procedure. +// If the pad had been added previously (see step "base_pool" than it will +// be part of the slices) +void SLAPrint::Steps::slice_supports(SLAPrintObject &po) { + auto& sd = po.m_supportdata; + + if(sd) sd->support_slices.clear(); + + // Don't bother if no supports and no pad is present. + if (!po.m_config.supports_enable.getBool() && !po.m_config.pad_enable.getBool()) + return; + + if(sd && sd->support_tree_ptr) { + auto heights = reserve_vector(po.m_slice_index.size()); + + for(auto& rec : po.m_slice_index) heights.emplace_back(rec.slice_level()); + + sd->support_slices = sd->support_tree_ptr->slice( + heights, float(po.config().slice_closing_radius.value)); + } + + double doffs = m_print->m_printer_config.absolute_correction.getFloat(); + coord_t clpr_offs = scaled(doffs); + + for (size_t i = 0; i < sd->support_slices.size() && i < po.m_slice_index.size(); ++i) { + // We apply the printer correction offset here. + if (clpr_offs != 0) + sd->support_slices[i] = offset_ex(sd->support_slices[i], float(clpr_offs)); + + po.m_slice_index[i].set_support_slice_idx(po, i); + } + + // Using RELOAD_SLA_PREVIEW to tell the Plater to pass the update + // status to the 3D preview to load the SLA slices. + report_status(-2, "", SlicingStatus::RELOAD_SLA_PREVIEW); +} + +using ClipperPoint = ClipperLib::IntPoint; +using ClipperPolygon = ClipperLib::Polygon; // see clipper_polygon.hpp in libnest2d +using ClipperPolygons = std::vector; + +static ClipperPolygons polyunion(const ClipperPolygons &subjects) +{ + ClipperLib::Clipper clipper; + + bool closed = true; + + for(auto& path : subjects) { + clipper.AddPath(path.Contour, ClipperLib::ptSubject, closed); + clipper.AddPaths(path.Holes, ClipperLib::ptSubject, closed); + } + + auto mode = ClipperLib::pftPositive; + + return libnest2d::clipper_execute(clipper, ClipperLib::ctUnion, mode, mode); +} + +static ClipperPolygons polydiff(const ClipperPolygons &subjects, const ClipperPolygons& clips) +{ + ClipperLib::Clipper clipper; + + bool closed = true; + + for(auto& path : subjects) { + clipper.AddPath(path.Contour, ClipperLib::ptSubject, closed); + clipper.AddPaths(path.Holes, ClipperLib::ptSubject, closed); + } + + for(auto& path : clips) { + clipper.AddPath(path.Contour, ClipperLib::ptClip, closed); + clipper.AddPaths(path.Holes, ClipperLib::ptClip, closed); + } + + auto mode = ClipperLib::pftPositive; + + return libnest2d::clipper_execute(clipper, ClipperLib::ctDifference, mode, mode); +} + +// get polygons for all instances in the object +static ClipperPolygons get_all_polygons( + const ExPolygons & input_polygons, + const std::vector &instances, + bool is_lefthanded) +{ + namespace sl = libnest2d::sl; + + ClipperPolygons polygons; + polygons.reserve(input_polygons.size() * instances.size()); + + for (const ExPolygon& polygon : input_polygons) { + if(polygon.contour.empty()) continue; + + for (size_t i = 0; i < instances.size(); ++i) + { + ClipperPolygon poly; + + // We need to reverse if is_lefthanded is true but + bool needreverse = is_lefthanded; + + // should be a move + poly.Contour.reserve(polygon.contour.size() + 1); + + auto& cntr = polygon.contour.points; + if(needreverse) + for(auto it = cntr.rbegin(); it != cntr.rend(); ++it) + poly.Contour.emplace_back(it->x(), it->y()); + else + for(auto& p : cntr) + poly.Contour.emplace_back(p.x(), p.y()); + + for(auto& h : polygon.holes) { + poly.Holes.emplace_back(); + auto& hole = poly.Holes.back(); + hole.reserve(h.points.size() + 1); + + if(needreverse) + for(auto it = h.points.rbegin(); it != h.points.rend(); ++it) + hole.emplace_back(it->x(), it->y()); + else + for(auto& p : h.points) + hole.emplace_back(p.x(), p.y()); + } + + if(is_lefthanded) { + for(auto& p : poly.Contour) p.X = -p.X; + for(auto& h : poly.Holes) for(auto& p : h) p.X = -p.X; + } + + sl::rotate(poly, double(instances[i].rotation)); + sl::translate(poly, ClipperPoint{instances[i].shift(X), + instances[i].shift(Y)}); + + polygons.emplace_back(std::move(poly)); + } + } + + return polygons; +} + +void SLAPrint::Steps::initialize_printer_input() +{ + auto &printer_input = m_print->m_printer_input; + + // clear the rasterizer input + printer_input.clear(); + + size_t mx = 0; + for(SLAPrintObject * o : m_print->m_objects) { + if(auto m = o->get_slice_index().size() > mx) mx = m; + } + + printer_input.reserve(mx); + + auto eps = coord_t(SCALED_EPSILON); + + for(SLAPrintObject * o : m_print->m_objects) { + coord_t gndlvl = o->get_slice_index().front().print_level() - ilhs; + + for(const SliceRecord& slicerecord : o->get_slice_index()) { + coord_t lvlid = slicerecord.print_level() - gndlvl; + + // Neat trick to round the layer levels to the grid. + lvlid = eps * (lvlid / eps); + + auto it = std::lower_bound(printer_input.begin(), + printer_input.end(), + PrintLayer(lvlid)); + + if(it == printer_input.end() || it->level() != lvlid) + it = printer_input.insert(it, PrintLayer(lvlid)); + + + it->add(slicerecord); + } + } +} + +// Merging the slices from all the print objects into one slice grid and +// calculating print statistics from the merge result. +void SLAPrint::Steps::merge_slices_and_eval_stats() { + + initialize_printer_input(); + + auto &print_statistics = m_print->m_print_statistics; + auto &printer_config = m_print->m_printer_config; + auto &material_config = m_print->m_material_config; + auto &printer_input = m_print->m_printer_input; + + print_statistics.clear(); + + // libnest calculates positive area for clockwise polygons, Slic3r is in counter-clockwise + auto areafn = [](const ClipperPolygon& poly) { return - libnest2d::sl::area(poly); }; + + const double area_fill = printer_config.area_fill.getFloat()*0.01;// 0.5 (50%); + const double fast_tilt = printer_config.fast_tilt_time.getFloat();// 5.0; + const double slow_tilt = printer_config.slow_tilt_time.getFloat();// 8.0; + + const double init_exp_time = material_config.initial_exposure_time.getFloat(); + const double exp_time = material_config.exposure_time.getFloat(); + + const int fade_layers_cnt = m_print->m_default_object_config.faded_layers.getInt();// 10 // [3;20] + + const auto width = scaled(printer_config.display_width.getFloat()); + const auto height = scaled(printer_config.display_height.getFloat()); + const double display_area = width*height; + + double supports_volume(0.0); + double models_volume(0.0); + + double estim_time(0.0); + + size_t slow_layers = 0; + size_t fast_layers = 0; + + const double delta_fade_time = (init_exp_time - exp_time) / (fade_layers_cnt + 1); + double fade_layer_time = init_exp_time; + + sla::ccr::SpinningMutex mutex; + using Lock = std::lock_guard; + + // Going to parallel: + auto printlayerfn = [ + // functions and read only vars + areafn, area_fill, display_area, exp_time, init_exp_time, fast_tilt, slow_tilt, delta_fade_time, + + // write vars + &mutex, &models_volume, &supports_volume, &estim_time, &slow_layers, + &fast_layers, &fade_layer_time](PrintLayer& layer, size_t sliced_layer_cnt) + { + // vector of slice record references + auto& slicerecord_references = layer.slices(); + + if(slicerecord_references.empty()) return; + + // Layer height should match for all object slices for a given level. + const auto l_height = double(slicerecord_references.front().get().layer_height()); + + // Calculation of the consumed material + + ClipperPolygons model_polygons; + ClipperPolygons supports_polygons; + + size_t c = std::accumulate(layer.slices().begin(), + layer.slices().end(), + size_t(0), + [](size_t a, const SliceRecord &sr) { + return a + sr.get_slice(soModel).size(); + }); + + model_polygons.reserve(c); + + c = std::accumulate(layer.slices().begin(), + layer.slices().end(), + size_t(0), + [](size_t a, const SliceRecord &sr) { + return a + sr.get_slice(soModel).size(); + }); + + supports_polygons.reserve(c); + + for(const SliceRecord& record : layer.slices()) { + const SLAPrintObject *po = record.print_obj(); + + const ExPolygons &modelslices = record.get_slice(soModel); + + bool is_lefth = record.print_obj()->is_left_handed(); + if (!modelslices.empty()) { + ClipperPolygons v = get_all_polygons(modelslices, po->instances(), is_lefth); + for(ClipperPolygon& p_tmp : v) model_polygons.emplace_back(std::move(p_tmp)); + } + + const ExPolygons &supportslices = record.get_slice(soSupport); + + if (!supportslices.empty()) { + ClipperPolygons v = get_all_polygons(supportslices, po->instances(), is_lefth); + for(ClipperPolygon& p_tmp : v) supports_polygons.emplace_back(std::move(p_tmp)); + } + } + + model_polygons = polyunion(model_polygons); + double layer_model_area = 0; + for (const ClipperPolygon& polygon : model_polygons) + layer_model_area += areafn(polygon); + + if (layer_model_area < 0 || layer_model_area > 0) { + Lock lck(mutex); models_volume += layer_model_area * l_height; + } + + if(!supports_polygons.empty()) { + if(model_polygons.empty()) supports_polygons = polyunion(supports_polygons); + else supports_polygons = polydiff(supports_polygons, model_polygons); + // allegedly, union of subject is done withing the diff according to the pftPositive polyFillType + } + + double layer_support_area = 0; + for (const ClipperPolygon& polygon : supports_polygons) + layer_support_area += areafn(polygon); + + if (layer_support_area < 0 || layer_support_area > 0) { + Lock lck(mutex); supports_volume += layer_support_area * l_height; + } + + // Here we can save the expensively calculated polygons for printing + ClipperPolygons trslices; + trslices.reserve(model_polygons.size() + supports_polygons.size()); + for(ClipperPolygon& poly : model_polygons) trslices.emplace_back(std::move(poly)); + for(ClipperPolygon& poly : supports_polygons) trslices.emplace_back(std::move(poly)); + + layer.transformed_slices(polyunion(trslices)); + + // Calculation of the slow and fast layers to the future controlling those values on FW + + const bool is_fast_layer = (layer_model_area + layer_support_area) <= display_area*area_fill; + const double tilt_time = is_fast_layer ? fast_tilt : slow_tilt; + + { Lock lck(mutex); + if (is_fast_layer) + fast_layers++; + else + slow_layers++; + + + // Calculation of the printing time + + if (sliced_layer_cnt < 3) + estim_time += init_exp_time; + else if (fade_layer_time > exp_time) + { + fade_layer_time -= delta_fade_time; + estim_time += fade_layer_time; + } + else + estim_time += exp_time; + + estim_time += tilt_time; + } + }; + + // sequential version for debugging: + // for(size_t i = 0; i < m_printer_input.size(); ++i) printlayerfn(i); + sla::ccr::enumerate(printer_input.begin(), printer_input.end(), printlayerfn); + + auto SCALING2 = SCALING_FACTOR * SCALING_FACTOR; + print_statistics.support_used_material = supports_volume * SCALING2; + print_statistics.objects_used_material = models_volume * SCALING2; + + // Estimated printing time + // A layers count o the highest object + if (printer_input.size() == 0) + print_statistics.estimated_print_time = std::nan(""); + else + print_statistics.estimated_print_time = estim_time; + + print_statistics.fast_layers_count = fast_layers; + print_statistics.slow_layers_count = slow_layers; + + report_status(-2, "", SlicingStatus::RELOAD_SLA_PREVIEW); +} + +// Rasterizing the model objects, and their supports +void SLAPrint::Steps::rasterize() +{ + if(canceled()) return; + + auto &print_statistics = m_print->m_print_statistics; + auto &printer_input = m_print->m_printer_input; + + // Set up the printer, allocate space for all the layers + sla::RasterWriter &printer = m_print->init_printer(); + + auto lvlcnt = unsigned(printer_input.size()); + printer.layers(lvlcnt); + + // coefficient to map the rasterization state (0-99) to the allocated + // portion (slot) of the process state + double sd = (100 - max_objstatus) / 100.0; + + // slot is the portion of 100% that is realted to rasterization + unsigned slot = PRINT_STEP_LEVELS[slapsRasterize]; + + // pst: previous state + double pst = current_status(); + + double increment = (slot * sd) / printer_input.size(); + double dstatus = current_status(); + + sla::ccr::SpinningMutex slck; + using Lock = std::lock_guard; + + // procedure to process one height level. This will run in parallel + auto lvlfn = + [this, &slck, &printer, increment, &dstatus, &pst] + (PrintLayer& printlayer, size_t idx) + { + if(canceled()) return; + auto level_id = unsigned(idx); + + // Switch to the appropriate layer in the printer + printer.begin_layer(level_id); + + for(const ClipperLib::Polygon& poly : printlayer.transformed_slices()) + printer.draw_polygon(poly, level_id); + + // Finish the layer for later saving it. + printer.finish_layer(level_id); + + // Status indication guarded with the spinlock + { + Lock lck(slck); + dstatus += increment; + double st = std::round(dstatus); + if(st > pst) { + report_status(st, PRINT_STEP_LABELS(slapsRasterize)); + pst = st; + } + } + }; + + // last minute escape + if(canceled()) return; + + // Sequential version (for testing) + // for(unsigned l = 0; l < lvlcnt; ++l) lvlfn(l); + + // Print all the layers in parallel + sla::ccr::enumerate(printer_input.begin(), printer_input.end(), lvlfn); + + // Set statistics values to the printer + sla::RasterWriter::PrintStatistics stats; + stats.used_material = (print_statistics.objects_used_material + + print_statistics.support_used_material) / 1000; + + int num_fade = m_print->m_default_object_config.faded_layers.getInt(); + stats.num_fade = num_fade >= 0 ? size_t(num_fade) : size_t(0); + stats.num_fast = print_statistics.fast_layers_count; + stats.num_slow = print_statistics.slow_layers_count; + stats.estimated_print_time_s = print_statistics.estimated_print_time; + + printer.set_statistics(stats); +} + +std::string SLAPrint::Steps::label(SLAPrintObjectStep step) +{ + return OBJ_STEP_LABELS(step); +} + +std::string SLAPrint::Steps::label(SLAPrintStep step) +{ + return PRINT_STEP_LABELS(step); +} + +double SLAPrint::Steps::progressrange(SLAPrintObjectStep step) const +{ + return OBJ_STEP_LEVELS[step] * objectstep_scale; +} + +double SLAPrint::Steps::progressrange(SLAPrintStep step) const +{ + return PRINT_STEP_LEVELS[step] * (100 - max_objstatus) / 100.0; +} + +void SLAPrint::Steps::execute(SLAPrintObjectStep step, SLAPrintObject &obj) +{ + switch(step) { + case slaposHollowing: hollow_model(obj); break; + case slaposObjectSlice: slice_model(obj); break; + case slaposDrillHolesIfHollowed: break; + case slaposSupportPoints: support_points(obj); break; + case slaposSupportTree: support_tree(obj); break; + case slaposPad: generate_pad(obj); break; + case slaposSliceSupports: slice_supports(obj); break; + case slaposCount: assert(false); + } +} + +void SLAPrint::Steps::execute(SLAPrintStep step) +{ + switch (step) { + case slapsMergeSlicesAndEval: merge_slices_and_eval_stats(); break; + case slapsRasterize: rasterize(); break; + case slapsCount: assert(false); + } +} + +} diff --git a/src/libslic3r/SLAPrintSteps.hpp b/src/libslic3r/SLAPrintSteps.hpp new file mode 100644 index 0000000000..c62558671c --- /dev/null +++ b/src/libslic3r/SLAPrintSteps.hpp @@ -0,0 +1,68 @@ +#ifndef SLAPRINTSTEPS_HPP +#define SLAPRINTSTEPS_HPP + +#include + +#include +#include + +namespace Slic3r { + +class SLAPrint::Steps +{ +private: + SLAPrint *m_print = nullptr; + +public: + // where the per object operations start and end + static const constexpr unsigned min_objstatus = 0; + static const constexpr unsigned max_objstatus = 50; + +private: + const size_t objcount; + + // shortcut to initial layer height + const double ilhd; + const float ilh; + const coord_t ilhs; + + // the coefficient that multiplies the per object status values which + // are set up for <0, 100>. They need to be scaled into the whole process + const double objectstep_scale; + + template void report_status(Args&&...args) + { + m_print->m_report_status(*m_print, std::forward(args)...); + } + + double current_status() const { return m_print->m_report_status.status(); } + void throw_if_canceled() const { m_print->throw_if_canceled(); } + bool canceled() const { return m_print->canceled(); } + void initialize_printer_input(); + +public: + Steps(SLAPrint *print); + + void hollow_model(SLAPrintObject &po); + void slice_model(SLAPrintObject& po); + void support_points(SLAPrintObject& po); + void support_tree(SLAPrintObject& po); + void generate_pad(SLAPrintObject& po); + void slice_supports(SLAPrintObject& po); + + void merge_slices_and_eval_stats(); + void rasterize(); + + void execute(SLAPrintObjectStep step, SLAPrintObject &obj); + void execute(SLAPrintStep step); + + static std::string label(SLAPrintObjectStep step); + static std::string label(SLAPrintStep step); + + double progressrange(SLAPrintObjectStep step) const; + double progressrange(SLAPrintStep step) const; +}; + +} + +#endif // SLAPRINTSTEPS_HPP diff --git a/src/libslic3r/SimplifyMesh.cpp b/src/libslic3r/SimplifyMesh.cpp new file mode 100644 index 0000000000..d30ecfec57 --- /dev/null +++ b/src/libslic3r/SimplifyMesh.cpp @@ -0,0 +1,66 @@ +#include "SimplifyMesh.hpp" +#include "SimplifyMeshImpl.hpp" + +namespace SimplifyMesh { + +template<> struct vertex_traits { + using coord_type = float; + using compute_type = double; + + static inline float x(const stl_vertex &v) { return v.x(); } + static inline float& x(stl_vertex &v) { return v.x(); } + + static inline float y(const stl_vertex &v) { return v.y(); } + static inline float& y(stl_vertex &v) { return v.y(); } + + static inline float z(const stl_vertex &v) { return v.z(); } + static inline float& z(stl_vertex &v) { return v.z(); } +}; + +template<> struct mesh_traits { + using vertex_t = stl_vertex; + static size_t face_count(const indexed_triangle_set &m) + { + return m.indices.size(); + } + static size_t vertex_count(const indexed_triangle_set &m) + { + return m.vertices.size(); + } + static vertex_t vertex(const indexed_triangle_set &m, size_t idx) + { + return m.vertices[idx]; + } + static void vertex(indexed_triangle_set &m, size_t idx, const vertex_t &v) + { + m.vertices[idx] = v; + } + static Index3 triangle(const indexed_triangle_set &m, size_t idx) + { + std::array t; + for (size_t i = 0; i < 3; ++i) t[i] = size_t(m.indices[idx](int(i))); + return t; + } + static void triangle(indexed_triangle_set &m, size_t fidx, const Index3 &t) + { + auto &face = m.indices[fidx]; + face(0) = int(t[0]); face(1) = int(t[1]); face(2) = int(t[2]); + } + static void update(indexed_triangle_set &m, size_t vc, size_t fc) + { + m.vertices.resize(vc); + m.indices.resize(fc); + } +}; + +} // namespace SimplifyMesh + +namespace Slic3r { + +void simplify_mesh(indexed_triangle_set &m) +{ + SimplifyMesh::implementation::SimplifiableMesh sm{&m}; + sm.simplify_mesh_lossless(); +} + +} diff --git a/src/libslic3r/SimplifyMesh.hpp b/src/libslic3r/SimplifyMesh.hpp new file mode 100644 index 0000000000..fb3e73d049 --- /dev/null +++ b/src/libslic3r/SimplifyMesh.hpp @@ -0,0 +1,25 @@ +#ifndef MESHSIMPLIFY_HPP +#define MESHSIMPLIFY_HPP + +#include + +#include + +namespace Slic3r { + +void simplify_mesh(indexed_triangle_set &); + +// TODO: (but this can be done with IGL as well) +// void simplify_mesh(indexed_triangle_set &, int face_count, float agressiveness = 0.5f); + +template void simplify_mesh(TriangleMesh &m, Args &&...a) +{ + m.require_shared_vertices(); + simplify_mesh(m.its, std::forward(a)...); + m = TriangleMesh{m.its}; + m.require_shared_vertices(); +} + +} // namespace Slic3r + +#endif // MESHSIMPLIFY_H diff --git a/src/libslic3r/SimplifyMeshImpl.hpp b/src/libslic3r/SimplifyMeshImpl.hpp new file mode 100644 index 0000000000..6add08930c --- /dev/null +++ b/src/libslic3r/SimplifyMeshImpl.hpp @@ -0,0 +1,699 @@ +// /////////////////////////////////////////// +// +// Mesh Simplification Tutorial +// +// (C) by Sven Forstmann in 2014 +// +// License : MIT +// http://opensource.org/licenses/MIT +// +// https://github.com/sp4cerat/Fast-Quadric-Mesh-Simplification +// +// 5/2016: Chris Rorden created minimal version for OSX/Linux/Windows compile +// https://github.com/sp4cerat/Fast-Quadric-Mesh-Simplification/ +// +// libslic3r refactor by tamasmeszaros + +#ifndef SIMPLIFYMESHIMPL_HPP +#define SIMPLIFYMESHIMPL_HPP + +#include +#include +#include +#include + +#ifndef NDEBUG +#include +#endif + +namespace SimplifyMesh { + +using Bary = std::array; +using Index3 = std::array; + +template struct vertex_traits { + using coord_type = typename Vertex::coord_type; + using compute_type = coord_type; + + static coord_type x(const Vertex &v); + static coord_type& x(Vertex &v); + + static coord_type y(const Vertex &v); + static coord_type& y(Vertex &v); + + static coord_type z(const Vertex &v); + static coord_type& z(Vertex &v); +}; + +template struct mesh_traits { + using vertex_t = typename Mesh::vertex_t; + + static size_t face_count(const Mesh &m); + static size_t vertex_count(const Mesh &m); + static vertex_t vertex(const Mesh &m, size_t vertex_idx); + static void vertex(Mesh &m, size_t vertex_idx, const vertex_t &v); + static Index3 triangle(const Mesh &m, size_t face_idx); + static void triangle(Mesh &m, size_t face_idx, const Index3 &t); + static void update(Mesh &m, size_t vertex_count, size_t face_count); +}; + +namespace implementation { + +// A shorter C++14 style form of the enable_if metafunction +template +using enable_if_t = typename std::enable_if::type; + +// Meta predicates for floating, 'scaled coord' and generic arithmetic types +template +using FloatingOnly = enable_if_t::value, O>; + +template +using IntegerOnly = enable_if_t::value, O>; + +template +using ArithmeticOnly = enable_if_t::value, O>; + +template< class T > +struct remove_cvref { + using type = typename std::remove_cv< + typename std::remove_reference::type>::type; +}; + +template< class T > +using remove_cvref_t = typename remove_cvref::type; + +struct DOut { +#ifndef NDEBUG + std::ostream& out = std::cout; +#endif +}; + +template +inline DOut&& operator<<( DOut&& out, T&& d) { +#ifndef NDEBUG + out.out << d; +#endif + return std::move(out); +} + +inline DOut dout() { return DOut(); } + +template FloatingOnly is_approx(T val, T ref) { return std::abs(val - ref) < 1e-8; } +template IntegerOnly is_approx(T val, T ref) { val == ref; } + +template class SymetricMatrix { +public: + + explicit SymetricMatrix(ArithmeticOnly c = T()) { std::fill(m, m + N, c); } + + SymetricMatrix(T m11, T m12, T m13, T m14, + T m22, T m23, T m24, + T m33, T m34, + T m44) + { + m[0] = m11; m[1] = m12; m[2] = m13; m[3] = m14; + m[4] = m22; m[5] = m23; m[6] = m24; + m[7] = m33; m[8] = m34; + m[9] = m44; + } + + // Make plane + SymetricMatrix(T a, T b, T c, T d) + { + m[0] = a * a; m[1] = a * b; m[2] = a * c; m[3] = a * d; + m[4] = b * b; m[5] = b * c; m[6] = b * d; + m[7] = c * c; m[8] = c * d; + m[9] = d * d; + } + + T operator[](int c) const { return m[c]; } + + // Determinant + T det(int a11, int a12, int a13, + int a21, int a22, int a23, + int a31, int a32, int a33) + { + T det = m[a11] * m[a22] * m[a33] + m[a13] * m[a21] * m[a32] + + m[a12] * m[a23] * m[a31] - m[a13] * m[a22] * m[a31] - + m[a11] * m[a23] * m[a32] - m[a12] * m[a21] * m[a33]; + + return det; + } + + const SymetricMatrix operator+(const SymetricMatrix& n) const + { + return SymetricMatrix(m[0] + n[0], m[1] + n[1], m[2] + n[2], m[3]+n[3], + m[4] + n[4], m[5] + n[5], m[6] + n[6], + m[7] + n[7], m[8] + n[8], + m[9] + n[9]); + } + + SymetricMatrix& operator+=(const SymetricMatrix& n) + { + m[0]+=n[0]; m[1]+=n[1]; m[2]+=n[2]; m[3]+=n[3]; + m[4]+=n[4]; m[5]+=n[5]; m[6]+=n[6]; m[7]+=n[7]; + m[8]+=n[8]; m[9]+=n[9]; + + return *this; + } + + T m[N]; +}; + +template using TCoord = typename vertex_traits>::coord_type; +template using TCompute = typename vertex_traits>::compute_type; +template inline TCoord x(const V &v) { return vertex_traits>::x(v); } +template inline TCoord y(const V &v) { return vertex_traits>::y(v); } +template inline TCoord z(const V &v) { return vertex_traits>::z(v); } +template inline TCoord& x(V &v) { return vertex_traits>::x(v); } +template inline TCoord& y(V &v) { return vertex_traits>::y(v); } +template inline TCoord& z(V &v) { return vertex_traits>::z(v); } +template using TVertex = typename mesh_traits>::vertex_t; +template using TMeshCoord = TCoord>; + +template TCompute dot(const Vertex &v1, const Vertex &v2) +{ + return TCompute(x(v1)) * x(v2) + + TCompute(y(v1)) * y(v2) + + TCompute(z(v1)) * z(v2); +} + +template Vertex cross(const Vertex &a, const Vertex &b) +{ + return Vertex{y(a) * z(b) - z(a) * y(b), + z(a) * x(b) - x(a) * z(b), + x(a) * y(b) - y(a) * x(b)}; +} + +template TCompute lengthsq(const Vertex &v) +{ + return TCompute(x(v)) * x(v) + TCompute(y(v)) * y(v) + + TCompute(z(v)) * z(v); +} + +template void normalize(Vertex &v) +{ + double square = std::sqrt(lengthsq(v)); + x(v) /= square; y(v) /= square; z(v) /= square; +} + +using Bary = std::array; + +template +Bary barycentric(const Vertex &p, const Vertex &a, const Vertex &b, const Vertex &c) +{ + Vertex v0 = (b - a); + Vertex v1 = (c - a); + Vertex v2 = (p - a); + + double d00 = dot(v0, v0); + double d01 = dot(v0, v1); + double d11 = dot(v1, v1); + double d20 = dot(v2, v0); + double d21 = dot(v2, v1); + double denom = d00 * d11 - d01 * d01; + double v = (d11 * d20 - d01 * d21) / denom; + double w = (d00 * d21 - d01 * d20) / denom; + double u = 1.0 - v - w; + + return {u, v, w}; +} + +template class SimplifiableMesh { + Mesh *m_mesh; + + using Vertex = TVertex; + using Coord = TMeshCoord; + using HiPrecison = TCompute>; + using SymMat = SymetricMatrix; + + struct FaceInfo { + size_t idx; + double err[4] = {0.}; + bool deleted = false, dirty = false; + Vertex n; + explicit FaceInfo(size_t id): idx(id) {} + }; + + struct VertexInfo { + size_t idx; + size_t tstart = 0, tcount = 0; + bool border = false; + SymMat q; + explicit VertexInfo(size_t id): idx(id) {} + }; + + struct Ref { size_t face; size_t vertex; }; + + std::vector m_refs; + std::vector m_faceinfo; + std::vector m_vertexinfo; + + void compact_faces(); + void compact(); + + size_t mesh_vcount() const { return mesh_traits::vertex_count(*m_mesh); } + size_t mesh_facecount() const { return mesh_traits::face_count(*m_mesh); } + + size_t vcount() const { return m_vertexinfo.size(); } + + inline Vertex read_vertex(size_t vi) const + { + return mesh_traits::vertex(*m_mesh, vi); + } + + inline Vertex read_vertex(const VertexInfo &vinf) const + { + return read_vertex(vinf.idx); + } + + inline void write_vertex(size_t idx, const Vertex &v) const + { + mesh_traits::vertex(*m_mesh, idx, v); + } + + inline void write_vertex(const VertexInfo &vinf, const Vertex &v) const + { + write_vertex(vinf.idx, v); + } + + inline Index3 read_triangle(size_t fi) const + { + return mesh_traits::triangle(*m_mesh, fi); + } + + inline Index3 read_triangle(const FaceInfo &finf) const + { + return read_triangle(finf.idx); + } + + inline void write_triangle(size_t idx, const Index3 &t) + { + return mesh_traits::triangle(*m_mesh, idx, t); + } + + inline void write_triangle(const FaceInfo &finf, const Index3 &t) + { + return write_triangle(finf.idx, t); + } + + inline std::array triangle_vertices(const Index3 &f) const + { + std::array p; + for (size_t i = 0; i < 3; ++i) p[i] = read_vertex(f[i]); + return p; + } + + // Error between vertex and Quadric + static double vertex_error(const SymMat &q, const Vertex &v) + { + Coord _x = x(v) , _y = y(v), _z = z(v); + return q[0] * _x * _x + 2 * q[1] * _x * _y + 2 * q[2] * _x * _z + + 2 * q[3] * _x + q[4] * _y * _y + 2 * q[5] * _y * _z + + 2 * q[6] * _y + q[7] * _z * _z + 2 * q[8] * _z + q[9]; + } + + // Error for one edge + double calculate_error(size_t id_v1, size_t id_v2, Vertex &p_result); + + void calculate_error(FaceInfo &fi) + { + Vertex p; + Index3 t = read_triangle(fi); + for (size_t j = 0; j < 3; ++j) + fi.err[j] = calculate_error(t[j], t[(j + 1) % 3], p); + + fi.err[3] = std::min(fi.err[0], std::min(fi.err[1], fi.err[2])); + } + + void update_mesh(int iteration); + + // Update triangle connections and edge error after a edge is collapsed + void update_triangles(size_t i, VertexInfo &vi, std::vector &deleted, int &deleted_triangles); + + // Check if a triangle flips when this edge is removed + bool flipped(const Vertex &p, size_t i0, size_t i1, VertexInfo &v0, VertexInfo &v1, std::vector &deleted); + +public: + + explicit SimplifiableMesh(Mesh *m) : m_mesh{m} + { + static_assert( + std::is_arithmetic::value, + "Coordinate type of mesh has to be an arithmetic type!"); + + m_faceinfo.reserve(mesh_traits::face_count(*m)); + m_vertexinfo.reserve(mesh_traits::vertex_count(*m)); + for (size_t i = 0; i < mesh_facecount(); ++i) m_faceinfo.emplace_back(i); + for (size_t i = 0; i < mesh_vcount(); ++i) m_vertexinfo.emplace_back(i); + + } + + void simplify_mesh_lossless(); +}; + + +template void SimplifiableMesh::compact_faces() +{ + auto it = std::remove_if(m_faceinfo.begin(), m_faceinfo.end(), + [](const FaceInfo &inf) { return inf.deleted; }); + + m_faceinfo.erase(it, m_faceinfo.end()); +} + +template void SimplifiableMesh::compact() +{ + for (auto &vi : m_vertexinfo) vi.tcount = 0; + + compact_faces(); + + for (FaceInfo &fi : m_faceinfo) + for (size_t vidx : read_triangle(fi)) m_vertexinfo[vidx].tcount = 1; + + size_t dst = 0; + for (VertexInfo &vi : m_vertexinfo) { + if (vi.tcount) { + vi.tstart = dst; + write_vertex(dst++, read_vertex(vi)); + } + } + + size_t vertex_count = dst; + + dst = 0; + for (const FaceInfo &fi : m_faceinfo) { + Index3 t = read_triangle(fi); + for (size_t &idx : t) idx = m_vertexinfo[idx].tstart; + write_triangle(dst++, t); + } + + mesh_traits::update(*m_mesh, vertex_count, m_faceinfo.size()); +} + +template +double SimplifiableMesh::calculate_error(size_t id_v1, size_t id_v2, Vertex &p_result) +{ + // compute interpolated vertex + + SymMat q = m_vertexinfo[id_v1].q + m_vertexinfo[id_v2].q; + + bool border = m_vertexinfo[id_v1].border & m_vertexinfo[id_v2].border; + double error = 0; + HiPrecison det = q.det(0, 1, 2, 1, 4, 5, 2, 5, 7); + + if (!is_approx(det, HiPrecison(0)) && !border) + { + // q_delta is invertible + x(p_result) = Coord(-1) / det * q.det(1, 2, 3, 4, 5, 6, 5, 7, 8); // vx = A41/det(q_delta) + y(p_result) = Coord( 1) / det * q.det(0, 2, 3, 1, 5, 6, 2, 7, 8); // vy = A42/det(q_delta) + z(p_result) = Coord(-1) / det * q.det(0, 1, 3, 1, 4, 6, 2, 5, 8); // vz = A43/det(q_delta) + + error = vertex_error(q, p_result); + } else { + // det = 0 -> try to find best result + Vertex p1 = read_vertex(id_v1); + Vertex p2 = read_vertex(id_v2); + Vertex p3 = (p1 + p2) / 2; + double error1 = vertex_error(q, p1); + double error2 = vertex_error(q, p2); + double error3 = vertex_error(q, p3); + error = std::min(error1, std::min(error2, error3)); + + if (is_approx(error1, error)) p_result = p1; + if (is_approx(error2, error)) p_result = p2; + if (is_approx(error3, error)) p_result = p3; + } + + return error; +} + +template void SimplifiableMesh::update_mesh(int iteration) +{ + if (iteration > 0) compact_faces(); + + assert(mesh_vcount() == m_vertexinfo.size()); + + // + // Init Quadrics by Plane & Edge Errors + // + // required at the beginning ( iteration == 0 ) + // recomputing during the simplification is not required, + // but mostly improves the result for closed meshes + // + if (iteration == 0) { + + for (VertexInfo &vinf : m_vertexinfo) vinf.q = SymMat{}; + for (FaceInfo &finf : m_faceinfo) { + Index3 t = read_triangle(finf); + std::array p = triangle_vertices(t); + Vertex n = cross(Vertex(p[1] - p[0]), Vertex(p[2] - p[0])); + normalize(n); + finf.n = n; + + for (size_t fi : t) + m_vertexinfo[fi].q += SymMat(x(n), y(n), z(n), -dot(n, p[0])); + + calculate_error(finf); + } + } + + // Init Reference ID list + for (VertexInfo &vi : m_vertexinfo) { vi.tstart = 0; vi.tcount = 0; } + + for (FaceInfo &fi : m_faceinfo) + for (size_t vidx : read_triangle(fi)) + m_vertexinfo[vidx].tcount++; + + size_t tstart = 0; + for (VertexInfo &vi : m_vertexinfo) { + vi.tstart = tstart; + tstart += vi.tcount; + vi.tcount = 0; + } + + // Write References + m_refs.resize(m_faceinfo.size() * 3); + for (size_t i = 0; i < m_faceinfo.size(); ++i) { + const FaceInfo &fi = m_faceinfo[i]; + Index3 t = read_triangle(fi); + for (size_t j = 0; j < 3; ++j) { + VertexInfo &vi = m_vertexinfo[t[j]]; + + assert(vi.tstart + vi.tcount < m_refs.size()); + + Ref &ref = m_refs[vi.tstart + vi.tcount]; + ref.face = i; + ref.vertex = j; + vi.tcount++; + } + } + + // Identify boundary : vertices[].border=0,1 + if (iteration == 0) { + for (VertexInfo &vi: m_vertexinfo) vi.border = false; + + std::vector vcount, vids; + + for (VertexInfo &vi: m_vertexinfo) { + vcount.clear(); + vids.clear(); + + for(size_t j = 0; j < vi.tcount; ++j) { + assert(vi.tstart + j < m_refs.size()); + FaceInfo &fi = m_faceinfo[m_refs[vi.tstart + j].face]; + Index3 t = read_triangle(fi); + + for (size_t fid : t) { + size_t ofs=0; + while (ofs < vcount.size()) + { + if (vids[ofs] == fid) break; + ofs++; + } + if (ofs == vcount.size()) + { + vcount.emplace_back(1); + vids.emplace_back(fid); + } + else + vcount[ofs]++; + } + } + + for (size_t j = 0; j < vcount.size(); ++j) + if(vcount[j] == 1) m_vertexinfo[vids[j]].border = true; + } + } +} + +template +void SimplifiableMesh::update_triangles(size_t i0, + VertexInfo & vi, + std::vector &deleted, + int &deleted_triangles) +{ + Vertex p; + for (size_t k = 0; k < vi.tcount; ++k) { + assert(vi.tstart + k < m_refs.size()); + + Ref &r = m_refs[vi.tstart + k]; + FaceInfo &fi = m_faceinfo[r.face]; + + if (fi.deleted) continue; + + if (deleted[k]) { + fi.deleted = true; + deleted_triangles++; + continue; + } + + Index3 t = read_triangle(fi); + t[r.vertex] = i0; + write_triangle(fi, t); + + fi.dirty = true; + fi.err[0] = calculate_error(t[0], t[1], p); + fi.err[1] = calculate_error(t[1], t[2], p); + fi.err[2] = calculate_error(t[2], t[0], p); + fi.err[3] = std::min(fi.err[0], std::min(fi.err[1], fi.err[2])); + m_refs.emplace_back(r); + } +} + +template +bool SimplifiableMesh::flipped(const Vertex & p, + size_t /*i0*/, + size_t i1, + VertexInfo & v0, + VertexInfo & /*v1*/, + std::vector &deleted) +{ + for (size_t k = 0; k < v0.tcount; ++k) { + size_t ridx = v0.tstart + k; + assert(ridx < m_refs.size()); + + FaceInfo &fi = m_faceinfo[m_refs[ridx].face]; + if (fi.deleted) continue; + + Index3 t = read_triangle(fi); + int s = m_refs[ridx].vertex; + size_t id1 = t[(s+1) % 3]; + size_t id2 = t[(s+2) % 3]; + + if(id1 == i1 || id2 == i1) // delete ? + { + deleted[k] = true; + continue; + } + + Vertex d1 = read_vertex(id1) - p; + normalize(d1); + Vertex d2 = read_vertex(id2) - p; + normalize(d2); + + if (std::abs(dot(d1, d2)) > 0.999) return true; + + Vertex n = cross(d1, d2); + normalize(n); + + deleted[k] = false; + if (dot(n, fi.n) < 0.2) return true; + } + + return false; +} + +template +void SimplifiableMesh::simplify_mesh_lossless() +{ + // init + for (FaceInfo &fi : m_faceinfo) fi.deleted = false; + + // main iteration loop + int deleted_triangles=0; + std::vector deleted0, deleted1; + + for (int iteration = 0; iteration < 9999; iteration ++) { + // update mesh constantly + update_mesh(iteration); + + // clear dirty flag + for (FaceInfo &fi : m_faceinfo) fi.dirty = false; + + // + // All triangles with edges below the threshold will be removed + // + // The following numbers works well for most models. + // If it does not, try to adjust the 3 parameters + // + double threshold = std::numeric_limits::epsilon(); //1.0E-3 EPS; // Really? (tm) + + dout() << "lossless iteration " << iteration << "\n"; + + for (FaceInfo &fi : m_faceinfo) { + if (fi.err[3] > threshold || fi.deleted || fi.dirty) continue; + + for (size_t j = 0; j < 3; ++j) { + if (fi.err[j] > threshold) continue; + + Index3 t = read_triangle(fi); + size_t i0 = t[j]; + VertexInfo &v0 = m_vertexinfo[i0]; + + size_t i1 = t[(j + 1) % 3]; + VertexInfo &v1 = m_vertexinfo[i1]; + + // Border check + if(v0.border != v1.border) continue; + + // Compute vertex to collapse to + Vertex p; + calculate_error(i0, i1, p); + + deleted0.resize(v0.tcount); // normals temporarily + deleted1.resize(v1.tcount); // normals temporarily + + // don't remove if flipped + if (flipped(p, i0, i1, v0, v1, deleted0)) continue; + if (flipped(p, i1, i0, v1, v0, deleted1)) continue; + + // not flipped, so remove edge + write_vertex(v0, p); + v0.q = v1.q + v0.q; + size_t tstart = m_refs.size(); + + update_triangles(i0, v0, deleted0, deleted_triangles); + update_triangles(i0, v1, deleted1, deleted_triangles); + + assert(m_refs.size() >= tstart); + + size_t tcount = m_refs.size() - tstart; + + if(tcount <= v0.tcount) + { + // save ram + if (tcount) { + auto from = m_refs.begin() + tstart, to = from + tcount; + std::copy(from, to, m_refs.begin() + v0.tstart); + } + } + else + // append + v0.tstart = tstart; + + v0.tcount = tcount; + break; + } + } + + if (deleted_triangles <= 0) break; + deleted_triangles = 0; + } + + compact(); +} + +} // namespace implementation +} // namespace SimplifyMesh + +#endif // SIMPLIFYMESHIMPL_HPP diff --git a/src/libslic3r/TriangleMesh.cpp b/src/libslic3r/TriangleMesh.cpp index 5cd97522d9..4c6cd62cf2 100644 --- a/src/libslic3r/TriangleMesh.cpp +++ b/src/libslic3r/TriangleMesh.cpp @@ -70,6 +70,34 @@ TriangleMesh::TriangleMesh(const Pointf3s &points, const std::vector& f stl_get_size(&stl); } +TriangleMesh::TriangleMesh(const indexed_triangle_set &M) +{ + stl.stats.type = inmemory; + + // count facets and allocate memory + stl.stats.number_of_facets = uint32_t(M.indices.size()); + stl.stats.original_num_facets = int(stl.stats.number_of_facets); + stl_allocate(&stl); + + for (uint32_t i = 0; i < stl.stats.number_of_facets; ++ i) { + stl_facet facet; + facet.vertex[0] = M.vertices[size_t(M.indices[i](0))]; + facet.vertex[1] = M.vertices[size_t(M.indices[i](1))]; + facet.vertex[2] = M.vertices[size_t(M.indices[i](2))]; + facet.extra[0] = 0; + facet.extra[1] = 0; + + stl_normal normal; + stl_calculate_normal(normal, &facet); + stl_normalize_vector(normal); + facet.normal = normal; + + stl.facet_start[i] = facet; + } + + stl_get_size(&stl); +} + // #define SLIC3R_TRACE_REPAIR void TriangleMesh::repair(bool update_shared_vertices) diff --git a/src/libslic3r/TriangleMesh.hpp b/src/libslic3r/TriangleMesh.hpp index ef935455ef..1a22a93435 100644 --- a/src/libslic3r/TriangleMesh.hpp +++ b/src/libslic3r/TriangleMesh.hpp @@ -23,6 +23,7 @@ class TriangleMesh public: TriangleMesh() : repaired(false) {} TriangleMesh(const Pointf3s &points, const std::vector &facets); + explicit TriangleMesh(const indexed_triangle_set &M); void clear() { this->stl.clear(); this->its.clear(); this->repaired = false; } bool ReadSTLFile(const char* input_file) { return stl_open(&stl, input_file); } bool write_ascii(const char* output_file) { return stl_write_ascii(&this->stl, output_file, ""); } diff --git a/src/libslic3r/libslic3r.h b/src/libslic3r/libslic3r.h index d3e4992ce8..0454644bb9 100644 --- a/src/libslic3r/libslic3r.h +++ b/src/libslic3r/libslic3r.h @@ -230,6 +230,22 @@ static inline bool is_approx(Number value, Number test_value) return std::fabs(double(value) - double(test_value)) < double(EPSILON); } +template +std::string string_printf(const char *const fmt, Args &&...args) +{ + static const size_t INITIAL_LEN = 1024; + std::vector buffer(INITIAL_LEN, '\0'); + + int bufflen = snprintf(buffer.data(), INITIAL_LEN - 1, fmt, std::forward(args)...); + + if (bufflen >= int(INITIAL_LEN)) { + buffer.resize(size_t(bufflen) + 1); + snprintf(buffer.data(), buffer.size(), fmt, std::forward(args)...); + } + + return std::string(buffer.begin(), buffer.begin() + bufflen); +} + } // namespace Slic3r #endif diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt index 9e4fb84eb7..4e8edf7aa2 100644 --- a/src/slic3r/CMakeLists.txt +++ b/src/slic3r/CMakeLists.txt @@ -47,6 +47,8 @@ set(SLIC3R_GUI_SOURCES GUI/Gizmos/GLGizmoFlatten.hpp GUI/Gizmos/GLGizmoCut.cpp GUI/Gizmos/GLGizmoCut.hpp + GUI/Gizmos/GLGizmoHollow.cpp + GUI/Gizmos/GLGizmoHollow.hpp GUI/GLSelectionRectangle.cpp GUI/GLSelectionRectangle.hpp GUI/GLTexture.hpp @@ -140,6 +142,7 @@ set(SLIC3R_GUI_SOURCES GUI/ProgressStatusBar.cpp GUI/PrintHostDialogs.cpp GUI/PrintHostDialogs.hpp + GUI/Job.hpp GUI/Mouse3DController.cpp GUI/Mouse3DController.hpp GUI/DoubleSlider.cpp diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index d847b297e5..2e90a87ee6 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -1438,14 +1438,14 @@ int GLCanvas3D::check_volumes_outside_state() const void GLCanvas3D::toggle_sla_auxiliaries_visibility(bool visible, const ModelObject* mo, int instance_idx) { + m_render_sla_auxiliaries = visible; + for (GLVolume* vol : m_volumes.volumes) { if ((mo == nullptr || m_model->objects[vol->composite_id.object_id] == mo) && (instance_idx == -1 || vol->composite_id.instance_id == instance_idx) && vol->composite_id.volume_id < 0) vol->is_active = visible; } - - m_render_sla_auxiliaries = visible; } void GLCanvas3D::toggle_model_objects_visibility(bool visible, const ModelObject* mo, int instance_idx) @@ -6061,6 +6061,8 @@ void GLCanvas3D::_load_sla_shells() unsigned int initial_volumes_count = (unsigned int)m_volumes.volumes.size(); for (const SLAPrintObject::Instance& instance : obj->instances()) { add_volume(*obj, 0, instance, obj->transformed_mesh(), GLVolume::MODEL_COLOR[0], true); + if (! obj->hollowed_interior_mesh().empty()) + add_volume(*obj, -int(slaposHollowing), instance, obj->hollowed_interior_mesh(), GLVolume::MODEL_COLOR[0], false); // Set the extruder_id and volume_id to achieve the same color as in the 3D scene when // through the update_volumes_colors_by_extruder() call. m_volumes.volumes.back()->extruder_id = obj->model_object()->volumes.front()->extruder_id(); diff --git a/src/slic3r/GUI/GLCanvas3D.hpp b/src/slic3r/GUI/GLCanvas3D.hpp index 9ea7568e45..a7f272f975 100644 --- a/src/slic3r/GUI/GLCanvas3D.hpp +++ b/src/slic3r/GUI/GLCanvas3D.hpp @@ -499,6 +499,7 @@ public: void set_color_by(const std::string& value); const Camera& get_camera() const { return m_camera; } + const Shader& get_shader() const { return m_shader; } Camera& get_camera() { return m_camera; } BoundingBoxf3 volumes_bounding_box() const; diff --git a/src/slic3r/GUI/GUI_ObjectList.cpp b/src/slic3r/GUI/GUI_ObjectList.cpp index 31e83cc450..b4f20524ef 100644 --- a/src/slic3r/GUI/GUI_ObjectList.cpp +++ b/src/slic3r/GUI/GUI_ObjectList.cpp @@ -103,6 +103,7 @@ ObjectList::ObjectList(wxWindow* parent) : // ptSLA CATEGORY_ICON[L("Supports")] = create_scaled_bitmap(nullptr, "support"/*"sla_supports"*/); CATEGORY_ICON[L("Pad")] = create_scaled_bitmap(nullptr, "pad"); + CATEGORY_ICON[L("Hollowing")] = create_scaled_bitmap(nullptr, "hollowing"); } // create control diff --git a/src/slic3r/GUI/Gizmos/GLGizmoBase.cpp b/src/slic3r/GUI/Gizmos/GLGizmoBase.cpp index cb18bdb166..7dce249f28 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoBase.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoBase.cpp @@ -135,7 +135,7 @@ void GLGizmoBase::Grabber::render_face(float half_size) const } -GLGizmoBase::GLGizmoBase(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id) +GLGizmoBase::GLGizmoBase(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id, CommonGizmosData* common_data_ptr) : m_parent(parent) , m_group_id(-1) , m_state(Off) @@ -146,6 +146,7 @@ GLGizmoBase::GLGizmoBase(GLCanvas3D& parent, const std::string& icon_filename, u , m_dragging(false) , m_imgui(wxGetApp().imgui()) , m_first_input_window_render(true) + , m_c(common_data_ptr) { ::memcpy((void*)m_base_color, (const void*)DEFAULT_BASE_COLOR, 4 * sizeof(float)); ::memcpy((void*)m_drag_color, (const void*)DEFAULT_DRAG_COLOR, 4 * sizeof(float)); diff --git a/src/slic3r/GUI/Gizmos/GLGizmoBase.hpp b/src/slic3r/GUI/Gizmos/GLGizmoBase.hpp index da30427793..9479174fbd 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoBase.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoBase.hpp @@ -30,7 +30,7 @@ static const float CONSTRAINED_COLOR[4] = { 0.5f, 0.5f, 0.5f, 1.0f }; class ImGuiWrapper; - +class CommonGizmosData; class GLGizmoBase { @@ -99,9 +99,13 @@ protected: mutable std::vector m_grabbers; ImGuiWrapper* m_imgui; bool m_first_input_window_render; + CommonGizmosData* m_c = nullptr; public: - GLGizmoBase(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id); + GLGizmoBase(GLCanvas3D& parent, + const std::string& icon_filename, + unsigned int sprite_id, + CommonGizmosData* common_data = nullptr); virtual ~GLGizmoBase() {} bool init() { return on_init(); } @@ -179,6 +183,34 @@ protected: // were not interpolated by alpha blending or multi sampling. extern unsigned char picking_checksum_alpha_channel(unsigned char red, unsigned char green, unsigned char blue); +class MeshRaycaster; +class MeshClipper; + +class CommonGizmosData { +public: + const TriangleMesh* mesh() const { + return (! m_mesh ? nullptr : (m_cavity_mesh ? m_cavity_mesh.get() : m_mesh)); + } + + + + ModelObject* m_model_object = nullptr; + const TriangleMesh* m_mesh; + std::unique_ptr m_mesh_raycaster; + std::unique_ptr m_object_clipper; + std::unique_ptr m_supports_clipper; + + std::unique_ptr m_cavity_mesh; + std::unique_ptr m_volume_with_cavity; + + int m_active_instance = -1; + float m_active_instance_bb_radius = 0; + ObjectID m_model_object_id = 0; + int m_print_object_idx = -1; + int m_print_objects_count = -1; + int m_old_timestamp = -1; +}; + } // namespace GUI } // namespace Slic3r diff --git a/src/slic3r/GUI/Gizmos/GLGizmoHollow.cpp b/src/slic3r/GUI/Gizmos/GLGizmoHollow.cpp new file mode 100644 index 0000000000..d02fdd0776 --- /dev/null +++ b/src/slic3r/GUI/Gizmos/GLGizmoHollow.cpp @@ -0,0 +1,1140 @@ +#include "GLGizmoHollow.hpp" +#include "slic3r/GUI/GLCanvas3D.hpp" +#include "slic3r/GUI/Gizmos/GLGizmos.hpp" + +#include + +#include "slic3r/GUI/GUI_App.hpp" +#include "slic3r/GUI/GUI_ObjectSettings.hpp" +#include "slic3r/GUI/GUI_ObjectList.hpp" +#include "slic3r/GUI/MeshUtils.hpp" +#include "slic3r/GUI/Plater.hpp" +#include "slic3r/GUI/PresetBundle.hpp" +#include "libslic3r/SLAPrint.hpp" +#include "libslic3r/TriangleMesh.hpp" +#include "libslic3r/MeshBoolean.hpp" + + +namespace Slic3r { +namespace GUI { + +GLGizmoHollow::GLGizmoHollow(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id, CommonGizmosData* cd) + : GLGizmoBase(parent, icon_filename, sprite_id, cd) + , m_quadric(nullptr) +{ + m_clipping_plane.reset(new ClippingPlane(Vec3d::Zero(), 0.)); + m_quadric = ::gluNewQuadric(); + if (m_quadric != nullptr) + // using GLU_FILL does not work when the instance's transformation + // contains mirroring (normals are reverted) + ::gluQuadricDrawStyle(m_quadric, GLU_FILL); +} + +GLGizmoHollow::~GLGizmoHollow() +{ + if (m_quadric != nullptr) + ::gluDeleteQuadric(m_quadric); +} + +bool GLGizmoHollow::on_init() +{ + m_shortcut_key = WXK_CONTROL_H; + m_desc["enable"] = _(L("Hollow this object")); + m_desc["preview"] = _(L("Preview")); + m_desc["offset"] = _(L("Offset")) + ": "; + m_desc["quality"] = _(L("Quality")) + ": "; + m_desc["closing_distance"] = _(L("Closing distance")) + ": "; + m_desc["hole_diameter"] = _(L("Hole diameter")) + ": "; + m_desc["hole_depth"] = _(L("Hole depth")) + ": "; + m_desc["remove_selected"] = _(L("Remove selected holes")); + m_desc["remove_all"] = _(L("Remove all holes")); + m_desc["clipping_of_view"] = _(L("Clipping of view"))+ ": "; + m_desc["reset_direction"] = _(L("Reset direction")); + m_desc["show_supports"] = _(L("Show supports")); + + return true; +} + +void GLGizmoHollow::set_sla_support_data(ModelObject* model_object, const Selection& selection) +{ + if (! model_object || selection.is_empty()) { + m_c->m_model_object = nullptr; + return; + } + + bool something_changed = false; + + if (m_c->m_model_object != model_object + || m_c->m_model_object_id != model_object->id() + || m_c->m_active_instance != selection.get_instance_idx()) { + m_c->m_model_object = model_object; + m_c->m_print_object_idx = -1; + m_c->m_active_instance = selection.get_instance_idx(); + something_changed = true; + } + + if (model_object && something_changed && selection.is_from_single_instance()) + { + // Cache the bb - it's needed for dealing with the clipping plane quite often + // It could be done inside update_mesh but one has to account for scaling of the instance. + m_c->m_active_instance_bb_radius = m_c->m_model_object->instance_bounding_box(m_c->m_active_instance).radius(); + + if (is_mesh_update_necessary()) { + update_mesh(); + reload_cache(); + } + + if (m_state == On) { + m_parent.toggle_model_objects_visibility(false); + m_parent.toggle_model_objects_visibility(! m_c->m_cavity_mesh, m_c->m_model_object, m_c->m_active_instance); + m_parent.toggle_sla_auxiliaries_visibility(m_show_supports, m_c->m_model_object, m_c->m_active_instance); + } + else + m_parent.toggle_model_objects_visibility(true, nullptr, -1); + } +} + + + +void GLGizmoHollow::on_render() const +{ + const Selection& selection = m_parent.get_selection(); + + // If current m_c->m_model_object does not match selection, ask GLCanvas3D to turn us off + if (m_state == On + && (m_c->m_model_object != selection.get_model()->objects[selection.get_object_idx()] + || m_c->m_active_instance != selection.get_instance_idx() + || m_c->m_model_object_id != m_c->m_model_object->id())) { + m_parent.post_event(SimpleEvent(EVT_GLCANVAS_RESETGIZMOS)); + return; + } + + if (! m_c->m_mesh) + const_cast(this)->update_mesh(); + + glsafe(::glEnable(GL_BLEND)); + glsafe(::glEnable(GL_DEPTH_TEST)); + + m_z_shift = selection.get_volume(*selection.get_volume_idxs().begin())->get_sla_shift_z(); + + render_hollowed_mesh(); + + if (m_quadric != nullptr && selection.is_from_single_instance()) + render_points(selection, false); + + m_selection_rectangle.render(m_parent); + render_clipping_plane(selection); + + glsafe(::glDisable(GL_BLEND)); +} + + + +void GLGizmoHollow::render_hollowed_mesh() const +{ + if (m_c->m_volume_with_cavity) { + m_c->m_volume_with_cavity->set_sla_shift_z(m_z_shift); + m_parent.get_shader().start_using(); + + GLint current_program_id; + glsafe(::glGetIntegerv(GL_CURRENT_PROGRAM, ¤t_program_id)); + GLint color_id = (current_program_id > 0) ? ::glGetUniformLocation(current_program_id, "uniform_color") : -1; + GLint print_box_detection_id = (current_program_id > 0) ? ::glGetUniformLocation(current_program_id, "print_box.volume_detection") : -1; + GLint print_box_worldmatrix_id = (current_program_id > 0) ? ::glGetUniformLocation(current_program_id, "print_box.volume_world_matrix") : -1; + glcheck(); + m_c->m_volume_with_cavity->set_render_color(); + const Geometry::Transformation& volume_trafo = m_c->m_model_object->volumes.front()->get_transformation(); + m_c->m_volume_with_cavity->set_volume_transformation(volume_trafo); + m_c->m_volume_with_cavity->set_instance_transformation(m_c->m_model_object->instances[size_t(m_c->m_active_instance)]->get_transformation()); + m_c->m_volume_with_cavity->render(color_id, print_box_detection_id, print_box_worldmatrix_id); + m_parent.get_shader().stop_using(); + } +} + + + +void GLGizmoHollow::render_clipping_plane(const Selection& selection) const +{ + if (m_clipping_plane_distance == 0.f || m_c->mesh()->empty()) + return; + + // Get transformation of the instance + const GLVolume* vol = selection.get_volume(*selection.get_volume_idxs().begin()); + Geometry::Transformation trafo = vol->get_instance_transformation(); + trafo.set_offset(trafo.get_offset() + Vec3d(0., 0., m_z_shift)); + + // Get transformation of supports + Geometry::Transformation supports_trafo; + supports_trafo.set_offset(Vec3d(trafo.get_offset()(0), trafo.get_offset()(1), vol->get_sla_shift_z())); + supports_trafo.set_rotation(Vec3d(0., 0., trafo.get_rotation()(2))); + // I don't know why, but following seems to be correct. + supports_trafo.set_mirror(Vec3d(trafo.get_mirror()(0) * trafo.get_mirror()(1) * trafo.get_mirror()(2), + 1, + 1.)); + + // Now initialize the TMS for the object, perform the cut and save the result. + if (! m_c->m_object_clipper) { + m_c->m_object_clipper.reset(new MeshClipper); + m_c->m_object_clipper->set_mesh(*m_c->mesh()); + } + m_c->m_object_clipper->set_plane(*m_clipping_plane); + m_c->m_object_clipper->set_transformation(trafo); + + + // Next, ask the backend if supports are already calculated. If so, we are gonna cut them too. + // First we need a pointer to the respective SLAPrintObject. The index into objects vector is + // cached so we don't have todo it on each render. We only search for the po if needed: + if (m_c->m_print_object_idx < 0 || (int)m_parent.sla_print()->objects().size() != m_c->m_print_objects_count) { + m_c->m_print_objects_count = m_parent.sla_print()->objects().size(); + m_c->m_print_object_idx = -1; + for (const SLAPrintObject* po : m_parent.sla_print()->objects()) { + ++m_c->m_print_object_idx; + if (po->model_object()->id() == m_c->m_model_object->id()) + break; + } + } + if (m_c->m_print_object_idx >= 0) { + const SLAPrintObject* print_object = m_parent.sla_print()->objects()[m_c->m_print_object_idx]; + + if (print_object->is_step_done(slaposSupportTree) && !print_object->get_mesh(slaposSupportTree).empty()) { + // If the supports are already calculated, save the timestamp of the respective step + // so we can later tell they were recalculated. + size_t timestamp = print_object->step_state_with_timestamp(slaposSupportTree).timestamp; + + if (! m_c->m_supports_clipper || (int)timestamp != m_c->m_old_timestamp) { + // The timestamp has changed. + m_c->m_supports_clipper.reset(new MeshClipper); + // The mesh should already have the shared vertices calculated. + m_c->m_supports_clipper->set_mesh(print_object->support_mesh()); + m_c->m_old_timestamp = timestamp; + } + m_c->m_supports_clipper->set_plane(*m_clipping_plane); + m_c->m_supports_clipper->set_transformation(supports_trafo); + } + else + // The supports are not valid. We better dump the cached data. + m_c->m_supports_clipper.reset(); + } + + // At this point we have the triangulated cuts for both the object and supports - let's render. + if (! m_c->m_object_clipper->get_triangles().empty()) { + ::glPushMatrix(); + ::glColor3f(1.0f, 0.37f, 0.0f); + ::glBegin(GL_TRIANGLES); + for (const Vec3f& point : m_c->m_object_clipper->get_triangles()) + ::glVertex3f(point(0), point(1), point(2)); + ::glEnd(); + ::glPopMatrix(); + } + + if (m_show_supports && m_c->m_supports_clipper && ! m_c->m_supports_clipper->get_triangles().empty()) { + ::glPushMatrix(); + ::glColor3f(1.0f, 0.f, 0.37f); + ::glBegin(GL_TRIANGLES); + for (const Vec3f& point : m_c->m_supports_clipper->get_triangles()) + ::glVertex3f(point(0), point(1), point(2)); + ::glEnd(); + ::glPopMatrix(); + } +} + + +void GLGizmoHollow::on_render_for_picking() const +{ + const Selection& selection = m_parent.get_selection(); +#if ENABLE_RENDER_PICKING_PASS + m_z_shift = selection.get_volume(*selection.get_volume_idxs().begin())->get_sla_shift_z(); +#endif + + glsafe(::glEnable(GL_DEPTH_TEST)); + render_points(selection, true); + render_hollowed_mesh(); +} + +void GLGizmoHollow::render_points(const Selection& selection, bool picking) const +{ + if (!picking) + glsafe(::glEnable(GL_LIGHTING)); + + const GLVolume* vol = selection.get_volume(*selection.get_volume_idxs().begin()); + const Transform3d& instance_scaling_matrix_inverse = vol->get_instance_transformation().get_matrix(true, true, false, true).inverse(); + const Transform3d& instance_matrix = vol->get_instance_transformation().get_matrix(); + + glsafe(::glPushMatrix()); + glsafe(::glTranslated(0.0, 0.0, m_z_shift)); + glsafe(::glMultMatrixd(instance_matrix.data())); + + float render_color[4]; + size_t cache_size = m_c->m_model_object->sla_drain_holes.size(); + for (size_t i = 0; i < cache_size; ++i) + { + const sla::DrainHole& drain_hole = m_c->m_model_object->sla_drain_holes[i]; + const bool& point_selected = m_selected[i]; + + if (is_mesh_point_clipped((drain_hole.pos+HoleStickOutLength*drain_hole.normal).cast())) + continue; + + // First decide about the color of the point. + if (picking) { + std::array color = picking_color_component(i); + render_color[0] = color[0]; + render_color[1] = color[1]; + render_color[2] = color[2]; + render_color[3] = color[3]; + } + else { + render_color[3] = 1.f; + if (size_t(m_hover_id) == i) { + render_color[0] = 0.f; + render_color[1] = 1.0f; + render_color[2] = 1.0f; + } + else { // neigher hover nor picking + render_color[0] = point_selected ? 1.0f : 0.7f; + render_color[1] = point_selected ? 0.3f : 0.7f; + render_color[2] = point_selected ? 0.3f : 0.7f; + render_color[3] = 0.5f; + } + } + glsafe(::glColor4fv(render_color)); + float render_color_emissive[4] = { 0.5f * render_color[0], 0.5f * render_color[1], 0.5f * render_color[2], 1.f}; + glsafe(::glMaterialfv(GL_FRONT, GL_EMISSION, render_color_emissive)); + + // Inverse matrix of the instance scaling is applied so that the mark does not scale with the object. + glsafe(::glPushMatrix()); + glsafe(::glTranslatef(drain_hole.pos(0), drain_hole.pos(1), drain_hole.pos(2))); + glsafe(::glMultMatrixd(instance_scaling_matrix_inverse.data())); + + if (vol->is_left_handed()) + glFrontFace(GL_CW); + + // Matrices set, we can render the point mark now. + + Eigen::Quaterniond q; + q.setFromTwoVectors(Vec3d{0., 0., 1.}, instance_scaling_matrix_inverse * (-drain_hole.normal).cast()); + Eigen::AngleAxisd aa(q); + glsafe(::glRotated(aa.angle() * (180. / M_PI), aa.axis()(0), aa.axis()(1), aa.axis()(2))); + glsafe(::glPushMatrix()); + glsafe(::glTranslated(0., 0., -drain_hole.height)); + ::gluCylinder(m_quadric, drain_hole.radius, drain_hole.radius, drain_hole.height, 24, 1); + glsafe(::glTranslated(0., 0., drain_hole.height)); + ::gluDisk(m_quadric, 0.0, drain_hole.radius, 24, 1); + glsafe(::glTranslated(0., 0., -drain_hole.height)); + glsafe(::glRotatef(180.f, 1.f, 0.f, 0.f)); + ::gluDisk(m_quadric, 0.0, drain_hole.radius, 24, 1); + glsafe(::glPopMatrix()); + + if (vol->is_left_handed()) + glFrontFace(GL_CCW); + glsafe(::glPopMatrix()); + } + + { + // Reset emissive component to zero (the default value) + float render_color_emissive[4] = { 0.f, 0.f, 0.f, 1.f }; + glsafe(::glMaterialfv(GL_FRONT, GL_EMISSION, render_color_emissive)); + } + + if (!picking) + glsafe(::glDisable(GL_LIGHTING)); + + glsafe(::glPopMatrix()); +} + + + +bool GLGizmoHollow::is_mesh_point_clipped(const Vec3d& point) const +{ + if (m_clipping_plane_distance == 0.f) + return false; + + Vec3d transformed_point = m_c->m_model_object->instances[m_c->m_active_instance]->get_transformation().get_matrix() * point; + transformed_point(2) += m_z_shift; + return m_clipping_plane->is_point_clipped(transformed_point); +} + + + +bool GLGizmoHollow::is_mesh_update_necessary() const +{ + return ((m_state == On) && (m_c->m_model_object != nullptr) && !m_c->m_model_object->instances.empty()) + && ((m_c->m_model_object->id() != m_c->m_model_object_id) || ! m_c->m_mesh); +} + + + +void GLGizmoHollow::update_mesh() +{ + if (! m_c->m_model_object) + return; + + wxBusyCursor wait; + // this way we can use that mesh directly. + // This mesh does not account for the possible Z up SLA offset. + m_c->m_mesh = &m_c->m_model_object->volumes.front()->mesh(); + + // If this is different mesh than last time + if (m_c->m_model_object_id != m_c->m_model_object->id()) { + m_c->m_cavity_mesh.reset(); // dump the cavity + m_c->m_volume_with_cavity.reset(); + m_parent.toggle_model_objects_visibility(true, m_c->m_model_object, m_c->m_active_instance); + m_c->m_mesh_raycaster.reset(); + } + + if (! m_c->m_mesh_raycaster) + m_c->m_mesh_raycaster.reset(new MeshRaycaster(*m_c->mesh())); + + m_c->m_model_object_id = m_c->m_model_object->id(); +} + + + +// Unprojects the mouse position on the mesh and saves hit point and normal of the facet into pos_and_normal +// Return false if no intersection was found, true otherwise. +bool GLGizmoHollow::unproject_on_mesh(const Vec2d& mouse_pos, std::pair& pos_and_normal) +{ + // if the gizmo doesn't have the V, F structures for igl, calculate them first: + if (! m_c->m_mesh_raycaster) + update_mesh(); + + const Camera& camera = m_parent.get_camera(); + const Selection& selection = m_parent.get_selection(); + const GLVolume* volume = selection.get_volume(*selection.get_volume_idxs().begin()); + Geometry::Transformation trafo = volume->get_instance_transformation(); + trafo.set_offset(trafo.get_offset() + Vec3d(0., 0., m_z_shift)); + + // The raycaster query + Vec3f hit; + Vec3f normal; + if (m_c->m_mesh_raycaster->unproject_on_mesh(mouse_pos, trafo.get_matrix(), camera, hit, normal, m_clipping_plane.get())) { + // Return both the point and the facet normal. + pos_and_normal = std::make_pair(hit, normal); + return true; + } + else + return false; +} + +// Following function is called from GLCanvas3D to inform the gizmo about a mouse/keyboard event. +// The gizmo has an opportunity to react - if it does, it should return true so that the Canvas3D is +// aware that the event was reacted to and stops trying to make different sense of it. If the gizmo +// concludes that the event was not intended for it, it should return false. +bool GLGizmoHollow::gizmo_event(SLAGizmoEventType action, const Vec2d& mouse_position, bool shift_down, bool alt_down, bool control_down) +{ + // left down with shift - show the selection rectangle: + if (action == SLAGizmoEventType::LeftDown && (shift_down || alt_down || control_down)) { + if (m_hover_id == -1) { + if (shift_down || alt_down) { + m_selection_rectangle.start_dragging(mouse_position, shift_down ? GLSelectionRectangle::Select : GLSelectionRectangle::Deselect); + } + } + else { + if (m_selected[m_hover_id]) + unselect_point(m_hover_id); + else { + if (!alt_down) + select_point(m_hover_id); + } + } + + return true; + } + + // left down without selection rectangle - place point on the mesh: + if (action == SLAGizmoEventType::LeftDown && !m_selection_rectangle.is_dragging() && !shift_down) { + // If any point is in hover state, this should initiate its move - return control back to GLCanvas: + if (m_hover_id != -1) + return false; + + // If there is some selection, don't add new point and deselect everything instead. + if (m_selection_empty) { + std::pair pos_and_normal; + if (unproject_on_mesh(mouse_position, pos_and_normal)) { // we got an intersection + Plater::TakeSnapshot snapshot(wxGetApp().plater(), _(L("Add drainage hole"))); + + Vec3d scaling = m_c->m_model_object->instances[m_c->m_active_instance]->get_scaling_factor(); + Vec3f normal_transformed(pos_and_normal.second(0)/scaling(0), + pos_and_normal.second(1)/scaling(1), + pos_and_normal.second(2)/scaling(2)); + + m_c->m_model_object->sla_drain_holes.emplace_back(pos_and_normal.first + HoleStickOutLength * pos_and_normal.second/* normal_transformed.normalized()*/, + -pos_and_normal.second, m_new_hole_radius, m_new_hole_height+HoleStickOutLength); + m_selected.push_back(false); + assert(m_selected.size() == m_c->m_model_object->sla_drain_holes.size()); + m_parent.set_as_dirty(); + m_wait_for_up_event = true; + } + else + return false; + } + else + select_point(NoPoints); + + return true; + } + + // left up with selection rectangle - select points inside the rectangle: + if ((action == SLAGizmoEventType::LeftUp || action == SLAGizmoEventType::ShiftUp || action == SLAGizmoEventType::AltUp) && m_selection_rectangle.is_dragging()) { + // Is this a selection or deselection rectangle? + GLSelectionRectangle::EState rectangle_status = m_selection_rectangle.get_state(); + + // First collect positions of all the points in world coordinates. + Geometry::Transformation trafo = m_c->m_model_object->instances[m_c->m_active_instance]->get_transformation(); + trafo.set_offset(trafo.get_offset() + Vec3d(0., 0., m_z_shift)); + std::vector points; + for (unsigned int i=0; im_model_object->sla_drain_holes.size(); ++i) + points.push_back(trafo.get_matrix() * m_c->m_model_object->sla_drain_holes[i].pos.cast()); + + // Now ask the rectangle which of the points are inside. + std::vector points_inside; + std::vector points_idxs = m_selection_rectangle.stop_dragging(m_parent, points); + for (size_t idx : points_idxs) + points_inside.push_back(points[idx].cast()); + + // Only select/deselect points that are actually visible + for (size_t idx : m_c->m_mesh_raycaster->get_unobscured_idxs(trafo, m_parent.get_camera(), points_inside, m_clipping_plane.get())) + { + if (rectangle_status == GLSelectionRectangle::Deselect) + unselect_point(points_idxs[idx]); + else + select_point(points_idxs[idx]); + } + return true; + } + + // left up with no selection rectangle + if (action == SLAGizmoEventType::LeftUp) { + if (m_wait_for_up_event) { + m_wait_for_up_event = false; + return true; + } + } + + // dragging the selection rectangle: + if (action == SLAGizmoEventType::Dragging) { + if (m_wait_for_up_event) + return true; // point has been placed and the button not released yet + // this prevents GLCanvas from starting scene rotation + + if (m_selection_rectangle.is_dragging()) { + m_selection_rectangle.dragging(mouse_position); + return true; + } + + return false; + } + + if (action == SLAGizmoEventType::Delete) { + // delete key pressed + delete_selected_points(); + return true; + } + + if (action == SLAGizmoEventType::RightDown) { + if (m_hover_id != -1) { + select_point(NoPoints); + select_point(m_hover_id); + delete_selected_points(); + return true; + } + return false; + } + + if (action == SLAGizmoEventType::SelectAll) { + select_point(AllPoints); + return true; + } + + if (action == SLAGizmoEventType::MouseWheelUp && control_down) { + m_clipping_plane_distance = std::min(1.f, m_clipping_plane_distance + 0.01f); + update_clipping_plane(true); + return true; + } + + if (action == SLAGizmoEventType::MouseWheelDown && control_down) { + m_clipping_plane_distance = std::max(0.f, m_clipping_plane_distance - 0.01f); + update_clipping_plane(true); + return true; + } + + if (action == SLAGizmoEventType::ResetClippingPlane) { + update_clipping_plane(); + return true; + } + + return false; +} + +void GLGizmoHollow::delete_selected_points() +{ + Plater::TakeSnapshot snapshot(wxGetApp().plater(), _(L("Delete drainage hole"))); + + for (unsigned int idx=0; idxm_model_object->sla_drain_holes.size(); ++idx) { + if (m_selected[idx]) { + m_selected.erase(m_selected.begin()+idx); + m_c->m_model_object->sla_drain_holes.erase(m_c->m_model_object->sla_drain_holes.begin() + (idx--)); + } + } + + select_point(NoPoints); +} + +void GLGizmoHollow::on_update(const UpdateData& data) +{ + if (m_hover_id != -1) { + std::pair pos_and_normal; + if (! unproject_on_mesh(data.mouse_pos.cast(), pos_and_normal)) + return; + m_c->m_model_object->sla_drain_holes[m_hover_id].pos = pos_and_normal.first + HoleStickOutLength * pos_and_normal.second; + m_c->m_model_object->sla_drain_holes[m_hover_id].normal = -pos_and_normal.second; + } +} + +std::pair GLGizmoHollow::get_hollowing_parameters() const +{ + // FIXME this function is probably obsolete, caller could + // get the data from model config himself + auto opts = get_config_options({"hollowing_min_thickness", "hollowing_quality", "hollowing_closing_distance"}); + double offset = static_cast(opts[0].first)->value; + double quality = static_cast(opts[1].first)->value; + double closing_d = static_cast(opts[2].first)->value; + return std::make_pair(m_c->m_mesh, sla::HollowingConfig{offset, quality, closing_d}); +} + +void GLGizmoHollow::update_mesh_raycaster(std::unique_ptr &&rc) +{ + m_c->m_mesh_raycaster = std::move(rc); + m_c->m_object_clipper.reset(); + m_c->m_volume_with_cavity.reset(); +} + +void GLGizmoHollow::hollow_mesh() +{ + // Trigger a UI job to hollow the mesh. + wxGetApp().plater()->hollow(); +} + + +void GLGizmoHollow::update_hollowed_mesh(std::unique_ptr &&mesh) +{ + // Called from Plater when the UI job finishes + m_c->m_cavity_mesh = std::move(mesh); + + if(m_c->m_cavity_mesh) { + // First subtract the holes: + if (! m_c->m_model_object->sla_drain_holes.empty()) { + TriangleMesh holes_mesh; + for (const sla::DrainHole& hole : m_c->m_model_object->sla_drain_holes) { + TriangleMesh hole_mesh = make_cylinder(hole.radius, hole.height, 2*M_PI/32); + + Vec3d scaling = m_c->m_model_object->instances[m_c->m_active_instance]->get_scaling_factor(); + Vec3d normal_transformed = Vec3d(hole.normal(0)/scaling(0), hole.normal(1)/scaling(1), hole.normal(2)/scaling(2)); + normal_transformed.normalize(); + + // Rotate the cylinder appropriately + Eigen::Quaterniond q; + Transform3d m = Transform3d::Identity(); + m.matrix().block(0, 0, 3, 3) = q.setFromTwoVectors(Vec3d::UnitZ(), normal_transformed).toRotationMatrix(); + hole_mesh.transform(m); + + // If the instance is scaled, undo the scaling of the hole + hole_mesh.scale(Vec3d(1/scaling(0), 1/scaling(1), 1/scaling(2))); + + // Translate the hole into position and merge with the others + hole_mesh.translate(hole.pos); + holes_mesh.merge(hole_mesh); + holes_mesh.repair(); + } + MeshBoolean::minus(*m_c->m_cavity_mesh.get(), holes_mesh); + } + + // create a new GLVolume that only has the cavity inside + m_c->m_volume_with_cavity.reset(new GLVolume(GLVolume::MODEL_COLOR[2])); + m_c->m_volume_with_cavity->indexed_vertex_array.load_mesh(*m_c->m_cavity_mesh.get()); + m_c->m_volume_with_cavity->finalize_geometry(true); + m_c->m_volume_with_cavity->force_transparent = false; + + m_parent.toggle_model_objects_visibility(false, m_c->m_model_object, m_c->m_active_instance); + m_parent.toggle_sla_auxiliaries_visibility(true, m_c->m_model_object, m_c->m_active_instance); + + // Reset raycaster so it works with the new mesh: + m_c->m_mesh_raycaster.reset(new MeshRaycaster(*m_c->mesh())); + } + + if (m_clipping_plane_distance == 0.f) { + m_clipping_plane_distance = 0.5f; + update_clipping_plane(); + } +} + +std::vector> GLGizmoHollow::get_config_options(const std::vector& keys) const +{ + std::vector> out; + + if (!m_c->m_model_object) + return out; + + const DynamicPrintConfig& object_cfg = m_c->m_model_object->config; + const DynamicPrintConfig& print_cfg = wxGetApp().preset_bundle->sla_prints.get_edited_preset().config; + std::unique_ptr default_cfg = nullptr; + + for (const std::string& key : keys) { + if (object_cfg.has(key)) + out.emplace_back(object_cfg.option(key), &object_cfg.def()->options.at(key)); // at() needed for const map + else + if (print_cfg.has(key)) + out.emplace_back(print_cfg.option(key), &print_cfg.def()->options.at(key)); + else { // we must get it from defaults + if (default_cfg == nullptr) + default_cfg.reset(DynamicPrintConfig::new_from_defaults_keys(keys)); + out.emplace_back(default_cfg->option(key), &default_cfg->def()->options.at(key)); + } + } + + return out; +} + + +ClippingPlane GLGizmoHollow::get_sla_clipping_plane() const +{ + if (!m_c->m_model_object || m_state == Off || m_clipping_plane_distance == 0.f) + return ClippingPlane::ClipsNothing(); + else + return ClippingPlane(-m_clipping_plane->get_normal(), m_clipping_plane->get_data()[3]); +} + + +void GLGizmoHollow::on_render_input_window(float x, float y, float bottom_limit) +{ + if (! m_c->m_model_object) + return; + + bool first_run = true; // This is a hack to redraw the button when all points are removed, + // so it is not delayed until the background process finishes. +RENDER_AGAIN: + const float approx_height = m_imgui->scaled(20.0f); + y = std::min(y, bottom_limit - approx_height); + m_imgui->set_next_window_pos(x, y, ImGuiCond_Always); + + m_imgui->begin(on_get_name(), ImGuiWindowFlags_NoMove | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoCollapse); + + // First calculate width of all the texts that are could possibly be shown. We will decide set the dialog width based on that: + const float settings_sliders_left = + std::max(std::max(m_imgui->calc_text_size(m_desc.at("offset")).x, + m_imgui->calc_text_size(m_desc.at("quality")).x), + m_imgui->calc_text_size(m_desc.at("closing_distance")).x) + + m_imgui->scaled(1.f); + + const float clipping_slider_left = std::max(m_imgui->calc_text_size(m_desc.at("clipping_of_view")).x, m_imgui->calc_text_size(m_desc.at("reset_direction")).x) + m_imgui->scaled(1.5f); + const float diameter_slider_left = m_imgui->calc_text_size(m_desc.at("hole_diameter")).x + m_imgui->scaled(1.f); + const float minimal_slider_width = m_imgui->scaled(4.f); + //const float buttons_width_approx = m_imgui->calc_text_size(m_desc.at("apply_changes")).x + m_imgui->calc_text_size(m_desc.at("discard_changes")).x + m_imgui->scaled(1.5f); + + float window_width = minimal_slider_width + std::max(std::max(settings_sliders_left, clipping_slider_left), diameter_slider_left); + window_width = std::max(std::max(window_width, /*buttons_width_approx*/0.f), 0.f); + + { + auto opts = get_config_options({"hollowing_enable"}); + m_enable_hollowing = static_cast(opts[0].first)->value; + if (m_imgui->checkbox(m_desc["enable"], m_enable_hollowing)) { + m_c->m_model_object->config.opt("hollowing_enable", true)->value = m_enable_hollowing; + wxGetApp().obj_list()->update_and_show_object_settings_item(); + } + } + m_imgui->disabled_begin(! m_enable_hollowing); + + ImGui::SameLine(); + if (m_imgui->button(m_desc["preview"])) + hollow_mesh(); + + std::vector opts_keys = {"hollowing_min_thickness", "hollowing_quality", "hollowing_closing_distance"}; + auto opts = get_config_options(opts_keys); + auto* offset_cfg = static_cast(opts[0].first); + float offset = offset_cfg->value; + double offset_min = opts[0].second->min; + double offset_max = opts[0].second->max; + + auto* quality_cfg = static_cast(opts[1].first); + float quality = quality_cfg->value; + double quality_min = opts[1].second->min; + double quality_max = opts[1].second->max; + + auto* closing_d_cfg = static_cast(opts[2].first); + float closing_d = closing_d_cfg->value; + double closing_d_min = opts[2].second->min; + double closing_d_max = opts[2].second->max; + + + m_imgui->text(m_desc.at("offset")); + ImGui::SameLine(settings_sliders_left); + ImGui::PushItemWidth(window_width - settings_sliders_left); + ImGui::SliderFloat(" ", &offset, offset_min, offset_max, "%.1f"); + if (ImGui::IsItemHovered()) { + ImGui::BeginTooltip(); + ImGui::TextUnformatted(_(opts[0].second->tooltip).ToUTF8()); + ImGui::EndTooltip(); + } + bool slider_clicked = ImGui::IsItemClicked(); // someone clicked the slider + bool slider_edited = ImGui::IsItemEdited(); // someone is dragging the slider + bool slider_released = ImGui::IsItemDeactivatedAfterEdit(); // someone has just released the slider + + m_imgui->text(m_desc.at("quality")); + ImGui::SameLine(settings_sliders_left); + ImGui::SliderFloat(" ", &quality, quality_min, quality_max, "%.1f"); + if (ImGui::IsItemHovered()) { + ImGui::BeginTooltip(); + ImGui::TextUnformatted(_(opts[1].second->tooltip).ToUTF8()); + ImGui::EndTooltip(); + } + slider_clicked |= ImGui::IsItemClicked(); + slider_edited |= ImGui::IsItemEdited(); + slider_released |= ImGui::IsItemDeactivatedAfterEdit(); + + m_imgui->text(m_desc.at("closing_distance")); + ImGui::SameLine(settings_sliders_left); + ImGui::SliderFloat(" ", &closing_d, closing_d_min, closing_d_max, "%.1f"); + if (ImGui::IsItemHovered()) { + ImGui::BeginTooltip(); + ImGui::TextUnformatted(_(opts[2].second->tooltip).ToUTF8()); + ImGui::EndTooltip(); + } + slider_clicked |= ImGui::IsItemClicked(); + slider_edited |= ImGui::IsItemEdited(); + slider_released |= ImGui::IsItemDeactivatedAfterEdit(); + + if (slider_clicked) { + m_offset_stash = offset; + m_quality_stash = quality; + m_closing_d_stash = closing_d; + } + if (slider_edited || slider_released) { + if (slider_released) { + m_c->m_model_object->config.opt("hollowing_min_thickness", true)->value = m_offset_stash; + m_c->m_model_object->config.opt("hollowing_quality", true)->value = m_quality_stash; + m_c->m_model_object->config.opt("hollowing_closing_distance", true)->value = m_closing_d_stash; + Plater::TakeSnapshot snapshot(wxGetApp().plater(), _(L("Hollowing parameter change"))); + } + m_c->m_model_object->config.opt("hollowing_min_thickness", true)->value = offset; + m_c->m_model_object->config.opt("hollowing_quality", true)->value = quality; + m_c->m_model_object->config.opt("hollowing_closing_distance", true)->value = closing_d; + if (slider_released) + wxGetApp().obj_list()->update_and_show_object_settings_item(); + } + + m_imgui->disabled_end(); + + bool force_refresh = false; + bool remove_selected = false; + bool remove_all = false; + + // m_imgui->text(" "); // vertical gap + ImGui::Separator(); + + float diameter_upper_cap = 5.f; + if (m_new_hole_radius > diameter_upper_cap) + m_new_hole_radius = diameter_upper_cap; + m_imgui->text(m_desc.at("hole_diameter")); + ImGui::SameLine(diameter_slider_left); + ImGui::PushItemWidth(window_width - diameter_slider_left); + + float diam = 2.f * m_new_hole_radius; + ImGui::SliderFloat("", &diam, 1.f, diameter_upper_cap, "%.1f"); + m_new_hole_radius = diam / 2.f; + bool clicked = ImGui::IsItemClicked(); + bool edited = ImGui::IsItemEdited(); + bool deactivated = ImGui::IsItemDeactivatedAfterEdit(); + + m_imgui->text(m_desc["hole_depth"]); + ImGui::SameLine(diameter_slider_left); + m_new_hole_height -= HoleStickOutLength; + ImGui::SliderFloat(" ", &m_new_hole_height, 0.f, 10.f, "%.1f"); + m_new_hole_height += HoleStickOutLength; + + clicked |= ImGui::IsItemClicked(); + edited |= ImGui::IsItemEdited(); + deactivated |= ImGui::IsItemDeactivatedAfterEdit(); + + // Following is a nasty way to: + // - save the initial value of the slider before one starts messing with it + // - keep updating the head radius during sliding so it is continuosly refreshed in 3D scene + // - take correct undo/redo snapshot after the user is done with moving the slider + if (! m_selection_empty) { + if (clicked) { + m_holes_stash = m_c->m_model_object->sla_drain_holes; + } + if (edited) { + for (size_t idx=0; idxm_model_object->sla_drain_holes[idx].radius = m_new_hole_radius; + m_c->m_model_object->sla_drain_holes[idx].height = m_new_hole_height; + } + } + if (deactivated) { + // momentarily restore the old value to take snapshot + sla::DrainHoles new_holes = m_c->m_model_object->sla_drain_holes; + m_c->m_model_object->sla_drain_holes = m_holes_stash; + float backup_rad = m_new_hole_radius; + float backup_hei = m_new_hole_height; + for (size_t i=0; im_model_object->sla_drain_holes = new_holes; + } + } + + m_imgui->disabled_begin(m_selection_empty); + remove_selected = m_imgui->button(m_desc.at("remove_selected")); + m_imgui->disabled_end(); + + m_imgui->disabled_begin(m_c->m_model_object->sla_drain_holes.empty()); + remove_all = m_imgui->button(m_desc.at("remove_all")); + m_imgui->disabled_end(); + + // Following is rendered in both editing and non-editing mode: + // m_imgui->text(""); + ImGui::Separator(); + if (m_clipping_plane_distance == 0.f) + m_imgui->text(m_desc.at("clipping_of_view")); + else { + if (m_imgui->button(m_desc.at("reset_direction"))) { + wxGetApp().CallAfter([this](){ + update_clipping_plane(); + }); + } + } + + ImGui::SameLine(clipping_slider_left); + ImGui::PushItemWidth(window_width - clipping_slider_left); + if (ImGui::SliderFloat(" ", &m_clipping_plane_distance, 0.f, 1.f, "%.2f")) + update_clipping_plane(true); + + // make sure supports are shown/hidden as appropriate + if (m_imgui->checkbox(m_desc["show_supports"], m_show_supports)) { + m_parent.toggle_sla_auxiliaries_visibility(m_show_supports, m_c->m_model_object, m_c->m_active_instance); + force_refresh = true; + } + + m_imgui->end(); + + + if (remove_selected || remove_all) { + force_refresh = false; + m_parent.set_as_dirty(); + + if (remove_all) { + select_point(AllPoints); + delete_selected_points(); + } + if (remove_selected) + delete_selected_points(); + + if (first_run) { + first_run = false; + goto RENDER_AGAIN; + } + } + + if (force_refresh) + m_parent.set_as_dirty(); +} + +bool GLGizmoHollow::on_is_activable() const +{ + const Selection& selection = m_parent.get_selection(); + + if (wxGetApp().preset_bundle->printers.get_edited_preset().printer_technology() != ptSLA + || !selection.is_from_single_instance()) + return false; + + // Check that none of the selected volumes is outside. Only SLA auxiliaries (supports) are allowed outside. + const Selection::IndicesList& list = selection.get_volume_idxs(); + for (const auto& idx : list) + if (selection.get_volume(idx)->is_outside && selection.get_volume(idx)->composite_id.volume_id >= 0) + return false; + + return true; +} + +bool GLGizmoHollow::on_is_selectable() const +{ + return (wxGetApp().preset_bundle->printers.get_edited_preset().printer_technology() == ptSLA); +} + +std::string GLGizmoHollow::on_get_name() const +{ + return (_(L("Hollowing")) + " [H]").ToUTF8().data(); +} + + + +void GLGizmoHollow::on_set_state() +{ + // m_c->m_model_object pointer can be invalid (for instance because of undo/redo action), + // we should recover it from the object id + m_c->m_model_object = nullptr; + for (const auto mo : wxGetApp().model().objects) { + if (mo->id() == m_c->m_model_object_id) { + m_c->m_model_object = mo; + break; + } + } + + if (m_state == m_old_state) + return; + + if (m_state == On && m_old_state != On) { // the gizmo was just turned on + //Plater::TakeSnapshot snapshot(wxGetApp().plater(), _(L("SLA gizmo turned on"))); + if (is_mesh_update_necessary()) + update_mesh(); + + // we'll now reload support points: + if (m_c->m_model_object) + reload_cache(); + + m_parent.toggle_model_objects_visibility(false); + if (m_c->m_model_object) + m_parent.toggle_model_objects_visibility(! m_c->m_cavity_mesh, m_c->m_model_object, m_c->m_active_instance); + m_parent.toggle_sla_auxiliaries_visibility(m_show_supports, m_c->m_model_object, m_c->m_active_instance); + + // Set default head diameter from config. + //const DynamicPrintConfig& cfg = wxGetApp().preset_bundle->sla_prints.get_edited_preset().config; + //m_new_hole_radius = static_cast(cfg.option("support_head_front_diameter"))->value; + } + if (m_state == Off && m_old_state != Off) { // the gizmo was just turned Off + //Plater::TakeSnapshot snapshot(wxGetApp().plater(), _(L("SLA gizmo turned off"))); + m_parent.toggle_model_objects_visibility(true); + m_clipping_plane_distance = 0.f; + update_clipping_plane(); + // Release clippers and the AABB raycaster. + m_c->m_object_clipper.reset(); + m_c->m_supports_clipper.reset(); + //m_c->m_mesh_raycaster.reset(); + //m_c->m_cavity_mesh.reset(); + //m_c->m_volume_with_cavity.reset(); + } + m_old_state = m_state; +} + + + +void GLGizmoHollow::on_start_dragging() +{ + if (m_hover_id != -1) { + select_point(NoPoints); + select_point(m_hover_id); + m_hole_before_drag = m_c->m_model_object->sla_drain_holes[m_hover_id].pos; + } + else + m_hole_before_drag = Vec3f::Zero(); +} + + +void GLGizmoHollow::on_stop_dragging() +{ + if (m_hover_id != -1) { + Vec3f backup = m_c->m_model_object->sla_drain_holes[m_hover_id].pos; + + if (m_hole_before_drag != Vec3f::Zero() // some point was touched + && backup != m_hole_before_drag) // and it was moved, not just selected + { + m_c->m_model_object->sla_drain_holes[m_hover_id].pos = m_hole_before_drag; + Plater::TakeSnapshot snapshot(wxGetApp().plater(), _(L("Move drainage hole"))); + m_c->m_model_object->sla_drain_holes[m_hover_id].pos = backup; + } + } + m_hole_before_drag = Vec3f::Zero(); +} + + + +void GLGizmoHollow::on_load(cereal::BinaryInputArchive& ar) +{ + ar(m_clipping_plane_distance, + *m_clipping_plane, + m_c->m_model_object_id, + m_new_hole_radius, + m_new_hole_height, + m_selected, + m_selection_empty + ); +} + + + +void GLGizmoHollow::on_save(cereal::BinaryOutputArchive& ar) const +{ + ar(m_clipping_plane_distance, + *m_clipping_plane, + m_c->m_model_object_id, + m_new_hole_radius, + m_new_hole_height, + m_selected, + m_selection_empty + ); +} + + + +void GLGizmoHollow::select_point(int i) +{ + if (i == AllPoints || i == NoPoints) { + m_selected.assign(m_selected.size(), i == AllPoints); + m_selection_empty = (i == NoPoints); + + if (i == AllPoints) { + m_new_hole_radius = m_c->m_model_object->sla_drain_holes[0].radius; + m_new_hole_height = m_c->m_model_object->sla_drain_holes[0].height - HoleStickOutLength; + } + } + else { + while (size_t(i) >= m_selected.size()) + m_selected.push_back(false); + m_selected[i] = true; + m_selection_empty = false; + m_new_hole_radius = m_c->m_model_object->sla_drain_holes[i].radius; + m_new_hole_height = m_c->m_model_object->sla_drain_holes[i].height - HoleStickOutLength; + } +} + + +void GLGizmoHollow::unselect_point(int i) +{ + m_selected[i] = false; + m_selection_empty = true; + for (const bool sel : m_selected) { + if (sel) { + m_selection_empty = false; + break; + } + } +} + +void GLGizmoHollow::reload_cache() +{ + m_selected.clear(); + m_selected.assign(m_c->m_model_object->sla_drain_holes.size(), false); +} + +void GLGizmoHollow::update_clipping_plane(bool keep_normal) const +{ + Vec3d normal = (keep_normal && m_clipping_plane->get_normal() != Vec3d::Zero() ? + m_clipping_plane->get_normal() : -m_parent.get_camera().get_dir_forward()); + + const Vec3d& center = m_c->m_model_object->instances[m_c->m_active_instance]->get_offset() + Vec3d(0., 0., m_z_shift); + float dist = normal.dot(center); + *m_clipping_plane = ClippingPlane(normal, (dist - (-m_c->m_active_instance_bb_radius) - m_clipping_plane_distance * 2*m_c->m_active_instance_bb_radius)); + m_parent.set_as_dirty(); +} + + + + +} // namespace GUI +} // namespace Slic3r diff --git a/src/slic3r/GUI/Gizmos/GLGizmoHollow.hpp b/src/slic3r/GUI/Gizmos/GLGizmoHollow.hpp new file mode 100644 index 0000000000..f6560c861e --- /dev/null +++ b/src/slic3r/GUI/Gizmos/GLGizmoHollow.hpp @@ -0,0 +1,131 @@ +#ifndef slic3r_GLGizmoHollow_hpp_ +#define slic3r_GLGizmoHollow_hpp_ + +#include "GLGizmoBase.hpp" +#include "slic3r/GUI/GLSelectionRectangle.hpp" + +#include +#include + +#include + + +namespace Slic3r { +namespace GUI { + +class ClippingPlane; +class MeshClipper; +class MeshRaycaster; +enum class SLAGizmoEventType : unsigned char; + +class GLGizmoHollow : public GLGizmoBase +{ +private: + mutable double m_z_shift = 0.; + bool unproject_on_mesh(const Vec2d& mouse_pos, std::pair& pos_and_normal); + + const float HoleStickOutLength = 1.f; + + GLUquadricObj* m_quadric; + + +public: + GLGizmoHollow(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id, CommonGizmosData* cd); + ~GLGizmoHollow() override; + void set_sla_support_data(ModelObject* model_object, const Selection& selection); + bool gizmo_event(SLAGizmoEventType action, const Vec2d& mouse_position, bool shift_down, bool alt_down, bool control_down); + void delete_selected_points(); + ClippingPlane get_sla_clipping_plane() const; + + + std::pair get_hollowing_parameters() const; + void update_mesh_raycaster(std::unique_ptr &&rc); + void update_hollowed_mesh(std::unique_ptr &&mesh); + + bool is_selection_rectangle_dragging() const { return m_selection_rectangle.is_dragging(); } + +private: + bool on_init() override; + void on_update(const UpdateData& data) override; + void on_render() const override; + void on_render_for_picking() const override; + + void render_points(const Selection& selection, bool picking = false) const; + void render_clipping_plane(const Selection& selection) const; + void render_hollowed_mesh() const; + bool is_mesh_update_necessary() const; + void update_mesh(); + void hollow_mesh(); + bool unsaved_changes() const; + + bool m_show_supports = true; + float m_new_hole_radius = 2.f; // Size of a new hole. + float m_new_hole_height = 5.f; + mutable std::vector m_selected; // which holes are currently selected + + bool m_enable_hollowing = true; + + // Stashes to keep data for undo redo. Is taken after the editing + // is done, the data are updated continuously. + float m_offset_stash = 3.0f; + float m_quality_stash = 0.5f; + float m_closing_d_stash = 2.f; + Vec3f m_hole_before_drag = Vec3f::Zero(); + + + sla::DrainHoles m_holes_stash; + + + + float m_clipping_plane_distance = 0.f; + std::unique_ptr m_clipping_plane; + + // This map holds all translated description texts, so they can be easily referenced during layout calculations + // etc. When language changes, GUI is recreated and this class constructed again, so the change takes effect. + std::map m_desc; + + GLSelectionRectangle m_selection_rectangle; + + bool m_wait_for_up_event = false; + bool m_selection_empty = true; + EState m_old_state = Off; // to be able to see that the gizmo has just been closed (see on_set_state) + + std::vector> get_config_options(const std::vector& keys) const; + bool is_mesh_point_clipped(const Vec3d& point) const; + + // Methods that do the model_object and editing cache synchronization, + // editing mode selection, etc: + enum { + AllPoints = -2, + NoPoints, + }; + void select_point(int i); + void unselect_point(int i); + void reload_cache(); + void update_clipping_plane(bool keep_normal = false) const; + +protected: + void on_set_state() override; + void on_set_hover_id() override + + { + if (int(m_c->m_model_object->sla_drain_holes.size()) <= m_hover_id) + m_hover_id = -1; + } + void on_start_dragging() override; + void on_stop_dragging() override; + void on_render_input_window(float x, float y, float bottom_limit) override; + + std::string on_get_name() const override; + bool on_is_activable() const override; + bool on_is_selectable() const override; + void on_load(cereal::BinaryInputArchive& ar) override; + void on_save(cereal::BinaryOutputArchive& ar) const override; +}; + + + +} // namespace GUI +} // namespace Slic3r + +#endif // slic3r_GLGizmoHollow_hpp_ diff --git a/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.cpp b/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.cpp index a0e84d4903..c6e0d9007b 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.cpp @@ -22,8 +22,8 @@ namespace Slic3r { namespace GUI { -GLGizmoSlaSupports::GLGizmoSlaSupports(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id) - : GLGizmoBase(parent, icon_filename, sprite_id) +GLGizmoSlaSupports::GLGizmoSlaSupports(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id, CommonGizmosData* cd) + : GLGizmoBase(parent, icon_filename, sprite_id, cd) , m_quadric(nullptr) , m_its(nullptr) { @@ -64,23 +64,35 @@ bool GLGizmoSlaSupports::on_init() void GLGizmoSlaSupports::set_sla_support_data(ModelObject* model_object, const Selection& selection) { if (! model_object || selection.is_empty()) { - m_model_object = nullptr; + m_c->m_model_object = nullptr; return; } - if (m_model_object != model_object || m_model_object_id != model_object->id()) { - m_model_object = model_object; - m_print_object_idx = -1; - } + bool something_changed = false; - m_active_instance = selection.get_instance_idx(); + if (m_c->m_model_object != model_object + || m_c->m_model_object_id != model_object->id() + || m_c->m_active_instance != selection.get_instance_idx()) { + m_c->m_model_object = model_object; + m_c->m_print_object_idx = -1; + m_c->m_active_instance = selection.get_instance_idx(); + something_changed = true; + } if (model_object && selection.is_from_single_instance()) { // Cache the bb - it's needed for dealing with the clipping plane quite often // It could be done inside update_mesh but one has to account for scaling of the instance. - //FIXME calling ModelObject::instance_bounding_box() is expensive! - m_active_instance_bb_radius = m_model_object->instance_bounding_box(m_active_instance).radius(); + if (something_changed) { + m_c->m_active_instance_bb_radius = m_c->m_model_object->instance_bounding_box(m_c->m_active_instance).radius(); + if (m_state == On) { + m_parent.toggle_model_objects_visibility(false); + m_parent.toggle_model_objects_visibility(! m_c->m_cavity_mesh, m_c->m_model_object, m_c->m_active_instance); + m_parent.toggle_sla_auxiliaries_visibility(! m_editing_mode, m_c->m_model_object, m_c->m_active_instance); + } + else + m_parent.toggle_model_objects_visibility(true, nullptr, -1); + } if (is_mesh_update_necessary()) { update_mesh(); @@ -88,15 +100,8 @@ void GLGizmoSlaSupports::set_sla_support_data(ModelObject* model_object, const S } // If we triggered autogeneration before, check backend and fetch results if they are there - if (m_model_object->sla_points_status == sla::PointsStatus::Generating) + if (m_c->m_model_object->sla_points_status == sla::PointsStatus::Generating) get_data_from_backend(); - - if (m_state == On) { - m_parent.toggle_model_objects_visibility(false); - m_parent.toggle_model_objects_visibility(true, m_model_object, m_active_instance); - } - else - m_parent.toggle_model_objects_visibility(true, nullptr, -1); } } @@ -106,16 +111,16 @@ void GLGizmoSlaSupports::on_render() const { const Selection& selection = m_parent.get_selection(); - // If current m_model_object does not match selection, ask GLCanvas3D to turn us off + // If current m_c->m_model_object does not match selection, ask GLCanvas3D to turn us off if (m_state == On - && (m_model_object != selection.get_model()->objects[selection.get_object_idx()] - || m_active_instance != selection.get_instance_idx() - || m_model_object_id != m_model_object->id())) { + && (m_c->m_model_object != selection.get_model()->objects[selection.get_object_idx()] + || m_c->m_active_instance != selection.get_instance_idx() + || m_c->m_model_object_id != m_c->m_model_object->id())) { m_parent.post_event(SimpleEvent(EVT_GLCANVAS_RESETGIZMOS)); return; } - if (! m_its || ! m_mesh) + if (! m_its || ! m_c->m_mesh) const_cast(this)->update_mesh(); glsafe(::glEnable(GL_BLEND)); @@ -123,6 +128,8 @@ void GLGizmoSlaSupports::on_render() const m_z_shift = selection.get_volume(*selection.get_volume_idxs().begin())->get_sla_shift_z(); + render_hollowed_mesh(); + if (m_quadric != nullptr && selection.is_from_single_instance()) render_points(selection, false); @@ -134,9 +141,32 @@ void GLGizmoSlaSupports::on_render() const +void GLGizmoSlaSupports::render_hollowed_mesh() const +{ + if (m_c->m_volume_with_cavity) { + m_c->m_volume_with_cavity->set_sla_shift_z(m_z_shift); + m_parent.get_shader().start_using(); + + GLint current_program_id; + glsafe(::glGetIntegerv(GL_CURRENT_PROGRAM, ¤t_program_id)); + GLint color_id = (current_program_id > 0) ? ::glGetUniformLocation(current_program_id, "uniform_color") : -1; + GLint print_box_detection_id = (current_program_id > 0) ? ::glGetUniformLocation(current_program_id, "print_box.volume_detection") : -1; + GLint print_box_worldmatrix_id = (current_program_id > 0) ? ::glGetUniformLocation(current_program_id, "print_box.volume_world_matrix") : -1; + glcheck(); + m_c->m_volume_with_cavity->set_render_color(); + const Geometry::Transformation& volume_trafo = m_c->m_model_object->volumes.front()->get_transformation(); + m_c->m_volume_with_cavity->set_volume_transformation(volume_trafo); + m_c->m_volume_with_cavity->set_instance_transformation(m_c->m_model_object->instances[size_t(m_c->m_active_instance)]->get_transformation()); + m_c->m_volume_with_cavity->render(color_id, print_box_detection_id, print_box_worldmatrix_id); + m_parent.get_shader().stop_using(); + } +} + + + void GLGizmoSlaSupports::render_clipping_plane(const Selection& selection) const { - if (m_clipping_plane_distance == 0.f) + if (m_clipping_plane_distance == 0.f || m_c->m_mesh->empty()) return; // Get transformation of the instance @@ -154,66 +184,66 @@ void GLGizmoSlaSupports::render_clipping_plane(const Selection& selection) const 1.)); // Now initialize the TMS for the object, perform the cut and save the result. - if (! m_object_clipper) { - m_object_clipper.reset(new MeshClipper); - m_object_clipper->set_mesh(*m_mesh); + if (! m_c->m_object_clipper) { + m_c->m_object_clipper.reset(new MeshClipper); + m_c->m_object_clipper->set_mesh(*m_c->mesh()); } - m_object_clipper->set_plane(*m_clipping_plane); - m_object_clipper->set_transformation(trafo); + m_c->m_object_clipper->set_plane(*m_clipping_plane); + m_c->m_object_clipper->set_transformation(trafo); // Next, ask the backend if supports are already calculated. If so, we are gonna cut them too. // First we need a pointer to the respective SLAPrintObject. The index into objects vector is // cached so we don't have todo it on each render. We only search for the po if needed: - if (m_print_object_idx < 0 || (int)m_parent.sla_print()->objects().size() != m_print_objects_count) { - m_print_objects_count = m_parent.sla_print()->objects().size(); - m_print_object_idx = -1; + if (m_c->m_print_object_idx < 0 || (int)m_parent.sla_print()->objects().size() != m_c->m_print_objects_count) { + m_c->m_print_objects_count = m_parent.sla_print()->objects().size(); + m_c->m_print_object_idx = -1; for (const SLAPrintObject* po : m_parent.sla_print()->objects()) { - ++m_print_object_idx; - if (po->model_object()->id() == m_model_object->id()) + ++m_c->m_print_object_idx; + if (po->model_object()->id() == m_c->m_model_object->id()) break; } } - if (m_print_object_idx >= 0) { - const SLAPrintObject* print_object = m_parent.sla_print()->objects()[m_print_object_idx]; + if (m_c->m_print_object_idx >= 0) { + const SLAPrintObject* print_object = m_parent.sla_print()->objects()[m_c->m_print_object_idx]; - if (print_object->is_step_done(slaposSupportTree)) { + if (print_object->is_step_done(slaposSupportTree) && !print_object->get_mesh(slaposSupportTree).empty()) { // If the supports are already calculated, save the timestamp of the respective step // so we can later tell they were recalculated. size_t timestamp = print_object->step_state_with_timestamp(slaposSupportTree).timestamp; - if (! m_supports_clipper || (int)timestamp != m_old_timestamp) { + if (! m_c->m_supports_clipper || (int)timestamp != m_c->m_old_timestamp) { // The timestamp has changed. - m_supports_clipper.reset(new MeshClipper); + m_c->m_supports_clipper.reset(new MeshClipper); // The mesh should already have the shared vertices calculated. - m_supports_clipper->set_mesh(print_object->support_mesh()); - m_old_timestamp = timestamp; + m_c->m_supports_clipper->set_mesh(print_object->support_mesh()); + m_c->m_old_timestamp = timestamp; } - m_supports_clipper->set_plane(*m_clipping_plane); - m_supports_clipper->set_transformation(supports_trafo); + m_c->m_supports_clipper->set_plane(*m_clipping_plane); + m_c->m_supports_clipper->set_transformation(supports_trafo); } else // The supports are not valid. We better dump the cached data. - m_supports_clipper.reset(); + m_c->m_supports_clipper.reset(); } // At this point we have the triangulated cuts for both the object and supports - let's render. - if (! m_object_clipper->get_triangles().empty()) { + if (! m_c->m_object_clipper->get_triangles().empty()) { ::glPushMatrix(); ::glColor3f(1.0f, 0.37f, 0.0f); ::glBegin(GL_TRIANGLES); - for (const Vec3f& point : m_object_clipper->get_triangles()) + for (const Vec3f& point : m_c->m_object_clipper->get_triangles()) ::glVertex3f(point(0), point(1), point(2)); ::glEnd(); ::glPopMatrix(); } - if (m_supports_clipper && ! m_supports_clipper->get_triangles().empty() && !m_editing_mode) { + if (m_c->m_supports_clipper && ! m_c->m_supports_clipper->get_triangles().empty() && !m_editing_mode) { // The supports are hidden in the editing mode, so it makes no sense to render the cuts. ::glPushMatrix(); ::glColor3f(1.0f, 0.f, 0.37f); ::glBegin(GL_TRIANGLES); - for (const Vec3f& point : m_supports_clipper->get_triangles()) + for (const Vec3f& point : m_c->m_supports_clipper->get_triangles()) ::glVertex3f(point(0), point(1), point(2)); ::glEnd(); ::glPopMatrix(); @@ -230,6 +260,7 @@ void GLGizmoSlaSupports::on_render_for_picking() const glsafe(::glEnable(GL_DEPTH_TEST)); render_points(selection, true); + render_hollowed_mesh(); } void GLGizmoSlaSupports::render_points(const Selection& selection, bool picking) const @@ -298,7 +329,7 @@ void GLGizmoSlaSupports::render_points(const Selection& selection, bool picking) if (m_editing_mode) { // in case the normal is not yet cached, find and cache it if (m_editing_cache[i].normal == Vec3f::Zero()) - m_mesh_raycaster->get_closest_point(m_editing_cache[i].support_point.pos, &m_editing_cache[i].normal); + m_c->m_mesh_raycaster->get_closest_point(m_editing_cache[i].support_point.pos, &m_editing_cache[i].normal); Eigen::Quaterniond q; q.setFromTwoVectors(Vec3d{0., 0., 1.}, instance_scaling_matrix_inverse * m_editing_cache[i].normal.cast()); @@ -327,6 +358,44 @@ void GLGizmoSlaSupports::render_points(const Selection& selection, bool picking) glsafe(::glMaterialfv(GL_FRONT, GL_EMISSION, render_color_emissive)); } + // Now render the drain holes: + if (! m_c->m_cavity_mesh) { + render_color[0] = 0.7f; + render_color[1] = 0.7f; + render_color[2] = 0.7f; + render_color[3] = 0.7f; + glsafe(::glColor4fv(render_color)); + for (const sla::DrainHole& drain_hole : m_c->m_model_object->sla_drain_holes) { + // Inverse matrix of the instance scaling is applied so that the mark does not scale with the object. + glsafe(::glPushMatrix()); + glsafe(::glTranslatef(drain_hole.pos(0), drain_hole.pos(1), drain_hole.pos(2))); + glsafe(::glMultMatrixd(instance_scaling_matrix_inverse.data())); + + if (vol->is_left_handed()) + glFrontFace(GL_CW); + + // Matrices set, we can render the point mark now. + + Eigen::Quaterniond q; + q.setFromTwoVectors(Vec3d{0., 0., 1.}, instance_scaling_matrix_inverse * (-drain_hole.normal).cast()); + Eigen::AngleAxisd aa(q); + glsafe(::glRotated(aa.angle() * (180. / M_PI), aa.axis()(0), aa.axis()(1), aa.axis()(2))); + glsafe(::glPushMatrix()); + glsafe(::glTranslated(0., 0., -drain_hole.height)); + ::gluCylinder(m_quadric, drain_hole.radius, drain_hole.radius, drain_hole.height, 24, 1); + glsafe(::glTranslated(0., 0., drain_hole.height)); + ::gluDisk(m_quadric, 0.0, drain_hole.radius, 24, 1); + glsafe(::glTranslated(0., 0., -drain_hole.height)); + glsafe(::glRotatef(180.f, 1.f, 0.f, 0.f)); + ::gluDisk(m_quadric, 0.0, drain_hole.radius, 24, 1); + glsafe(::glPopMatrix()); + + if (vol->is_left_handed()) + glFrontFace(GL_CCW); + glsafe(::glPopMatrix()); + } + } + if (!picking) glsafe(::glDisable(GL_LIGHTING)); @@ -340,7 +409,7 @@ bool GLGizmoSlaSupports::is_mesh_point_clipped(const Vec3d& point) const if (m_clipping_plane_distance == 0.f) return false; - Vec3d transformed_point = m_model_object->instances.front()->get_transformation().get_matrix() * point; + Vec3d transformed_point = m_c->m_model_object->instances[m_c->m_active_instance]->get_transformation().get_matrix() * point; transformed_point(2) += m_z_shift; return m_clipping_plane->is_point_clipped(transformed_point); } @@ -349,39 +418,38 @@ bool GLGizmoSlaSupports::is_mesh_point_clipped(const Vec3d& point) const bool GLGizmoSlaSupports::is_mesh_update_necessary() const { - return ((m_state == On) && (m_model_object != nullptr) && !m_model_object->instances.empty()) - && ((m_model_object->id() != m_model_object_id) || m_its == nullptr); + return ((m_state == On) && (m_c->m_model_object != nullptr) && !m_c->m_model_object->instances.empty()) + && ((m_c->m_model_object->id() != m_c->m_model_object_id) || m_its == nullptr); } void GLGizmoSlaSupports::update_mesh() { - if (! m_model_object) + if (! m_c->m_model_object) return; wxBusyCursor wait; // this way we can use that mesh directly. // This mesh does not account for the possible Z up SLA offset. - m_mesh = &m_model_object->volumes.front()->mesh(); - m_its = &m_mesh->its; + m_c->m_mesh = &m_c->m_model_object->volumes.front()->mesh(); + m_its = &m_c->m_mesh->its; // If this is different mesh than last time or if the AABB tree is uninitialized, recalculate it. - if (m_model_object_id != m_model_object->id() || ! m_mesh_raycaster) - m_mesh_raycaster.reset(new MeshRaycaster(*m_mesh)); + if (m_c->m_model_object_id != m_c->m_model_object->id() || ! m_c->m_mesh_raycaster) + m_c->m_mesh_raycaster.reset(new MeshRaycaster(*m_c->mesh())); - m_model_object_id = m_model_object->id(); + m_c->m_model_object_id = m_c->m_model_object->id(); disable_editing_mode(); } - // Unprojects the mouse position on the mesh and saves hit point and normal of the facet into pos_and_normal // Return false if no intersection was found, true otherwise. bool GLGizmoSlaSupports::unproject_on_mesh(const Vec2d& mouse_pos, std::pair& pos_and_normal) { // if the gizmo doesn't have the V, F structures for igl, calculate them first: - if (! m_mesh_raycaster) + if (! m_c->m_mesh_raycaster) update_mesh(); const Camera& camera = m_parent.get_camera(); @@ -393,13 +461,28 @@ bool GLGizmoSlaSupports::unproject_on_mesh(const Vec2d& mouse_pos, std::pairunproject_on_mesh(mouse_pos, trafo.get_matrix(), camera, hit, normal, m_clipping_plane.get())) { - // Return both the point and the facet normal. - pos_and_normal = std::make_pair(hit, normal); - return true; + if (m_c->m_mesh_raycaster->unproject_on_mesh(mouse_pos, trafo.get_matrix(), camera, hit, normal, m_clipping_plane.get())) { + // Check whether the hit is in a hole + bool in_hole = false; + // In case the hollowed and drilled mesh is available, we can allow + // placing points in holes, because they should never end up + // on surface that's been drilled away. + if (! m_c->m_cavity_mesh) { + for (const sla::DrainHole& hole : m_c->m_model_object->sla_drain_holes) { + if (hole.is_inside(hit)) { + in_hole = true; + break; + } + } + } + if (! in_hole) { + // Return both the point and the facet normal. + pos_and_normal = std::make_pair(hit, normal); + return true; + } } - else - return false; + + return false; } // Following function is called from GLCanvas3D to inform the gizmo about a mouse/keyboard event. @@ -459,7 +542,7 @@ bool GLGizmoSlaSupports::gizmo_event(SLAGizmoEventType action, const Vec2d& mous GLSelectionRectangle::EState rectangle_status = m_selection_rectangle.get_state(); // First collect positions of all the points in world coordinates. - Geometry::Transformation trafo = m_model_object->instances[m_active_instance]->get_transformation(); + Geometry::Transformation trafo = m_c->m_model_object->instances[m_c->m_active_instance]->get_transformation(); trafo.set_offset(trafo.get_offset() + Vec3d(0., 0., m_z_shift)); std::vector points; for (unsigned int i=0; i()); // Only select/deselect points that are actually visible - for (size_t idx : m_mesh_raycaster->get_unobscured_idxs(trafo, m_parent.get_camera(), points_inside, m_clipping_plane.get())) + for (size_t idx : m_c->m_mesh_raycaster->get_unobscured_idxs(trafo, m_parent.get_camera(), points_inside, m_clipping_plane.get())) { if (rectangle_status == GLSelectionRectangle::Deselect) unselect_point(points_idxs[idx]); @@ -610,10 +693,10 @@ std::vector GLGizmoSlaSupports::get_config_options(const st { std::vector out; - if (!m_model_object) + if (!m_c->m_model_object) return out; - const DynamicPrintConfig& object_cfg = m_model_object->config; + const DynamicPrintConfig& object_cfg = m_c->m_model_object->config; const DynamicPrintConfig& print_cfg = wxGetApp().preset_bundle->sla_prints.get_edited_preset().config; std::unique_ptr default_cfg = nullptr; @@ -636,7 +719,7 @@ std::vector GLGizmoSlaSupports::get_config_options(const st ClippingPlane GLGizmoSlaSupports::get_sla_clipping_plane() const { - if (!m_model_object || m_state == Off || m_clipping_plane_distance == 0.f) + if (!m_c->m_model_object || m_state == Off || m_clipping_plane_distance == 0.f) return ClippingPlane::ClipsNothing(); else return ClippingPlane(-m_clipping_plane->get_normal(), m_clipping_plane->get_data()[3]); @@ -665,7 +748,7 @@ void GLGizmoSlaSupports::find_intersecting_facets(const igl::AABBvolumes.front()->mesh); + TriangleMeshSlicer tms(&m_c->m_model_object->volumes.front()->mesh); Vec3f normal(0.f, 1.f, 1.f); double d = 0.; @@ -689,7 +772,7 @@ void GLGizmoSlaSupports::on_render_input_window(float x, float y, float bottom_l static float last_y = 0.0f; static float last_h = 0.0f; - if (!m_model_object) + if (! m_c->m_model_object) return; bool first_run = true; // This is a hack to redraw the button when all points are removed, @@ -699,7 +782,7 @@ RENDER_AGAIN: //const ImVec2 window_size(m_imgui->scaled(18.f, 16.f)); //ImGui::SetNextWindowPos(ImVec2(x, y - std::max(0.f, y+window_size.y-bottom_limit) )); //ImGui::SetNextWindowSize(ImVec2(window_size)); - + m_imgui->begin(on_get_name(), ImGuiWindowFlags_NoMove | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoCollapse); // adjust window position to avoid overlap the view toolbar @@ -728,7 +811,6 @@ RENDER_AGAIN: float window_width = minimal_slider_width + std::max(std::max(settings_sliders_left, clipping_slider_left), diameter_slider_left); window_width = std::max(std::max(window_width, buttons_width_approx), lock_supports_width_approx); - bool force_refresh = false; bool remove_selected = false; bool remove_all = false; @@ -827,15 +909,15 @@ RENDER_AGAIN: m_density_stash = density; } if (slider_edited) { - m_model_object->config.opt("support_points_minimal_distance", true)->value = minimal_point_distance; - m_model_object->config.opt("support_points_density_relative", true)->value = (int)density; + m_c->m_model_object->config.opt("support_points_minimal_distance", true)->value = minimal_point_distance; + m_c->m_model_object->config.opt("support_points_density_relative", true)->value = (int)density; } if (slider_released) { - m_model_object->config.opt("support_points_minimal_distance", true)->value = m_minimal_point_distance_stash; - m_model_object->config.opt("support_points_density_relative", true)->value = (int)m_density_stash; + m_c->m_model_object->config.opt("support_points_minimal_distance", true)->value = m_minimal_point_distance_stash; + m_c->m_model_object->config.opt("support_points_density_relative", true)->value = (int)m_density_stash; Plater::TakeSnapshot snapshot(wxGetApp().plater(), _(L("Support parameter change"))); - m_model_object->config.opt("support_points_minimal_distance", true)->value = minimal_point_distance; - m_model_object->config.opt("support_points_density_relative", true)->value = (int)density; + m_c->m_model_object->config.opt("support_points_minimal_distance", true)->value = minimal_point_distance; + m_c->m_model_object->config.opt("support_points_density_relative", true)->value = (int)density; wxGetApp().obj_list()->update_and_show_object_settings_item(); } @@ -853,10 +935,10 @@ RENDER_AGAIN: m_imgui->disabled_end(); // m_imgui->text(""); - // m_imgui->text(m_model_object->sla_points_status == sla::PointsStatus::NoPoints ? _(L("No points (will be autogenerated)")) : - // (m_model_object->sla_points_status == sla::PointsStatus::AutoGenerated ? _(L("Autogenerated points (no modifications)")) : - // (m_model_object->sla_points_status == sla::PointsStatus::UserModified ? _(L("User-modified points")) : - // (m_model_object->sla_points_status == sla::PointsStatus::Generating ? _(L("Generation in progress...")) : "UNKNOWN STATUS")))); + // m_imgui->text(m_c->m_model_object->sla_points_status == sla::PointsStatus::NoPoints ? _(L("No points (will be autogenerated)")) : + // (m_c->m_model_object->sla_points_status == sla::PointsStatus::AutoGenerated ? _(L("Autogenerated points (no modifications)")) : + // (m_c->m_model_object->sla_points_status == sla::PointsStatus::UserModified ? _(L("User-modified points")) : + // (m_c->m_model_object->sla_points_status == sla::PointsStatus::Generating ? _(L("Generation in progress...")) : "UNKNOWN STATUS")))); } @@ -890,12 +972,6 @@ RENDER_AGAIN: m_imgui->end(); - if (m_editing_mode != m_old_editing_state) { // user toggled between editing/non-editing mode - m_parent.toggle_sla_auxiliaries_visibility(!m_editing_mode, m_model_object, m_active_instance); - force_refresh = true; - } - m_old_editing_state = m_editing_mode; - if (remove_selected || remove_all) { force_refresh = false; m_parent.set_as_dirty(); @@ -952,12 +1028,12 @@ std::string GLGizmoSlaSupports::on_get_name() const void GLGizmoSlaSupports::on_set_state() { - // m_model_object pointer can be invalid (for instance because of undo/redo action), + // m_c->m_model_object pointer can be invalid (for instance because of undo/redo action), // we should recover it from the object id - m_model_object = nullptr; + m_c->m_model_object = nullptr; for (const auto mo : wxGetApp().model().objects) { - if (mo->id() == m_model_object_id) { - m_model_object = mo; + if (mo->id() == m_c->m_model_object_id) { + m_c->m_model_object = mo; break; } } @@ -971,19 +1047,20 @@ void GLGizmoSlaSupports::on_set_state() update_mesh(); // we'll now reload support points: - if (m_model_object) + if (m_c->m_model_object) reload_cache(); m_parent.toggle_model_objects_visibility(false); - if (m_model_object) - m_parent.toggle_model_objects_visibility(true, m_model_object, m_active_instance); + if (m_c->m_model_object && ! m_c->m_cavity_mesh) + m_parent.toggle_model_objects_visibility(true, m_c->m_model_object, m_c->m_active_instance); + m_parent.toggle_sla_auxiliaries_visibility(! m_editing_mode, m_c->m_model_object, m_c->m_active_instance); // Set default head diameter from config. const DynamicPrintConfig& cfg = wxGetApp().preset_bundle->sla_prints.get_edited_preset().config; m_new_point_head_diameter = static_cast(cfg.option("support_head_front_diameter"))->value; } if (m_state == Off && m_old_state != Off) { // the gizmo was just turned Off - bool will_ask = m_model_object && m_editing_mode && unsaved_changes(); + bool will_ask = m_c->m_model_object && m_editing_mode && unsaved_changes(); if (will_ask) { wxGetApp().CallAfter([this]() { // Following is called through CallAfter, because otherwise there was a problem @@ -1005,11 +1082,12 @@ void GLGizmoSlaSupports::on_set_state() m_parent.toggle_model_objects_visibility(true); m_normal_cache.clear(); m_clipping_plane_distance = 0.f; + update_clipping_plane(); // Release clippers and the AABB raycaster. m_its = nullptr; - m_object_clipper.reset(); - m_supports_clipper.reset(); - m_mesh_raycaster.reset(); + m_c->m_object_clipper.reset(); + m_c->m_supports_clipper.reset(); + m_c->m_mesh_raycaster.reset(); } } m_old_state = m_state; @@ -1051,7 +1129,7 @@ void GLGizmoSlaSupports::on_load(cereal::BinaryInputArchive& ar) { ar(m_clipping_plane_distance, *m_clipping_plane, - m_model_object_id, + m_c->m_model_object_id, m_new_point_head_diameter, m_normal_cache, m_editing_cache, @@ -1065,7 +1143,7 @@ void GLGizmoSlaSupports::on_save(cereal::BinaryOutputArchive& ar) const { ar(m_clipping_plane_distance, *m_clipping_plane, - m_model_object_id, + m_c->m_model_object_id, m_new_point_head_diameter, m_normal_cache, m_editing_cache, @@ -1143,9 +1221,9 @@ void GLGizmoSlaSupports::editing_mode_apply_changes() for (const CacheEntry& ce : m_editing_cache) m_normal_cache.push_back(ce.support_point); - m_model_object->sla_points_status = sla::PointsStatus::UserModified; - m_model_object->sla_support_points.clear(); - m_model_object->sla_support_points = m_normal_cache; + m_c->m_model_object->sla_points_status = sla::PointsStatus::UserModified; + m_c->m_model_object->sla_support_points.clear(); + m_c->m_model_object->sla_support_points = m_normal_cache; reslice_SLA_supports(); } @@ -1156,10 +1234,10 @@ void GLGizmoSlaSupports::editing_mode_apply_changes() void GLGizmoSlaSupports::reload_cache() { m_normal_cache.clear(); - if (m_model_object->sla_points_status == sla::PointsStatus::AutoGenerated || m_model_object->sla_points_status == sla::PointsStatus::Generating) + if (m_c->m_model_object->sla_points_status == sla::PointsStatus::AutoGenerated || m_c->m_model_object->sla_points_status == sla::PointsStatus::Generating) get_data_from_backend(); else - for (const sla::SupportPoint& point : m_model_object->sla_support_points) + for (const sla::SupportPoint& point : m_c->m_model_object->sla_support_points) m_normal_cache.emplace_back(point); } @@ -1168,7 +1246,7 @@ bool GLGizmoSlaSupports::has_backend_supports() const { // find SlaPrintObject with this ID for (const SLAPrintObject* po : m_parent.sla_print()->objects()) { - if (po->model_object()->id() == m_model_object->id()) + if (po->model_object()->id() == m_c->m_model_object->id()) return po->is_step_done(slaposSupportPoints); } return false; @@ -1176,7 +1254,7 @@ bool GLGizmoSlaSupports::has_backend_supports() const void GLGizmoSlaSupports::reslice_SLA_supports(bool postpone_error_messages) const { - wxGetApp().CallAfter([this, postpone_error_messages]() { wxGetApp().plater()->reslice_SLA_supports(*m_model_object, postpone_error_messages); }); + wxGetApp().CallAfter([this, postpone_error_messages]() { wxGetApp().plater()->reslice_SLA_supports(*m_c->m_model_object, postpone_error_messages); }); } void GLGizmoSlaSupports::get_data_from_backend() @@ -1186,14 +1264,14 @@ void GLGizmoSlaSupports::get_data_from_backend() // find the respective SLAPrintObject, we need a pointer to it for (const SLAPrintObject* po : m_parent.sla_print()->objects()) { - if (po->model_object()->id() == m_model_object->id()) { + if (po->model_object()->id() == m_c->m_model_object->id()) { m_normal_cache.clear(); const std::vector& points = po->get_support_points(); auto mat = po->trafo().inverse().cast(); for (unsigned int i=0; isla_points_status = sla::PointsStatus::AutoGenerated; + m_c->m_model_object->sla_points_status = sla::PointsStatus::AutoGenerated; break; } } @@ -1210,10 +1288,10 @@ void GLGizmoSlaSupports::auto_generate() _(L("Are you sure you want to do it?")) + "\n", _(L("Warning")), wxICON_WARNING | wxYES | wxNO); - if (m_model_object->sla_points_status != sla::PointsStatus::UserModified || m_normal_cache.empty() || dlg.ShowModal() == wxID_YES) { + if (m_c->m_model_object->sla_points_status != sla::PointsStatus::UserModified || m_normal_cache.empty() || dlg.ShowModal() == wxID_YES) { Plater::TakeSnapshot snapshot(wxGetApp().plater(), _(L("Autogenerate support points"))); wxGetApp().CallAfter([this]() { reslice_SLA_supports(); }); - m_model_object->sla_points_status = sla::PointsStatus::Generating; + m_c->m_model_object->sla_points_status = sla::PointsStatus::Generating; } } @@ -1227,6 +1305,9 @@ void GLGizmoSlaSupports::switch_to_editing_mode() for (const sla::SupportPoint& sp : m_normal_cache) m_editing_cache.emplace_back(sp); select_point(NoPoints); + + m_parent.toggle_sla_auxiliaries_visibility(false, m_c->m_model_object, m_c->m_active_instance); + m_parent.set_as_dirty(); } @@ -1235,6 +1316,8 @@ void GLGizmoSlaSupports::disable_editing_mode() if (m_editing_mode) { m_editing_mode = false; wxGetApp().plater()->leave_gizmos_stack(); + m_parent.toggle_sla_auxiliaries_visibility(true, m_c->m_model_object, m_c->m_active_instance); + m_parent.set_as_dirty(); } } @@ -1258,9 +1341,9 @@ void GLGizmoSlaSupports::update_clipping_plane(bool keep_normal) const Vec3d normal = (keep_normal && m_clipping_plane->get_normal() != Vec3d::Zero() ? m_clipping_plane->get_normal() : -m_parent.get_camera().get_dir_forward()); - const Vec3d& center = m_model_object->instances[m_active_instance]->get_offset() + Vec3d(0., 0., m_z_shift); + const Vec3d& center = m_c->m_model_object->instances[m_c->m_active_instance]->get_offset() + Vec3d(0., 0., m_z_shift); float dist = normal.dot(center); - *m_clipping_plane = ClippingPlane(normal, (dist - (-m_active_instance_bb_radius) - m_clipping_plane_distance * 2*m_active_instance_bb_radius)); + *m_clipping_plane = ClippingPlane(normal, (dist - (-m_c->m_active_instance_bb_radius) - m_clipping_plane_distance * 2*m_c->m_active_instance_bb_radius)); m_parent.set_as_dirty(); } diff --git a/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.hpp b/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.hpp index 15b2df80ea..3697e7af69 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.hpp @@ -4,7 +4,7 @@ #include "GLGizmoBase.hpp" #include "slic3r/GUI/GLSelectionRectangle.hpp" -#include "libslic3r/SLA/SLACommon.hpp" +#include "libslic3r/SLA/Common.hpp" #include #include @@ -21,10 +21,10 @@ enum class SLAGizmoEventType : unsigned char; class GLGizmoSlaSupports : public GLGizmoBase { private: - ModelObject* m_model_object = nullptr; - ObjectID m_model_object_id = 0; - int m_active_instance = -1; - float m_active_instance_bb_radius; // to cache the bb + //ModelObject* m_model_object = nullptr; + //ObjectID m_model_object_id = 0; + //int m_active_instance = -1; + //float m_active_instance_bb_radius; // to cache the bb mutable double m_z_shift = 0.f; bool unproject_on_mesh(const Vec2d& mouse_pos, std::pair& pos_and_normal); @@ -34,15 +34,12 @@ private: typedef Eigen::Map> MapMatrixXfUnaligned; typedef Eigen::Map> MapMatrixXiUnaligned; - std::unique_ptr m_mesh_raycaster; - const TriangleMesh* m_mesh; + //std::unique_ptr m_mesh_raycaster; + //const TriangleMesh* m_mesh; const indexed_triangle_set* m_its; - mutable const TriangleMesh* m_supports_mesh; - mutable std::vector m_triangles; - mutable std::vector m_supports_triangles; - mutable int m_old_timestamp = -1; - mutable int m_print_object_idx = -1; - mutable int m_print_objects_count = -1; + //mutable int m_old_timestamp = -1; + //mutable int m_print_object_idx = -1; + //mutable int m_print_objects_count = -1; class CacheEntry { public: @@ -72,7 +69,7 @@ private: }; public: - GLGizmoSlaSupports(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id); + GLGizmoSlaSupports(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id, CommonGizmosData* cd); ~GLGizmoSlaSupports() override; void set_sla_support_data(ModelObject* model_object, const Selection& selection); bool gizmo_event(SLAGizmoEventType action, const Vec2d& mouse_position, bool shift_down, bool alt_down, bool control_down); @@ -93,13 +90,13 @@ private: //void render_selection_rectangle() const; void render_points(const Selection& selection, bool picking = false) const; void render_clipping_plane(const Selection& selection) const; + void render_hollowed_mesh() const; bool is_mesh_update_necessary() const; void update_mesh(); bool unsaved_changes() const; bool m_lock_unique_islands = false; bool m_editing_mode = false; // Is editing mode active? - bool m_old_editing_state = false; // To keep track of whether the user toggled between the modes (needed for imgui refreshes). float m_new_point_head_diameter; // Size of a new point. CacheEntry m_point_before_drag; // undo/redo - so we know what state was edited float m_old_point_head_diameter = 0.; // the same @@ -121,11 +118,12 @@ private: bool m_selection_empty = true; EState m_old_state = Off; // to be able to see that the gizmo has just been closed (see on_set_state) - mutable std::unique_ptr m_object_clipper; - mutable std::unique_ptr m_supports_clipper; + //mutable std::unique_ptr m_object_clipper; + //mutable std::unique_ptr m_supports_clipper; std::vector get_config_options(const std::vector& keys) const; bool is_mesh_point_clipped(const Vec3d& point) const; + bool is_point_in_hole(const Vec3f& pt) const; //void find_intersecting_facets(const igl::AABB* aabb, const Vec3f& normal, double offset, std::vector& out) const; // Methods that do the model_object and editing cache synchronization, diff --git a/src/slic3r/GUI/Gizmos/GLGizmos.hpp b/src/slic3r/GUI/Gizmos/GLGizmos.hpp index 272fa098a3..9f97c42b46 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmos.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmos.hpp @@ -32,5 +32,6 @@ enum class SLAGizmoEventType : unsigned char { #include "slic3r/GUI/Gizmos/GLGizmoFlatten.hpp" #include "slic3r/GUI/Gizmos/GLGizmoSlaSupports.hpp" #include "slic3r/GUI/Gizmos/GLGizmoCut.hpp" +#include "slic3r/GUI/Gizmos/GLGizmoHollow.hpp" #endif //slic3r_GLGizmos_hpp_ diff --git a/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp b/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp index e639e3d892..ccc6369e46 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp @@ -83,12 +83,16 @@ bool GLGizmosManager::init() return false; } + m_common_gizmos_data.reset(new CommonGizmosData()); + + // Order of gizmos in the vector must match order in EType! m_gizmos.emplace_back(new GLGizmoMove3D(m_parent, "move.svg", 0)); m_gizmos.emplace_back(new GLGizmoScale3D(m_parent, "scale.svg", 1)); m_gizmos.emplace_back(new GLGizmoRotate3D(m_parent, "rotate.svg", 2)); m_gizmos.emplace_back(new GLGizmoFlatten(m_parent, "place.svg", 3)); m_gizmos.emplace_back(new GLGizmoCut(m_parent, "cut.svg", 4)); - m_gizmos.emplace_back(new GLGizmoSlaSupports(m_parent, "sla_supports.svg", 5)); + m_gizmos.emplace_back(new GLGizmoHollow(m_parent, "hollow.svg", 5, m_common_gizmos_data.get())); + m_gizmos.emplace_back(new GLGizmoSlaSupports(m_parent, "sla_supports.svg", 6, m_common_gizmos_data.get())); for (auto& gizmo : m_gizmos) { if (! gizmo->init()) { @@ -345,6 +349,7 @@ void GLGizmosManager::set_sla_support_data(ModelObject* model_object) return; dynamic_cast(m_gizmos[SlaSupports].get())->set_sla_support_data(model_object, m_parent.get_selection()); + dynamic_cast(m_gizmos[Hollow].get())->set_sla_support_data(model_object, m_parent.get_selection()); } // Returns true if the gizmo used the event to do something, false otherwise. @@ -353,15 +358,22 @@ bool GLGizmosManager::gizmo_event(SLAGizmoEventType action, const Vec2d& mouse_p if (!m_enabled || m_gizmos.empty()) return false; - return dynamic_cast(m_gizmos[SlaSupports].get())->gizmo_event(action, mouse_position, shift_down, alt_down, control_down); + if (m_current == SlaSupports) + return dynamic_cast(m_gizmos[SlaSupports].get())->gizmo_event(action, mouse_position, shift_down, alt_down, control_down); + if (m_current == Hollow) + return dynamic_cast(m_gizmos[Hollow].get())->gizmo_event(action, mouse_position, shift_down, alt_down, control_down); + return false; } ClippingPlane GLGizmosManager::get_sla_clipping_plane() const { - if (!m_enabled || m_current != SlaSupports || m_gizmos.empty()) + if (!m_enabled || (m_current != SlaSupports && m_current != Hollow) || m_gizmos.empty()) return ClippingPlane::ClipsNothing(); - return dynamic_cast(m_gizmos[SlaSupports].get())->get_sla_clipping_plane(); + if (m_current == SlaSupports) + return dynamic_cast(m_gizmos[SlaSupports].get())->get_sla_clipping_plane(); + else + return dynamic_cast(m_gizmos[Hollow].get())->get_sla_clipping_plane(); } bool GLGizmosManager::wants_reslice_supports_on_undo() const @@ -401,7 +413,7 @@ bool GLGizmosManager::on_mouse_wheel(wxMouseEvent& evt) { bool processed = false; - if (m_current == SlaSupports) { + if (m_current == SlaSupports || m_current == Hollow) { float rot = (float)evt.GetWheelRotation() / (float)evt.GetWheelDelta(); if (gizmo_event((rot > 0.f ? SLAGizmoEventType::MouseWheelUp : SLAGizmoEventType::MouseWheelDown), Vec2d::Zero(), evt.ShiftDown(), evt.AltDown(), evt.ControlDown())) processed = true; @@ -459,7 +471,7 @@ bool GLGizmosManager::on_mouse(wxMouseEvent& evt) if (evt.LeftDown()) { - if ((m_current == SlaSupports) && gizmo_event(SLAGizmoEventType::LeftDown, mouse_pos, evt.ShiftDown(), evt.AltDown(), evt.ControlDown())) + if ((m_current == SlaSupports || m_current == Hollow) && gizmo_event(SLAGizmoEventType::LeftDown, mouse_pos, evt.ShiftDown(), evt.AltDown(), evt.ControlDown())) // the gizmo got the event and took some action, there is no need to do anything more processed = true; else if (!selection.is_empty() && grabber_contains_mouse()) { @@ -477,17 +489,17 @@ bool GLGizmosManager::on_mouse(wxMouseEvent& evt) processed = true; } } - else if (evt.RightDown() && (selected_object_idx != -1) && (m_current == SlaSupports) && gizmo_event(SLAGizmoEventType::RightDown)) + else if (evt.RightDown() && (selected_object_idx != -1) && (m_current == SlaSupports || m_current == Hollow) && gizmo_event(SLAGizmoEventType::RightDown)) { // we need to set the following right up as processed to avoid showing the context menu if the user release the mouse over the object pending_right_up = true; // event was taken care of by the SlaSupports gizmo processed = true; } - else if (evt.Dragging() && (m_parent.get_move_volume_id() != -1) && (m_current == SlaSupports)) + else if (evt.Dragging() && (m_parent.get_move_volume_id() != -1) && (m_current == SlaSupports || m_current == Hollow)) // don't allow dragging objects with the Sla gizmo on processed = true; - else if (evt.Dragging() && (m_current == SlaSupports) && gizmo_event(SLAGizmoEventType::Dragging, mouse_pos, evt.ShiftDown(), evt.AltDown(), evt.ControlDown())) + else if (evt.Dragging() && (m_current == SlaSupports || m_current == Hollow) && gizmo_event(SLAGizmoEventType::Dragging, mouse_pos, evt.ShiftDown(), evt.AltDown(), evt.ControlDown())) { // the gizmo got the event and took some action, no need to do anything more here m_parent.set_as_dirty(); @@ -513,10 +525,10 @@ bool GLGizmosManager::on_mouse(wxMouseEvent& evt) case Scale: { // Apply new temporary scale factors - TransformationType transformation_type(TransformationType::Local_Absolute_Joint); - if (evt.AltDown()) - transformation_type.set_independent(); - selection.scale(get_scale(), transformation_type); + TransformationType transformation_type(TransformationType::Local_Absolute_Joint); + if (evt.AltDown()) + transformation_type.set_independent(); + selection.scale(get_scale(), transformation_type); if (evt.ControlDown()) selection.translate(get_scale_offset(), true); wxGetApp().obj_manipul()->set_dirty(); @@ -563,7 +575,7 @@ bool GLGizmosManager::on_mouse(wxMouseEvent& evt) processed = true; } - else if (evt.LeftUp() && (m_current == SlaSupports) && !m_parent.is_mouse_dragging()) + else if (evt.LeftUp() && (m_current == SlaSupports || m_current == Hollow) && !m_parent.is_mouse_dragging()) { // in case SLA gizmo is selected, we just pass the LeftUp event and stop processing - neither // object moving or selecting is suppressed in that case @@ -628,7 +640,7 @@ bool GLGizmosManager::on_char(wxKeyEvent& evt) #endif /* __APPLE__ */ { // Sla gizmo selects all support points - if ((m_current == SlaSupports) && gizmo_event(SLAGizmoEventType::SelectAll)) + if ((m_current == SlaSupports || m_current == Hollow) && gizmo_event(SLAGizmoEventType::SelectAll)) processed = true; break; @@ -662,7 +674,7 @@ bool GLGizmosManager::on_char(wxKeyEvent& evt) case 'r' : case 'R' : { - if ((m_current == SlaSupports) && gizmo_event(SLAGizmoEventType::ResetClippingPlane)) + if ((m_current == SlaSupports || m_current == Hollow) && gizmo_event(SLAGizmoEventType::ResetClippingPlane)) processed = true; break; @@ -674,7 +686,7 @@ bool GLGizmosManager::on_char(wxKeyEvent& evt) case WXK_DELETE: #endif /* __APPLE__ */ { - if ((m_current == SlaSupports) && gizmo_event(SLAGizmoEventType::Delete)) + if ((m_current == SlaSupports || m_current == Hollow) && gizmo_event(SLAGizmoEventType::Delete)) processed = true; break; @@ -695,7 +707,7 @@ bool GLGizmosManager::on_char(wxKeyEvent& evt) { if ((m_current == SlaSupports) && gizmo_event(SLAGizmoEventType::ManualEditing)) processed = true; - + break; } case 'F': @@ -736,20 +748,31 @@ bool GLGizmosManager::on_key(wxKeyEvent& evt) if (evt.GetEventType() == wxEVT_KEY_UP) { - if (m_current == SlaSupports) + if (m_current == SlaSupports || m_current == Hollow) { - GLGizmoSlaSupports* gizmo = dynamic_cast(get_current()); + bool is_editing = true; + bool is_rectangle_dragging = false; + + if (m_current == SlaSupports) { + GLGizmoSlaSupports* gizmo = dynamic_cast(get_current()); + is_editing = gizmo->is_in_editing_mode(); + is_rectangle_dragging = gizmo->is_selection_rectangle_dragging(); + } + else { + GLGizmoHollow* gizmo = dynamic_cast(get_current()); + is_rectangle_dragging = gizmo->is_selection_rectangle_dragging(); + } if (keyCode == WXK_SHIFT) { // shift has been just released - SLA gizmo might want to close rectangular selection. - if (gizmo_event(SLAGizmoEventType::ShiftUp) || (gizmo->is_in_editing_mode() && gizmo->is_selection_rectangle_dragging())) + if (gizmo_event(SLAGizmoEventType::ShiftUp) || (is_editing && is_rectangle_dragging)) processed = true; } else if (keyCode == WXK_ALT) { // alt has been just released - SLA gizmo might want to close rectangular selection. - if (gizmo_event(SLAGizmoEventType::AltUp) || (gizmo->is_in_editing_mode() && gizmo->is_selection_rectangle_dragging())) + if (gizmo_event(SLAGizmoEventType::AltUp) || (is_editing && is_rectangle_dragging)) processed = true; } } diff --git a/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp b/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp index 6120e961a7..2110e7b69b 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp @@ -54,11 +54,13 @@ public: enum EType : unsigned char { + // Order must match index in m_gizmos! Move, Scale, Rotate, Flatten, Cut, + Hollow, SlaSupports, Undefined }; @@ -111,6 +113,7 @@ private: MouseCapture m_mouse_capture; std::string m_tooltip; bool m_serializing; + std::unique_ptr m_common_gizmos_data; public: explicit GLGizmosManager(GLCanvas3D& parent); @@ -168,6 +171,7 @@ public: void update_data(); EType get_current_type() const { return m_current; } + GLGizmoBase* get_current() const; bool is_running() const; bool handle_shortcut(int key); @@ -216,8 +220,6 @@ private: float get_scaled_total_height() const; float get_scaled_total_width() const; - GLGizmoBase* get_current() const; - bool generate_icons_texture() const; void update_on_off_state(const Vec2d& mouse_pos); diff --git a/src/slic3r/GUI/Job.hpp b/src/slic3r/GUI/Job.hpp new file mode 100644 index 0000000000..ac31b9bdb0 --- /dev/null +++ b/src/slic3r/GUI/Job.hpp @@ -0,0 +1,155 @@ +#ifndef JOB_HPP +#define JOB_HPP + +#include + +#include +#include +#include + +#include + +#include + +namespace Slic3r { namespace GUI { + +// A class to handle UI jobs like arranging and optimizing rotation. +// These are not instant jobs, the user has to be informed about their +// state in the status progress indicator. On the other hand they are +// separated from the background slicing process. Ideally, these jobs should +// run when the background process is not running. +// +// TODO: A mechanism would be useful for blocking the plater interactions: +// objects would be frozen for the user. In case of arrange, an animation +// could be shown, or with the optimize orientations, partial results +// could be displayed. +class Job : public wxEvtHandler +{ + int m_range = 100; + boost::thread m_thread; + std::atomic m_running{false}, m_canceled{false}; + bool m_finalized = false; + std::shared_ptr m_progress; + + void run() + { + m_running.store(true); + process(); + m_running.store(false); + + // ensure to call the last status to finalize the job + update_status(status_range(), ""); + } + +protected: + // status range for a particular job + virtual int status_range() const { return 100; } + + // status update, to be used from the work thread (process() method) + void update_status(int st, const wxString &msg = "") + { + auto evt = new wxThreadEvent(); + evt->SetInt(st); + evt->SetString(msg); + wxQueueEvent(this, evt); + } + + bool was_canceled() const { return m_canceled.load(); } + + // Launched just before start(), a job can use it to prepare internals + virtual void prepare() {} + + // Launched when the job is finished. It refreshes the 3Dscene by def. + virtual void finalize() { m_finalized = true; } + + +public: + Job(std::shared_ptr pri) : m_progress(pri) + { + Bind(wxEVT_THREAD, [this](const wxThreadEvent &evt) { + auto msg = evt.GetString(); + if (!msg.empty()) + m_progress->set_status_text(msg.ToUTF8().data()); + + if (m_finalized) return; + + m_progress->set_progress(evt.GetInt()); + if (evt.GetInt() == status_range()) { + // set back the original range and cancel callback + m_progress->set_range(m_range); + m_progress->set_cancel_callback(); + wxEndBusyCursor(); + + finalize(); + + // dont do finalization again for the same process + m_finalized = true; + } + }); + } + + bool is_finalized() const { return m_finalized; } + + Job(const Job &) = delete; + Job(Job &&) = delete; + Job &operator=(const Job &) = delete; + Job &operator=(Job &&) = delete; + + virtual void process() = 0; + + void start() + { // Start the job. No effect if the job is already running + if (!m_running.load()) { + prepare(); + + // Save the current status indicatior range and push the new one + m_range = m_progress->get_range(); + m_progress->set_range(status_range()); + + // init cancellation flag and set the cancel callback + m_canceled.store(false); + m_progress->set_cancel_callback( + [this]() { m_canceled.store(true); }); + + m_finalized = false; + + // Changing cursor to busy + wxBeginBusyCursor(); + + try { // Execute the job + m_thread = create_thread([this] { this->run(); }); + } catch (std::exception &) { + update_status(status_range(), + _(L("ERROR: not enough resources to " + "execute a new job."))); + } + + // The state changes will be undone when the process hits the + // last status value, in the status update handler (see ctor) + } + } + + // To wait for the running job and join the threads. False is + // returned if the timeout has been reached and the job is still + // running. Call cancel() before this fn if you want to explicitly + // end the job. + bool join(int timeout_ms = 0) + { + if (!m_thread.joinable()) return true; + + if (timeout_ms <= 0) + m_thread.join(); + else if (!m_thread.try_join_for(boost::chrono::milliseconds(timeout_ms))) + return false; + + return true; + } + + bool is_running() const { return m_running.load(); } + void cancel() { m_canceled.store(true); } +}; + +} +} + +#endif // JOB_HPP diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index 9fc74bcb86..d1466ef0b3 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -58,7 +58,8 @@ DPIFrame(NULL, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, wxDEFAULT_FRAME_S #endif // _WIN32 // initialize status bar - m_statusbar.reset(new ProgressStatusBar(this)); + m_statusbar = std::make_shared(this); + m_statusbar->set_font(GUI::wxGetApp().normal_font()); m_statusbar->embed(this); m_statusbar->set_status_text(_(L("Version")) + " " + SLIC3R_VERSION + diff --git a/src/slic3r/GUI/MainFrame.hpp b/src/slic3r/GUI/MainFrame.hpp index 7b93e70cb5..3d4818eaf5 100644 --- a/src/slic3r/GUI/MainFrame.hpp +++ b/src/slic3r/GUI/MainFrame.hpp @@ -135,7 +135,7 @@ public: Plater* m_plater { nullptr }; wxNotebook* m_tabpanel { nullptr }; wxProgressDialog* m_progress_dialog { nullptr }; - std::unique_ptr m_statusbar; + std::shared_ptr m_statusbar; }; } // GUI diff --git a/src/slic3r/GUI/MeshUtils.cpp b/src/slic3r/GUI/MeshUtils.cpp index d344667420..3bf99f73c6 100644 --- a/src/slic3r/GUI/MeshUtils.cpp +++ b/src/slic3r/GUI/MeshUtils.cpp @@ -5,10 +5,6 @@ #include "slic3r/GUI/Camera.hpp" -// There is an L function in igl that would be overridden by our localization macro. -#undef L -#include - #include @@ -99,57 +95,6 @@ void MeshClipper::recalculate_triangles() } -class MeshRaycaster::AABBWrapper { -public: - AABBWrapper(const TriangleMesh* mesh); - ~AABBWrapper() { m_AABB.deinit(); } - - typedef Eigen::Map> MapMatrixXfUnaligned; - typedef Eigen::Map> MapMatrixXiUnaligned; - igl::AABB m_AABB; - - Vec3f get_hit_pos(const igl::Hit& hit) const; - Vec3f get_hit_normal(const igl::Hit& hit) const; - -private: - const TriangleMesh* m_mesh; -}; - -MeshRaycaster::AABBWrapper::AABBWrapper(const TriangleMesh* mesh) - : m_mesh(mesh) -{ - m_AABB.init( - MapMatrixXfUnaligned(m_mesh->its.vertices.front().data(), m_mesh->its.vertices.size(), 3), - MapMatrixXiUnaligned(m_mesh->its.indices.front().data(), m_mesh->its.indices.size(), 3)); -} - - -MeshRaycaster::MeshRaycaster(const TriangleMesh& mesh) - : m_AABB_wrapper(new AABBWrapper(&mesh)), m_mesh(&mesh) -{ -} - -// Define the default destructor here. This is needed for the PIMPL with -// unique_ptr to work, the AABBWrapper is complete here. -MeshRaycaster::~MeshRaycaster() = default; - -Vec3f MeshRaycaster::AABBWrapper::get_hit_pos(const igl::Hit& hit) const -{ - const stl_triangle_vertex_indices& indices = m_mesh->its.indices[hit.id]; - return Vec3f((1-hit.u-hit.v) * m_mesh->its.vertices[indices(0)] - + hit.u * m_mesh->its.vertices[indices(1)] - + hit.v * m_mesh->its.vertices[indices(2)]); -} - - -Vec3f MeshRaycaster::AABBWrapper::get_hit_normal(const igl::Hit& hit) const -{ - const stl_triangle_vertex_indices& indices = m_mesh->its.indices[hit.id]; - Vec3f a(m_mesh->its.vertices[indices(1)] - m_mesh->its.vertices[indices(0)]); - Vec3f b(m_mesh->its.vertices[indices(2)] - m_mesh->its.vertices[indices(0)]); - return Vec3f(a.cross(b)); -} - bool MeshRaycaster::unproject_on_mesh(const Vec2d& mouse_pos, const Transform3d& trafo, const Camera& camera, Vec3f& position, Vec3f& normal, const ClippingPlane* clipping_plane) const @@ -163,27 +108,20 @@ bool MeshRaycaster::unproject_on_mesh(const Vec2d& mouse_pos, const Transform3d& ::gluUnProject(mouse_pos(0), viewport[3] - mouse_pos(1), 0., model_mat.data(), proj_mat.data(), viewport.data(), &pt1(0), &pt1(1), &pt1(2)); ::gluUnProject(mouse_pos(0), viewport[3] - mouse_pos(1), 1., model_mat.data(), proj_mat.data(), viewport.data(), &pt2(0), &pt2(1), &pt2(2)); - std::vector hits; - Transform3d inv = trafo.inverse(); - pt1 = inv * pt1; pt2 = inv * pt2; - if (! m_AABB_wrapper->m_AABB.intersect_ray( - AABBWrapper::MapMatrixXfUnaligned(m_mesh->its.vertices.front().data(), m_mesh->its.vertices.size(), 3), - AABBWrapper::MapMatrixXiUnaligned(m_mesh->its.indices.front().data(), m_mesh->its.indices.size(), 3), - pt1.cast(), (pt2-pt1).cast(), hits)) + std::vector hits = m_emesh.query_ray_hits(pt1, pt2-pt1); + if (hits.empty()) return false; // no intersection found - std::sort(hits.begin(), hits.end(), [](const igl::Hit& a, const igl::Hit& b) { return a.t < b.t; }); - unsigned i = 0; // Remove points that are obscured or cut by the clipping plane if (clipping_plane) { for (i=0; iis_point_clipped(trafo * m_AABB_wrapper->get_hit_pos(hits[i]).cast())) + if (! clipping_plane->is_point_clipped(trafo * hits[i].position())) break; if (i==hits.size() || (hits.size()-i) % 2 != 0) { @@ -194,8 +132,8 @@ bool MeshRaycaster::unproject_on_mesh(const Vec2d& mouse_pos, const Transform3d& } // Now stuff the points in the provided vector and calculate normals if asked about them: - position = m_AABB_wrapper->get_hit_pos(hits[i]); - normal = m_AABB_wrapper->get_hit_normal(hits[i]); + position = hits[i].position().cast(); + normal = hits[i].normal().cast(); return true; } @@ -219,24 +157,21 @@ std::vector MeshRaycaster::get_unobscured_idxs(const Geometry::Transfo bool is_obscured = false; // Cast a ray in the direction of the camera and look for intersection with the mesh: - std::vector hits; + std::vector hits; // Offset the start of the ray by EPSILON to account for numerical inaccuracies. - if (m_AABB_wrapper->m_AABB.intersect_ray( - AABBWrapper::MapMatrixXfUnaligned(m_mesh->its.vertices.front().data(), m_mesh->its.vertices.size(), 3), - AABBWrapper::MapMatrixXiUnaligned(m_mesh->its.indices.front().data(), m_mesh->its.indices.size(), 3), - inverse_trafo * pt + direction_to_camera_mesh * EPSILON, direction_to_camera_mesh, hits)) { + hits = m_emesh.query_ray_hits((inverse_trafo * pt + direction_to_camera_mesh * EPSILON).cast(), + direction_to_camera.cast()); - std::sort(hits.begin(), hits.end(), [](const igl::Hit& h1, const igl::Hit& h2) { return h1.t < h2.t; }); + if (! hits.empty()) { // If the closest hit facet normal points in the same direction as the ray, // we are looking through the mesh and should therefore discard the point: - if (m_AABB_wrapper->get_hit_normal(hits.front()).dot(direction_to_camera_mesh) > 0.f) + if (hits.front().normal().dot(direction_to_camera_mesh.cast()) > 0) is_obscured = true; // Eradicate all hits that the caller wants to ignore for (unsigned j=0; jis_point_clipped(trafo.get_matrix() * m_AABB_wrapper->get_hit_pos(hit).cast())) { + if (clipping_plane && clipping_plane->is_point_clipped(trafo.get_matrix() * hits[j].position())) { hits.erase(hits.begin()+j); --j; } @@ -257,17 +192,15 @@ std::vector MeshRaycaster::get_unobscured_idxs(const Geometry::Transfo Vec3f MeshRaycaster::get_closest_point(const Vec3f& point, Vec3f* normal) const { int idx = 0; - Eigen::Matrix closest_point; - m_AABB_wrapper->m_AABB.squared_distance( - AABBWrapper::MapMatrixXfUnaligned(m_mesh->its.vertices.front().data(), m_mesh->its.vertices.size(), 3), - AABBWrapper::MapMatrixXiUnaligned(m_mesh->its.indices.front().data(), m_mesh->its.indices.size(), 3), - point, idx, closest_point); + Vec3d closest_point; + m_emesh.squared_distance(point.cast(), idx, closest_point); if (normal) { - igl::Hit imag_hit; - imag_hit.id = idx; - *normal = m_AABB_wrapper->get_hit_normal(imag_hit); + auto indices = m_emesh.F().row(idx); + Vec3d a(m_emesh.V().row(indices(1)) - m_emesh.V().row(indices(0))); + Vec3d b(m_emesh.V().row(indices(2)) - m_emesh.V().row(indices(0))); + *normal = Vec3f(a.cross(b).cast()); } - return closest_point; + return closest_point.cast(); } diff --git a/src/slic3r/GUI/MeshUtils.hpp b/src/slic3r/GUI/MeshUtils.hpp index d05cc42065..b4ad030114 100644 --- a/src/slic3r/GUI/MeshUtils.hpp +++ b/src/slic3r/GUI/MeshUtils.hpp @@ -3,6 +3,7 @@ #include "libslic3r/Point.hpp" #include "libslic3r/Geometry.hpp" +#include "libslic3r/SLA/EigenMesh3D.hpp" #include @@ -46,7 +47,7 @@ public: bool operator!=(const ClippingPlane& cp) const { return ! (*this==cp); } double distance(const Vec3d& pt) const { - assert(is_approx(get_normal().norm(), 1.)); + // FIXME: this fails: assert(is_approx(get_normal().norm(), 1.)); return (-get_normal().dot(pt) + m_data[3]); } @@ -104,11 +105,11 @@ private: // whether certain points are visible or obscured by the mesh etc. class MeshRaycaster { public: - // The class saves a const* to the mesh, called is responsible - // for making sure it does not get invalid. - MeshRaycaster(const TriangleMesh& mesh); - - ~MeshRaycaster(); + // The class makes a copy of the mesh as EigenMesh3D. + // The pointer can be invalidated after constructor returns. + MeshRaycaster(const TriangleMesh& mesh) + : m_emesh(mesh) + {} // Given a mouse position, this returns true in case it is on the mesh. bool unproject_on_mesh( @@ -136,10 +137,7 @@ public: Vec3f get_closest_point(const Vec3f& point, Vec3f* normal = nullptr) const; private: - // PIMPL wrapper around igl::AABB so I don't have to include the header-only IGL here - class AABBWrapper; - std::unique_ptr m_AABB_wrapper; - const TriangleMesh* m_mesh = nullptr; + sla::EigenMesh3D m_emesh; }; diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index f5df8fc4fd..6d869baaa2 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -39,11 +39,13 @@ #include "libslic3r/GCode/ThumbnailData.hpp" #endif // ENABLE_THUMBNAIL_GENERATOR #include "libslic3r/Model.hpp" +#include "libslic3r/SLA/Hollowing.hpp" +#include "libslic3r/SLA/Rotfinder.hpp" +#include "libslic3r/SLA/SupportPoint.hpp" #include "libslic3r/Polygon.hpp" #include "libslic3r/Print.hpp" #include "libslic3r/PrintConfig.hpp" #include "libslic3r/SLAPrint.hpp" -#include "libslic3r/SLA/SLARotfinder.hpp" #include "libslic3r/Utils.hpp" //#include "libslic3r/ClipperUtils.hpp" @@ -70,6 +72,7 @@ #include "Camera.hpp" #include "Mouse3DController.hpp" #include "Tab.hpp" +#include "Job.hpp" #include "PresetBundle.hpp" #include "BackgroundSlicingProcess.hpp" #include "ProgressStatusBar.hpp" @@ -1503,144 +1506,39 @@ struct Plater::priv // objects would be frozen for the user. In case of arrange, an animation // could be shown, or with the optimize orientations, partial results // could be displayed. - class Job : public wxEvtHandler + class PlaterJob: public Job { - int m_range = 100; - boost::thread m_thread; - priv * m_plater = nullptr; - std::atomic m_running{false}, m_canceled{false}; - bool m_finalized = false; - - void run() - { - m_running.store(true); - process(); - m_running.store(false); - - // ensure to call the last status to finalize the job - update_status(status_range(), ""); - } - + priv *m_plater; protected: - // status range for a particular job - virtual int status_range() const { return 100; } - - // status update, to be used from the work thread (process() method) - void update_status(int st, const wxString &msg = "") - { - auto evt = new wxThreadEvent(); - evt->SetInt(st); - evt->SetString(msg); - wxQueueEvent(this, evt); - } priv & plater() { return *m_plater; } const priv &plater() const { return *m_plater; } - bool was_canceled() const { return m_canceled.load(); } - - // Launched just before start(), a job can use it to prepare internals - virtual void prepare() {} // Launched when the job is finished. It refreshes the 3Dscene by def. - virtual void finalize() + void finalize() override { // Do a full refresh of scene tree, including regenerating // all the GLVolumes. FIXME The update function shall just // reload the modified matrices. - if (!was_canceled()) + if (!Job::was_canceled()) plater().update(unsigned(UpdateParams::FORCE_FULL_SCREEN_REFRESH)); + + Job::finalize(); } public: - Job(priv *_plater) : m_plater(_plater) - { - Bind(wxEVT_THREAD, [this](const wxThreadEvent &evt) { - auto msg = evt.GetString(); - if (!msg.empty()) - plater().statusbar()->set_status_text(msg); - - if (m_finalized) return; - - plater().statusbar()->set_progress(evt.GetInt()); - if (evt.GetInt() == status_range()) { - // set back the original range and cancel callback - plater().statusbar()->set_range(m_range); - plater().statusbar()->set_cancel_callback(); - wxEndBusyCursor(); - - finalize(); - - // dont do finalization again for the same process - m_finalized = true; - } - }); - } - - Job(const Job &) = delete; - Job(Job &&) = delete; - Job &operator=(const Job &) = delete; - Job &operator=(Job &&) = delete; - - virtual void process() = 0; - - void start() - { // Start the job. No effect if the job is already running - if (!m_running.load()) { - prepare(); - - // Save the current status indicatior range and push the new one - m_range = plater().statusbar()->get_range(); - plater().statusbar()->set_range(status_range()); - - // init cancellation flag and set the cancel callback - m_canceled.store(false); - plater().statusbar()->set_cancel_callback( - [this]() { m_canceled.store(true); }); - - m_finalized = false; - - // Changing cursor to busy - wxBeginBusyCursor(); - - try { // Execute the job - m_thread = create_thread([this] { this->run(); }); - } catch (std::exception &) { - update_status(status_range(), - _(L("ERROR: not enough resources to " - "execute a new job."))); - } - - // The state changes will be undone when the process hits the - // last status value, in the status update handler (see ctor) - } - } - - // To wait for the running job and join the threads. False is - // returned if the timeout has been reached and the job is still - // running. Call cancel() before this fn if you want to explicitly - // end the job. - bool join(int timeout_ms = 0) - { - if (!m_thread.joinable()) return true; - - if (timeout_ms <= 0) - m_thread.join(); - else if (!m_thread.try_join_for(boost::chrono::milliseconds(timeout_ms))) - return false; - - return true; - } - - bool is_running() const { return m_running.load(); } - void cancel() { m_canceled.store(true); } + PlaterJob(priv *_plater) + : Job(_plater->statusbar()), m_plater(_plater) + {} }; enum class Jobs : size_t { Arrange, - Rotoptimize + Rotoptimize, + Hollow }; - class ArrangeJob : public Job + class ArrangeJob : public PlaterJob { using ArrangePolygon = arrangement::ArrangePolygon; using ArrangePolygons = arrangement::ArrangePolygons; @@ -1757,7 +1655,7 @@ struct Plater::priv } public: - using Job::Job; + using PlaterJob::PlaterJob; int status_range() const override { return int(m_selected.size()); } @@ -1774,13 +1672,30 @@ struct Plater::priv } }; - class RotoptimizeJob : public Job + class RotoptimizeJob : public PlaterJob { public: - using Job::Job; + using PlaterJob::PlaterJob; void process() override; }; + class HollowJob : public PlaterJob + { + public: + using PlaterJob::PlaterJob; + void prepare() override; + void process() override; + void finalize() override; + private: + GLGizmoHollow * get_gizmo(); + const GLGizmoHollow * get_gizmo() const; + + std::unique_ptr m_output_mesh; + std::unique_ptr m_output_raycaster; + const TriangleMesh* m_object_mesh = nullptr; + sla::HollowingConfig m_cfg; + }; + // Jobs defined inside the group class will be managed so that only one can // run at a time. Also, the background process will be stopped if a job is // started. @@ -1792,6 +1707,7 @@ struct Plater::priv ArrangeJob arrange_job{m_plater}; RotoptimizeJob rotoptimize_job{m_plater}; + HollowJob hollow_job{m_plater}; // To create a new job, just define a new subclass of Job, implement // the process and the optional prepare() and finalize() methods @@ -1799,7 +1715,8 @@ struct Plater::priv // if it cannot run concurrently with other jobs in this group std::vector> m_jobs{arrange_job, - rotoptimize_job}; + rotoptimize_job, + hollow_job}; public: ExclusiveJobGroup(priv *_plater) : m_plater(_plater) {} @@ -1876,7 +1793,7 @@ struct Plater::priv void reset_all_gizmos(); void update_ui_from_settings(); - ProgressStatusBar* statusbar(); + std::shared_ptr statusbar(); std::string get_config(const std::string &key) const; BoundingBoxf bed_shape_bb() const; BoundingBox scaled_bed_shape_bb() const; @@ -1901,6 +1818,7 @@ struct Plater::priv void reset(); void mirror(Axis axis); void arrange(); + void hollow(); void sla_optimize_rotation(); void split_object(); void split_volume(); @@ -2291,9 +2209,9 @@ void Plater::priv::update_ui_from_settings() preview->get_canvas3d()->update_ui_from_settings(); } -ProgressStatusBar* Plater::priv::statusbar() +std::shared_ptr Plater::priv::statusbar() { - return main_frame->m_statusbar.get(); + return main_frame->m_statusbar; } std::string Plater::priv::get_config(const std::string &key) const @@ -2815,6 +2733,12 @@ void Plater::priv::arrange() m_ui_jobs.start(Jobs::Arrange); } +void Plater::priv::hollow() +{ + this->take_snapshot(_(L("Hollow"))); + m_ui_jobs.start(Jobs::Hollow); +} + // This method will find an optimal orientation for the currently selected item // Very similar in nature to the arrange method above... void Plater::priv::sla_optimize_rotation() { @@ -2946,12 +2870,74 @@ void Plater::priv::RotoptimizeJob::process() : _(L("Orientation found."))); } +void Plater::priv::HollowJob::prepare() +{ + const GLGizmosManager& gizmo_manager = plater().q->canvas3D()->get_gizmos_manager(); + const GLGizmoHollow* gizmo_hollow = dynamic_cast(gizmo_manager.get_current()); + assert(gizmo_hollow); + auto hlw_data = gizmo_hollow->get_hollowing_parameters(); + m_object_mesh = hlw_data.first; + m_cfg = hlw_data.second; + m_output_mesh.reset(); +} + +void Plater::priv::HollowJob::process() +{ + sla::JobController ctl; + ctl.stopcondition = [this]{ return was_canceled(); }; + ctl.statuscb = [this](unsigned st, const std::string &s) { + if (st < 100) update_status(int(st), s); + }; + + std::unique_ptr omesh = + sla::generate_interior(*m_object_mesh, m_cfg, ctl); + + if (omesh && !omesh->empty()) { + m_output_mesh.reset(new TriangleMesh{*m_object_mesh}); + m_output_mesh->merge(*omesh); + m_output_mesh->require_shared_vertices(); + + update_status(90, _(L("Indexing hollowed object"))); + + m_output_raycaster.reset(new MeshRaycaster(*m_output_mesh)); + + update_status(100, was_canceled() ? _(L("Hollowing cancelled.")) : + _(L("Hollowing done."))); + } else { + update_status(100, _(L("Hollowing failed."))); + } +} + +void Plater::priv::HollowJob::finalize() +{ + if (auto gizmo = get_gizmo()) { + gizmo->update_mesh_raycaster(std::move(m_output_raycaster)); + gizmo->update_hollowed_mesh(std::move(m_output_mesh)); + } +} + +GLGizmoHollow *Plater::priv::HollowJob::get_gizmo() +{ + const GLGizmosManager& gizmo_manager = plater().q->canvas3D()->get_gizmos_manager(); + auto ret = dynamic_cast(gizmo_manager.get_current()); + assert(ret); + return ret; +} + +const GLGizmoHollow *Plater::priv::HollowJob::get_gizmo() const +{ + const GLGizmosManager& gizmo_manager = plater().q->canvas3D()->get_gizmos_manager(); + auto ret = dynamic_cast(gizmo_manager.get_current()); + assert(ret); + return ret; +} + void Plater::priv::split_object() { int obj_idx = get_selected_object_idx(); if (obj_idx == -1) return; - + // we clone model object because split_object() adds the split volumes // into the same model object, thus causing duplicates when we call load_model_objects() Model new_model = model; @@ -5066,6 +5052,11 @@ void Plater::export_toolpaths_to_obj() const p->preview->get_canvas3d()->export_toolpaths_to_obj(into_u8(path).c_str()); } +void Plater::hollow() +{ + p->hollow(); +} + void Plater::reslice() { // Stop arrange and (or) optimize rotation tasks. diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp index 5747923057..c03813a9ca 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -194,6 +194,7 @@ public: void reload_from_disk(); bool has_toolpaths_to_export() const; void export_toolpaths_to_obj() const; + void hollow(); void reslice(); void reslice_SLA_supports(const ModelObject &object, bool postpone_error_messages = false); void changed_object(int obj_idx); diff --git a/src/slic3r/GUI/Preset.cpp b/src/slic3r/GUI/Preset.cpp index 081b886d1a..e22675869a 100644 --- a/src/slic3r/GUI/Preset.cpp +++ b/src/slic3r/GUI/Preset.cpp @@ -505,6 +505,10 @@ const std::vector& Preset::sla_print_options() "pad_object_connector_stride", "pad_object_connector_width", "pad_object_connector_penetration", + "hollowing_enable", + "hollowing_min_thickness", + "hollowing_quality", + "hollowing_closing_distance", "output_filename_format", "default_sla_print_profile", "compatible_printers", diff --git a/src/slic3r/GUI/ProgressIndicator.hpp b/src/slic3r/GUI/ProgressIndicator.hpp index 280ba63ac8..674a81ba26 100644 --- a/src/slic3r/GUI/ProgressIndicator.hpp +++ b/src/slic3r/GUI/ProgressIndicator.hpp @@ -11,58 +11,17 @@ namespace Slic3r { */ class ProgressIndicator { public: - using CancelFn = std::function; // Cancel function signature. - -private: - float m_state = .0f, m_max = 1.f, m_step; - CancelFn m_cancelfunc = [](){}; - -public: - - inline virtual ~ProgressIndicator() {} - - /// Get the maximum of the progress range. - float max() const { return m_max; } - - /// Get the current progress state - float state() const { return m_state; } - - /// Set the maximum of the progress range - virtual void max(float maxval) { m_max = maxval; } - - /// Set the current state of the progress. - virtual void state(float val) { m_state = val; } - - /** - * @brief Number of states int the progress. Can be used instead of giving a - * maximum value. - */ - virtual void states(unsigned statenum) { - m_step = m_max / statenum; - } - - /// Message shown on the next status update. - virtual void message(const std::string&) = 0; - - /// Title of the operation. - virtual void title(const std::string&) = 0; - - /// Formatted message for the next status update. Works just like sprintf. - virtual void message_fmt(const std::string& fmt, ...); - - /// Set up a cancel callback for the operation if feasible. - virtual void on_cancel(CancelFn func = CancelFn()) { m_cancelfunc = func; } - - /** - * Explicitly shut down the progress indicator and call the associated - * callback. - */ - virtual void cancel() { m_cancelfunc(); } - - /// Convenience function to call message and status update in one function. - void update(float st, const std::string& msg) { - message(msg); state(st); - } + + /// Cancel callback function type + using CancelFn = std::function; + + virtual ~ProgressIndicator() = default; + + virtual void set_range(int range) = 0; + virtual void set_cancel_callback(CancelFn = CancelFn()) = 0; + virtual void set_progress(int pr) = 0; + virtual void set_status_text(const char *) = 0; // utf8 char array + virtual int get_range() const = 0; }; } diff --git a/src/slic3r/GUI/ProgressStatusBar.cpp b/src/slic3r/GUI/ProgressStatusBar.cpp index 33e4855cdd..1ec2b81930 100644 --- a/src/slic3r/GUI/ProgressStatusBar.cpp +++ b/src/slic3r/GUI/ProgressStatusBar.cpp @@ -15,8 +15,7 @@ namespace Slic3r { ProgressStatusBar::ProgressStatusBar(wxWindow *parent, int id) - : self{new wxStatusBar(parent ? parent : GUI::wxGetApp().mainframe, - id == -1 ? wxID_ANY : id)} + : self{new wxStatusBar(parent, id == -1 ? wxID_ANY : id)} , m_prog{new wxGauge(self, wxGA_HORIZONTAL, 100, @@ -32,7 +31,6 @@ ProgressStatusBar::ProgressStatusBar(wxWindow *parent, int id) m_prog->Hide(); m_cancelbutton->Hide(); - self->SetFont(GUI::wxGetApp().normal_font()); self->SetFieldsCount(3); int w[] = {-1, 150, 155}; self->SetStatusWidths(3, w); @@ -149,8 +147,7 @@ void ProgressStatusBar::run(int rate) void ProgressStatusBar::embed(wxFrame *frame) { - wxFrame* mf = frame ? frame : GUI::wxGetApp().mainframe; - if(mf) mf->SetStatusBar(self); + if(frame) frame->SetStatusBar(self); } void ProgressStatusBar::set_status_text(const wxString& txt) @@ -173,6 +170,11 @@ wxString ProgressStatusBar::get_status_text() const return self->GetStatusText(); } +void ProgressStatusBar::set_font(const wxFont &font) +{ + self->SetFont(font); +} + void ProgressStatusBar::show_cancel_button() { if(m_cancelbutton) m_cancelbutton->Show(); diff --git a/src/slic3r/GUI/ProgressStatusBar.hpp b/src/slic3r/GUI/ProgressStatusBar.hpp index 1aab67d5aa..faeb7a34ef 100644 --- a/src/slic3r/GUI/ProgressStatusBar.hpp +++ b/src/slic3r/GUI/ProgressStatusBar.hpp @@ -6,6 +6,8 @@ #include #include +#include "ProgressIndicator.hpp" + class wxTimer; class wxGauge; class wxButton; @@ -14,6 +16,7 @@ class wxStatusBar; class wxWindow; class wxFrame; class wxString; +class wxFont; namespace Slic3r { @@ -22,7 +25,7 @@ namespace Slic3r { * of the Slicer main window. It consists of a message area to the left and a * progress indication area to the right with an optional cancel button. */ -class ProgressStatusBar +class ProgressStatusBar : public ProgressIndicator { wxStatusBar *self; // we cheat! It should be the base class but: perl! wxGauge *m_prog; @@ -30,30 +33,29 @@ class ProgressStatusBar std::unique_ptr m_timer; public: - /// Cancel callback function type - using CancelFn = std::function; ProgressStatusBar(wxWindow *parent = nullptr, int id = -1); - ~ProgressStatusBar(); + ~ProgressStatusBar() override; int get_progress() const; // if the argument is less than 0 it shows the last state or // pulses if no state was set before. - void set_progress(int); - int get_range() const; - void set_range(int = 100); + void set_progress(int) override; + int get_range() const override; + void set_range(int = 100) override; void show_progress(bool); void start_busy(int = 100); void stop_busy(); inline bool is_busy() const { return m_busy; } - void set_cancel_callback(CancelFn = CancelFn()); + void set_cancel_callback(CancelFn = CancelFn()) override; inline void reset_cancel_callback() { set_cancel_callback(); } void run(int rate); void embed(wxFrame *frame = nullptr); void set_status_text(const wxString& txt); void set_status_text(const std::string& txt); - void set_status_text(const char *txt); + void set_status_text(const char *txt) override; wxString get_status_text() const; + void set_font(const wxFont &font); // Temporary methods to satisfy Perl side void show_cancel_button(); diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index e2f33f6dbd..a34a3e0b75 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -3608,6 +3608,13 @@ void TabSLAPrint::build() optgroup->append_single_option_line("pad_object_connector_stride"); optgroup->append_single_option_line("pad_object_connector_width"); optgroup->append_single_option_line("pad_object_connector_penetration"); + + page = add_options_page(_(L("Hollowing")), "hollowing"); + optgroup = page->new_optgroup(_(L("Hollowing"))); + optgroup->append_single_option_line("hollowing_enable"); + optgroup->append_single_option_line("hollowing_min_thickness"); + optgroup->append_single_option_line("hollowing_quality"); + optgroup->append_single_option_line("hollowing_closing_distance"); page = add_options_page(_(L("Advanced")), "wrench"); optgroup = page->new_optgroup(_(L("Slicing"))); diff --git a/tests/data/extruder_idler_quads.obj b/tests/data/extruder_idler_quads.obj new file mode 100644 index 0000000000..266cb242d1 --- /dev/null +++ b/tests/data/extruder_idler_quads.obj @@ -0,0 +1,7348 @@ +#### +# +# OBJ File Generated by Meshlab +# +#### +# Object extruder_idler_quads.obj +# +# Vertices: 3660 +# Faces: 3672 +# +#### +v 61.288807 37.917557 6.068595 +v 61.312790 37.917618 6.193261 +v 61.356049 37.917774 6.504920 +v 46.677547 37.917866 9.519013 +v 55.420834 37.917618 7.326775 +v 55.396851 37.917557 7.202109 +v 70.876183 37.682098 2.970614 +v 46.216084 37.450668 7.120349 +v 61.171124 37.838562 5.456883 +v 70.404060 35.864510 0.516525 +v 70.345604 35.376511 0.212692 +v 54.692173 35.864510 3.539228 +v 54.633720 35.376511 3.235396 +v 61.171051 37.731289 7.795076 +v 55.864639 37.839470 8.386023 +v 55.279167 37.838562 6.590397 +v 46.326366 37.682098 7.693588 +v 70.566055 36.778572 1.358596 +v 70.661987 37.147907 1.857247 +v 54.854176 36.778572 4.381299 +v 46.112171 37.147907 6.580221 +v 55.054016 37.450668 5.420078 +v 60.659683 36.348526 2.798444 +v 54.767727 36.348526 3.931957 +v 45.929794 36.348526 5.632228 +v 50.387226 39.697979 28.801762 +v 50.382210 39.699635 28.775690 +v 48.877922 40.196674 20.956472 +v 48.879765 39.946739 20.966045 +v 48.897690 38.450691 21.059217 +v 48.672623 38.559273 19.889320 +v 48.627918 34.768574 19.656961 +v 48.967503 37.518620 21.422112 +v 49.148136 36.364048 22.361021 +v 49.308113 35.827209 23.192574 +v 49.224934 36.068989 22.760210 +v 50.379288 38.947979 28.760509 +v 50.332268 37.976688 28.516096 +v 50.291908 37.522064 28.306307 +v 50.557526 37.884193 29.686979 +v 49.863396 35.643738 26.078924 +v 49.771500 35.518578 25.601242 +v 50.513439 34.145573 29.457813 +v 46.316734 33.337154 7.643516 +v 45.795788 35.376511 4.935667 +v 46.366432 33.896751 7.901852 +v 45.854240 35.864510 5.239500 +v 46.306992 32.926773 7.592876 +v 46.316822 32.516430 7.643979 +v 46.420128 31.655691 8.180969 +v 46.451801 31.534010 8.345595 +v 46.486115 31.434301 8.523966 +v 46.599277 31.280785 9.112178 +v 46.099930 28.231632 6.516577 +v 46.638424 31.280876 9.315653 +v 47.221455 27.861065 12.346222 +v 46.715168 31.358620 9.714571 +v 46.785843 31.534855 10.081941 +v 47.732357 31.056692 15.001865 +v 46.930408 32.928387 10.833368 +v 46.927967 32.721592 10.820692 +v 47.124451 37.150532 11.842004 +v 47.220444 36.781719 12.340965 +v 46.817513 31.656748 10.246544 +v 46.870964 31.958406 10.524391 +v 46.891937 32.133430 10.633411 +v 47.306953 36.352104 12.790643 +v 46.391373 34.056538 8.031486 +v 46.016239 36.778572 6.081570 +v 46.441235 37.838562 8.290668 +v 46.598972 34.574272 9.110590 +v 46.560146 34.548199 8.908764 +v 46.558918 37.917557 8.902380 +v 46.714870 34.497074 9.713024 +v 47.020462 37.452759 11.301476 +v 46.910164 37.683620 10.728139 +v 46.870785 33.898132 10.523453 +v 46.891777 33.723171 10.632566 +v 46.751282 34.420887 9.902280 +v 51.630699 27.764736 35.265278 +v 52.147446 28.245863 37.951313 +v 46.795250 37.839470 10.130821 +v 71.227364 37.917866 4.796038 +v 76.205231 29.860657 30.670778 +v 70.878639 33.534252 2.983394 +v 70.916252 33.896751 3.178878 +v 70.969704 34.198410 3.456725 +v 71.001366 34.320305 3.621328 +v 70.991051 37.838562 3.567693 +v 71.108734 37.917557 4.179405 +v 71.109962 34.548199 4.185790 +v 71.226776 34.548508 4.792975 +v 71.345062 37.839470 5.407847 +v 71.459976 37.683620 6.005164 +v 71.301102 34.420887 5.179305 +v 71.441589 33.723171 5.909592 +v 70.479614 36.348526 0.909254 +v 70.866547 33.337154 2.920542 +v 70.765900 37.450668 2.397374 +v 70.916428 31.957125 3.179823 +v 70.304688 31.906664 0.000000 +v 71.001617 31.534010 3.622621 +v 71.035934 31.434301 3.800991 +v 71.149094 31.280785 4.389203 +v 71.110252 31.306643 4.187318 +v 71.227066 31.306923 4.794502 +v 71.301376 31.434967 5.180758 +v 71.264984 31.358620 4.991596 +v 71.458290 33.535702 5.996392 +v 71.477760 33.135204 6.097586 +v 71.480225 32.928387 6.110393 +v 72.579239 34.928131 11.823022 +v 73.058540 33.686779 14.314425 +v 71.367325 31.656748 5.523570 +v 73.568642 32.857552 16.965889 +v 73.831314 32.604336 18.331276 +v 75.124626 28.113617 25.053829 +v 74.363899 32.428364 21.099625 +v 74.895103 32.695507 23.860798 +v 75.156105 32.993713 25.217482 +v 75.149338 30.209538 25.182302 +v 76.679512 26.740294 33.136051 +v 76.697266 28.245863 33.228336 +v 76.180519 27.764736 30.542305 +v 71.996040 35.338497 8.791578 +v 72.012505 35.153580 8.877157 +v 56.978554 37.452759 9.385705 +v 57.088562 37.406780 9.449879 +v 71.674271 37.150532 7.119029 +v 60.551853 37.406780 8.783600 +v 59.598507 37.150532 9.442204 +v 47.382626 35.868492 13.183980 +v 71.856773 36.352104 8.067668 +v 71.770264 36.781719 7.617991 +v 59.439774 37.126163 9.506907 +v 58.300529 37.132675 9.716898 +v 58.834583 37.093670 9.668919 +v 71.570282 37.452759 6.578501 +v 71.932442 35.868492 8.461005 +v 47.446220 35.338497 13.514553 +v 44.992771 27.189064 13.242427 +v 51.780609 26.868217 17.170813 +v 61.232605 27.449877 5.863266 +v 58.929241 27.283329 9.023430 +v 58.809258 27.282513 9.059827 +v 58.359135 27.139223 11.484049 +v 58.309364 27.285778 9.102742 +v 55.616589 27.411655 7.567233 +v 47.938751 27.189064 12.675670 +v 54.076530 26.062111 29.879890 +v 54.026432 26.058466 29.948986 +v 53.712753 26.029385 30.483768 +v 53.552719 25.999935 30.994987 +v 53.514042 25.981487 31.303373 +v 53.514153 25.976261 31.388626 +v 50.928787 26.201115 28.217762 +v 53.514442 25.962425 31.614302 +v 68.806427 26.062111 27.046103 +v 68.623718 26.048828 27.297941 +v 68.756279 26.058466 27.115208 +v 68.515053 26.037975 27.495895 +v 62.093616 26.073893 28.145342 +v 68.338547 26.012682 27.942495 +v 68.254539 25.986547 28.385019 +v 68.244034 25.976261 28.554842 +v 68.218460 26.326206 22.850769 +v 73.165169 25.937754 28.236309 +v 73.194099 25.909863 28.685766 +v 73.183594 25.899576 28.855589 +v 72.995468 25.856739 29.590630 +v 72.923080 25.848146 29.744713 +v 72.631706 25.824013 30.194506 +v 71.988434 25.798077 30.741371 +v 71.553574 25.790627 30.946589 +v 71.338646 25.788862 31.016699 +v 70.807259 25.790627 31.090166 +v 70.411186 25.796120 31.076721 +v 70.327293 25.798077 31.060946 +v 70.105293 25.803261 31.019094 +v 58.193188 25.848146 32.578499 +v 68.283722 25.943062 29.088823 +v 68.361572 25.923698 29.389732 +v 68.626915 25.886189 29.950634 +v 68.759964 25.873442 30.132986 +v 58.265583 25.856739 32.424416 +v 68.965179 25.856739 30.365990 +v 69.023651 25.852247 30.428022 +v 69.089577 25.848146 30.482216 +v 69.263924 25.837296 30.625723 +v 69.527039 25.824013 30.791792 +v 69.604355 25.820885 30.827938 +v 68.476524 25.904634 29.678623 +v 58.435280 25.937754 31.070093 +v 58.463875 25.923698 31.293884 +v 58.464321 25.904634 31.604805 +v 58.369694 25.873442 32.131901 +v 68.810265 25.868631 30.201771 +v 69.454826 25.827658 30.746227 +v 57.951962 25.827658 32.959187 +v 69.808907 25.812611 30.923559 +v 57.688858 25.812611 33.255257 +v 70.023972 25.805826 30.992867 +v 57.514957 25.805826 33.399391 +v 57.109299 25.794794 33.657429 +v 56.904839 25.791292 33.753883 +v 56.823685 25.790627 33.780373 +v 56.608757 25.788862 33.850483 +v 56.302967 25.788862 33.909313 +v 56.077370 25.790627 33.923950 +v 55.375401 25.803261 33.852879 +v 54.874439 25.820885 33.661728 +v 54.080334 25.868631 33.035564 +v 52.746826 25.600410 37.667839 +v 54.359653 25.848146 33.316006 +v 54.533997 25.837296 33.459515 +v 54.797138 25.824013 33.625580 +v 55.079014 25.812611 33.757343 +v 55.766525 25.794794 33.915756 +v 55.681297 25.796120 33.910507 +v 73.076561 25.962425 27.850878 +v 72.993141 25.976261 27.641191 +v 72.811218 25.999935 27.289974 +v 71.911095 26.062111 26.448816 +v 71.833778 26.065239 26.412672 +v 71.629128 26.073511 26.317068 +v 70.716064 26.094831 26.144928 +v 70.630875 26.095497 26.150444 +v 69.598946 26.091330 26.416964 +v 69.521751 26.090002 26.453463 +v 66.578064 26.868217 14.324030 +v 66.011253 26.886908 14.128163 +v 60.892159 27.567686 4.006828 +v 61.173477 27.469414 5.555912 +v 61.073132 26.073893 28.341667 +v 58.403152 25.948370 30.903088 +v 58.263256 25.976261 30.474977 +v 58.081326 25.999935 30.123760 +v 57.948280 26.012682 29.941408 +v 57.618668 26.037975 29.592178 +v 57.684593 26.033876 29.646372 +v 57.444317 26.048828 29.448671 +v 57.181206 26.062111 29.282602 +v 57.103886 26.065239 29.246456 +v 56.684273 26.080296 29.081528 +v 56.899239 26.073511 29.150852 +v 55.986176 26.094831 28.978714 +v 55.675388 26.097260 28.998867 +v 54.869053 26.091330 29.250750 +v 54.791859 26.090002 29.287249 +v 54.719810 26.088047 29.333023 +v 54.463398 26.080296 29.508787 +v 53.421013 26.326206 25.697552 +v 54.529179 26.082863 29.454258 +v 67.902382 34.970417 12.655975 +v 47.462685 35.153580 13.600132 +v 54.822617 34.805954 17.855391 +v 58.905815 34.974163 14.325680 +v 54.142002 34.777916 18.443687 +v 56.180191 34.357391 24.912003 +v 64.640221 42.441532 22.165798 +v 64.498436 42.393948 21.428791 +v 64.108391 38.000809 19.401363 +v 65.105515 36.364048 19.291088 +v 64.965973 41.759834 23.859028 +v 65.182312 36.068989 19.690277 +v 64.884438 37.973030 18.141928 +v 64.924889 37.518620 18.352179 +v 64.831230 39.445396 17.865381 +v 64.038155 39.445889 19.036266 +v 65.353737 35.642532 20.581331 +v 65.445679 35.517838 21.059244 +v 65.633987 43.439590 22.038057 +v 64.077850 40.550362 19.242599 +v 64.837143 39.946739 17.896112 +v 65.265495 35.827209 20.122641 +v 64.330994 39.446640 20.558413 +v 64.055885 40.191998 19.128443 +v 64.350174 39.980480 20.658138 +v 64.243858 37.134884 20.105499 +v 64.108124 40.891331 19.399967 +v 64.364799 40.145279 20.734161 +v 64.042656 39.069904 19.059660 +v 64.078056 38.341614 19.243677 +v 64.339630 39.086071 20.603313 +v 64.350266 38.912914 20.658621 +v 64.405319 38.454250 20.944765 +v 64.488396 38.135056 21.376616 +v 64.430038 36.593735 21.073254 +v 64.301460 36.913589 20.404915 +v 64.520393 38.068111 21.542925 +v 64.587852 38.000175 21.893555 +v 64.569603 36.453186 21.798729 +v 64.640778 36.453377 22.168688 +v 64.711395 36.500771 22.535736 +v 64.721664 38.135670 22.589117 +v 64.846489 36.733505 23.237947 +v 64.804688 38.455292 23.020670 +v 64.826614 38.595646 23.134649 +v 65.063599 37.685196 24.366478 +v 64.845032 38.749439 23.230366 +v 64.243446 41.757961 20.103352 +v 64.405144 40.439426 20.943855 +v 64.300987 41.979553 20.402464 +v 64.458015 40.671249 21.218702 +v 64.488167 40.759056 21.375410 +v 64.429504 42.300076 21.070503 +v 64.553452 40.871548 21.714767 +v 64.569054 42.441341 21.795839 +v 64.656105 40.871815 22.248354 +v 64.689438 40.826614 22.421604 +v 64.845985 42.162468 23.235317 +v 64.908371 41.981129 23.559612 +v 64.804512 40.440468 23.019760 +v 64.844902 40.146526 23.229715 +v 64.859566 39.981804 23.305906 +v 64.876671 39.629799 23.394819 +v 64.751572 40.672024 22.744589 +v 64.779427 40.565052 22.889357 +v 65.101707 38.003387 24.564560 +v 65.153946 38.702721 24.836084 +v 65.131981 38.344357 24.721928 +v 65.167244 39.072823 24.905212 +v 65.153801 40.194855 24.835358 +v 64.363846 36.732250 20.729210 +v 64.966385 37.136757 23.861176 +v 66.068886 36.366562 24.298624 +v 65.820778 35.643738 23.008991 +v 66.289383 40.921688 25.444761 +v 66.318748 40.444027 25.597404 +v 64.975334 41.796913 18.614428 +v 64.146233 41.209522 19.598049 +v 64.363342 42.161217 20.726580 +v 65.728134 43.376877 22.527445 +v 64.779793 42.300983 22.891273 +v 64.056030 38.699863 19.129168 +v 64.975761 37.094627 18.616638 +v 64.146568 37.682812 19.599808 +v 64.191963 37.392635 19.835770 +v 64.042587 39.821896 19.059315 +v 64.498993 36.500225 21.431665 +v 64.780319 36.594643 22.894024 +v 66.137520 42.186985 24.655401 +v 65.101440 40.893909 24.563162 +v 64.191582 41.499920 19.833786 +v 65.035782 42.184120 18.928610 +v 64.924530 41.372654 18.350315 +v 65.353035 43.250977 20.577698 +v 65.991501 42.825726 23.896412 +v 66.068298 42.530670 24.295601 +v 64.710838 42.394493 22.532862 +v 65.171677 39.448830 24.928261 +v 66.336670 38.947979 25.690577 +v 65.017868 41.502083 24.128757 +v 65.063263 41.211906 24.364719 +v 66.318924 38.454502 25.598343 +v 65.018250 37.394798 24.130741 +v 64.908844 36.915165 23.562061 +v 65.992126 36.071102 23.899647 +v 65.634720 35.455376 22.041878 +v 65.131775 40.553104 24.720850 +v 65.167175 39.824814 24.904867 +v 66.336571 39.950649 25.690052 +v 65.539093 43.439342 21.544811 +v 68.963753 42.184120 18.172934 +v 65.264824 43.065830 20.119164 +v 65.181694 42.823616 19.687042 +v 68.852501 41.372654 17.594639 +v 68.782860 40.440216 17.232672 +v 64.884171 40.918030 18.140526 +v 70.264542 39.950649 24.934376 +v 70.267563 39.699635 24.950079 +v 65.444939 43.376144 21.055380 +v 65.104935 42.528164 19.288065 +v 69.032906 42.528164 18.532389 +v 64.854889 40.440216 17.988346 +v 68.903305 41.796913 17.858751 +v 69.748047 43.252190 22.249683 +v 65.820076 43.252190 23.005358 +v 65.908325 43.067505 23.464048 +v 70.217354 40.921688 24.689085 +v 70.246719 40.444027 24.841728 +v 66.198051 41.800091 24.970051 +v 70.065491 42.186985 23.899725 +v 66.248932 41.376099 25.234510 +v 70.176903 41.376099 24.478834 +v 65.539833 35.455128 21.548632 +v 65.733398 35.391171 22.554787 +v 65.737389 35.300861 22.575521 +v 65.728882 35.518578 22.531309 +v 65.908989 35.828888 23.467525 +v 66.514908 37.884193 26.617046 +v 66.289650 37.976688 25.446163 +v 66.249290 37.522064 25.236374 +v 66.138039 36.710598 24.658079 +v 66.198479 37.097805 24.972261 +v 66.339592 39.699635 25.705755 +v 66.342583 39.449322 25.721308 +v 64.837250 38.944069 17.896637 +v 64.855072 38.450691 17.989285 +v 65.036293 36.707745 18.931288 +v 64.597069 35.766628 16.648207 +v 64.835304 40.196674 17.886539 +v 70.272575 39.697979 24.976152 +v 68.773964 40.193138 17.186436 +v 64.845993 40.193138 17.942112 +v 69.192795 43.065830 19.363487 +v 69.274765 40.145279 19.789566 +v 69.315109 40.439426 19.999262 +v 69.367981 40.671249 20.274107 +v 69.398132 40.759056 20.430815 +v 69.467064 43.439342 20.789135 +v 69.372910 43.376144 20.299704 +v 69.430099 40.826176 20.596979 +v 69.497551 40.894455 20.947596 +v 69.566071 40.871815 21.303759 +v 69.531944 40.894543 21.126377 +v 69.561958 43.439590 21.282381 +v 69.656105 43.376877 21.771769 +v 69.599403 40.826614 21.477009 +v 69.836296 43.067505 22.708372 +v 69.919472 42.825726 23.140736 +v 69.689392 40.565052 21.944761 +v 69.996269 42.530670 23.539925 +v 69.714478 40.440468 22.075167 +v 69.754868 40.146526 22.285120 +v 70.126022 41.800091 24.214375 +v 69.736404 40.300220 22.189165 +v 68.763275 40.196674 17.130863 +v 68.812134 40.918030 17.384850 +v 69.249519 39.807266 19.658356 +v 69.243126 39.264919 19.625113 +v 69.109665 42.823616 18.931366 +v 69.281006 43.250977 19.822021 +v 69.240952 39.446640 19.613817 +v 69.315285 38.454250 20.000172 +v 69.340370 38.329666 20.130575 +v 68.904175 32.857552 17.863255 +v 68.646049 33.219078 16.521528 +v 68.513268 34.768574 15.831351 +v 69.368217 38.222706 20.275343 +v 69.166855 32.604336 19.228642 +v 69.699440 32.428364 21.996990 +v 69.566338 38.023182 21.305166 +v 70.442879 37.884193 25.861370 +v 69.736580 38.595646 22.190054 +v 69.769524 39.981804 22.361311 +v 69.769615 38.914249 22.361794 +v 69.780167 39.808659 22.416618 +v 69.786629 39.629799 22.450226 +v 70.230637 32.695507 24.758162 +v 67.914772 34.928131 12.720387 +v 68.394073 33.686779 15.211790 +v 66.470818 34.145573 26.387878 +v 64.642441 35.766621 16.639477 +v 64.630005 38.559273 16.819387 +v 44.275478 27.861065 12.912979 +v 46.509041 38.329666 24.522942 +v 46.462021 38.594498 24.278538 +v 46.418194 39.807266 24.050722 +v 45.931946 40.196674 21.523230 +v 46.428902 38.912914 24.106392 +v 45.951530 40.440216 21.625038 +v 45.942635 40.193138 21.578802 +v 46.443562 38.748192 24.182585 +v 46.483952 38.454250 24.392538 +v 46.567032 38.135056 24.824387 +v 47.555691 33.147514 29.963390 +v 46.800297 38.135670 26.036888 +v 46.948906 39.087452 26.809347 +v 47.088142 42.825726 27.533102 +v 46.938290 38.914249 26.754160 +v 47.234161 42.186985 28.292091 +v 47.345573 41.376099 28.871201 +v 46.883320 38.455292 26.468443 +v 47.611549 37.884193 30.253736 +v 47.415386 40.444027 29.234095 +v 46.957474 39.448078 26.853886 +v 47.441246 39.697979 29.368519 +v 46.132423 42.184120 22.565300 +v 46.566799 40.759056 24.823181 +v 46.598766 40.826176 24.989346 +v 46.632088 40.871548 25.162539 +v 46.541576 43.376144 24.692070 +v 46.666222 40.894455 25.339962 +v 46.700615 40.894543 25.518744 +v 46.734741 40.871815 25.696125 +v 46.858059 40.565052 26.337128 +v 46.905079 40.300220 26.581532 +v 46.923538 40.146526 26.677486 +v 46.948837 39.808659 26.808985 +v 47.004963 43.067505 27.100739 +v 45.655373 32.515556 20.085615 +v 48.601349 32.515556 19.518858 +v 46.635735 43.439342 25.181501 +v 49.581715 43.439342 24.614744 +v 50.034122 42.825726 26.966345 +v 46.361465 43.065830 23.755854 +v 46.449680 43.250977 24.214388 +v 48.967144 41.372654 21.420248 +v 48.926785 40.918030 21.210459 +v 45.980808 40.918030 21.777216 +v 49.017956 41.796913 21.684361 +v 46.824780 43.376877 26.164135 +v 47.433212 39.950649 29.326742 +v 47.436234 39.699635 29.342445 +v 49.487556 43.376144 24.125313 +v 46.201576 42.528164 22.924755 +v 46.278332 42.823616 23.323732 +v 48.897511 40.440216 21.058281 +v 48.888615 40.193138 21.012045 +v 46.071980 41.796913 22.251118 +v 46.021168 41.372654 21.987005 +v 46.730629 43.439590 25.674747 +v 47.164940 42.530670 27.932291 +v 46.916721 43.252190 26.642050 +v 49.950943 43.067505 26.533981 +v 50.361366 40.444027 28.667337 +v 50.379189 39.950649 28.759985 +v 50.180138 42.186985 27.725334 +v 50.240673 41.800091 28.039984 +v 47.294693 41.800091 28.606741 +v 50.291550 41.376099 28.304443 +v 47.386021 40.921688 29.081451 +v 50.331997 40.921688 28.514694 +v 49.676605 43.439590 25.107990 +v 49.677341 35.455376 25.111813 +v 49.582447 35.455128 24.618565 +v 49.862698 43.252190 26.075293 +v 50.607594 36.733505 25.977272 +v 50.541428 36.594643 25.633348 +v 49.396355 35.642532 23.651264 +v 50.669476 41.981129 26.298937 +v 50.094238 39.628387 23.308878 +v 49.799259 39.445889 21.775591 +v 49.838955 40.550362 21.981924 +v 49.869232 40.891331 22.139292 +v 50.144321 40.299072 23.569202 +v 50.166248 40.439426 23.683182 +v 50.004551 41.757961 22.842676 +v 49.817135 38.699863 21.868494 +v 50.219124 40.671249 23.958029 +v 50.100666 39.807266 23.342276 +v 50.111282 39.980480 23.397465 +v 50.249271 40.759056 24.114737 +v 50.062096 41.979553 23.141790 +v 50.191307 40.564144 23.813421 +v 50.281239 40.826176 24.280899 +v 50.190613 42.300076 23.809828 +v 50.314560 40.871548 24.454092 +v 50.401329 42.441532 24.905123 +v 50.417213 40.871815 24.987679 +v 50.471947 42.394493 25.272188 +v 50.450542 40.826614 25.160929 +v 50.482540 40.759663 25.327238 +v 50.540531 40.565052 25.628683 +v 50.862545 40.893909 27.302488 +v 50.824368 41.211906 27.104046 +v 49.839165 38.341614 21.983002 +v 50.111374 38.912914 23.397947 +v 49.869499 38.000809 22.140690 +v 50.004963 37.134884 22.844826 +v 50.191513 38.329666 23.814495 +v 50.166424 38.454250 23.684092 +v 50.219364 38.222706 23.959265 +v 50.281502 38.068111 24.282249 +v 50.260098 36.500225 24.170990 +v 50.383350 38.000263 24.811661 +v 50.401886 36.453377 24.908012 +v 50.330711 36.453186 24.538054 +v 50.472500 36.500771 25.275061 +v 50.450802 38.068542 25.162279 +v 50.482769 38.135670 25.328442 +v 50.669949 36.915165 26.301388 +v 50.540737 38.330574 25.629755 +v 50.892879 40.553104 27.460176 +v 50.637775 39.629799 26.134146 +v 50.779354 37.394798 26.870068 +v 50.914909 40.194855 27.574684 +v 50.587723 38.595646 25.873976 +v 50.862812 38.003387 27.303885 +v 50.620762 38.914249 26.045713 +v 50.631378 39.087452 26.100901 +v 50.932781 39.448830 27.667585 +v 50.727493 37.136757 26.600500 +v 50.111500 36.366562 27.368559 +v 50.361546 38.454502 28.668276 +v 50.893085 38.344357 27.461254 +v 50.915051 38.702721 27.575411 +v 49.907337 41.209522 22.337376 +v 48.879868 38.944069 20.966570 +v 49.953072 37.392635 22.575094 +v 49.907677 37.682812 22.339132 +v 49.078915 36.707745 22.001223 +v 50.062565 36.913589 23.144239 +v 50.191143 36.593735 23.812578 +v 50.124954 36.732250 23.468536 +v 49.770756 43.376877 25.597380 +v 50.607086 42.162468 25.974642 +v 50.540901 42.300983 25.630598 +v 50.778973 41.502083 26.868084 +v 50.034744 36.071102 26.969580 +v 49.951611 35.828888 26.537458 +v 50.180653 36.710598 27.728012 +v 50.928349 39.072823 27.644539 +v 49.078400 42.184120 21.998545 +v 48.927055 37.973030 21.211861 +v 49.018383 37.094627 21.686571 +v 49.488297 35.517838 24.129177 +v 50.110920 42.530670 27.365534 +v 50.727081 41.759834 26.598352 +v 50.928280 39.824814 27.644192 +v 50.385201 39.449322 28.791241 +v 50.824703 37.685196 27.105803 +v 50.241096 37.097805 28.042196 +v 49.803696 39.821896 21.798639 +v 48.873852 39.445396 20.935314 +v 49.307442 43.065830 23.189098 +v 49.952690 41.499920 22.573111 +v 49.803761 39.069904 21.798986 +v 49.395657 43.250977 23.647631 +v 49.147552 42.528164 22.357998 +v 49.224308 42.823616 22.756975 +v 50.124451 42.161217 23.465906 +v 49.816994 40.191998 21.867767 +v 50.330158 42.441341 24.535164 +v 50.259544 42.393948 24.168116 +v 68.149811 34.257607 13.942123 +v 72.566849 34.970417 11.758610 +v 72.814278 34.257607 13.044758 +v 73.310516 33.219078 15.624163 +v 74.630486 32.506702 22.485336 +v 69.432487 32.460957 20.609421 +v 74.096954 32.460957 19.712055 +v 69.966026 32.506702 23.382702 +v 70.491646 32.993713 26.114845 +v 64.621979 40.894543 22.070972 +v 64.587585 40.894455 21.892191 +v 50.348694 40.894455 24.631517 +v 50.383087 40.894543 24.810297 +v 69.788803 39.448078 22.461519 +v 46.955303 39.629799 26.842592 +v 69.340164 40.564144 20.129501 +v 64.430199 40.564144 21.074097 +v 46.483776 40.439426 24.391628 +v 64.430405 38.329666 21.075171 +v 64.364929 38.748192 20.734812 +v 69.274887 38.748192 19.790218 +v 50.126034 38.748192 23.474138 +v 69.661537 40.672024 21.799994 +v 64.339561 39.807266 20.602951 +v 69.243095 39.628387 19.624958 +v 46.411766 39.628387 24.017324 +v 46.443436 40.145279 24.181932 +v 64.458260 38.222706 21.219938 +v 69.398361 38.135056 20.432020 +v 50.249504 38.135056 24.115940 +v 46.536892 38.222706 24.667709 +v 69.463684 38.022903 20.771578 +v 69.430359 38.068111 20.598330 +v 50.314831 38.022903 24.455498 +v 69.293350 38.594498 19.886171 +v 50.144493 38.594498 23.570091 +v 69.249596 39.086071 19.658720 +v 69.260231 38.912914 19.714025 +v 50.100735 39.086071 23.342640 +v 64.870201 39.808659 23.361214 +v 64.520134 40.826176 21.541573 +v 69.463417 40.871548 20.770172 +v 46.508835 40.564144 24.521868 +v 46.536652 40.671249 24.666473 +v 64.333130 39.628387 20.569553 +v 46.409622 39.446640 24.006184 +v 50.092098 39.446640 23.297737 +v 69.260139 39.980480 19.713545 +v 50.125908 40.145279 23.473488 +v 46.428810 39.980480 24.105911 +v 64.383217 40.299072 20.829878 +v 69.293182 40.299072 19.885283 +v 46.461849 40.299072 24.277649 +v 46.599026 38.068111 24.990696 +v 64.553726 38.022903 21.716173 +v 50.348957 38.000175 24.632881 +v 46.632359 38.022903 25.163944 +v 64.383385 38.594498 20.830767 +v 64.333160 39.264919 20.569706 +v 46.418262 39.086071 24.051086 +v 46.411797 39.264919 24.017479 +v 50.094269 39.264919 23.309032 +v 46.883144 40.440468 26.467533 +v 50.565620 40.440468 25.759087 +v 69.714653 38.455292 22.076077 +v 46.905251 38.595646 26.582420 +v 50.565792 38.455292 25.759996 +v 50.620667 39.981804 26.045231 +v 46.938194 39.981804 26.753677 +v 69.497810 38.000175 20.948959 +v 46.666485 38.000175 25.341326 +v 50.587551 40.300220 25.873085 +v 50.606010 40.146526 25.969040 +v 64.779633 38.330574 22.890430 +v 69.599663 38.068542 21.478359 +v 69.631630 38.135670 21.644522 +v 46.768330 38.068542 25.870726 +v 50.631310 39.808659 26.100538 +v 64.721436 40.759663 22.587912 +v 69.631393 40.759663 21.643318 +v 46.768070 40.826614 25.869375 +v 46.830208 40.672024 26.192360 +v 50.512680 40.672024 25.483913 +v 46.800064 40.759663 26.035685 +v 64.826447 40.300220 23.133760 +v 69.786659 39.266331 22.450378 +v 64.878838 39.448078 23.406115 +v 50.637802 39.266331 26.134300 +v 50.639946 39.448078 26.145439 +v 64.876694 39.266331 23.394974 +v 46.955330 39.266331 26.842745 +v 64.751808 38.223469 22.745825 +v 69.661774 38.223469 21.801229 +v 69.689598 38.330574 21.945835 +v 46.830444 38.223469 26.193596 +v 46.858265 38.330574 26.338202 +v 50.512917 38.223469 25.485149 +v 64.656380 38.023182 22.249760 +v 64.689697 38.068542 22.422955 +v 46.735012 38.023182 25.697533 +v 64.622246 38.000263 22.072336 +v 69.532204 38.000263 21.127741 +v 46.700878 38.000263 25.520107 +v 50.417484 38.023182 24.989086 +v 69.754990 38.749439 22.285770 +v 64.859657 38.914249 23.306389 +v 50.606136 38.749439 25.969690 +v 46.923664 38.749439 26.678137 +v 64.870270 39.087452 23.361576 +v 69.780235 39.087452 22.416981 +v 61.316620 34.574280 6.299973 +v 61.312920 34.574375 6.490880 +v 55.456875 34.574280 7.427290 +v 61.036880 32.926773 4.759091 +v 61.039318 33.133568 4.771767 +v 70.859245 33.133568 2.882577 +v 55.144924 32.926773 5.892605 +v 60.995525 31.783306 7.656045 +v 60.987606 31.798721 7.674161 +v 71.395844 31.798721 5.671790 +v 55.257820 34.198410 6.479429 +v 70.866638 32.516430 2.921004 +v 71.072044 34.496571 3.988700 +v 46.522228 34.496571 8.711675 +v 46.485836 34.420193 8.522511 +v 70.895279 33.721729 3.069858 +v 46.328823 33.534252 7.706368 +v 55.183392 33.721729 6.092562 +v 55.166759 33.534252 6.006098 +v 70.941185 34.056538 3.308511 +v 61.096321 33.896751 5.068068 +v 61.058811 32.319458 4.873092 +v 70.878738 32.319458 2.983902 +v 46.328922 32.319458 7.706877 +v 70.859268 32.719955 2.882709 +v 61.046711 32.516430 4.810194 +v 46.908474 33.535702 10.719367 +v 56.492821 33.535702 8.875499 +v 56.404926 33.723171 8.802396 +v 71.420784 31.958406 5.801417 +v 60.925636 31.958406 7.820507 +v 46.846027 31.798721 10.394764 +v 61.272781 34.524712 5.985285 +v 61.256676 34.524712 5.988383 +v 55.360161 34.496571 7.011404 +v 61.312759 34.574272 6.279903 +v 71.148788 34.574272 4.387615 +v 61.273930 34.548199 6.078078 +v 55.453011 34.574272 7.407220 +v 55.414185 34.548199 7.205395 +v 46.419884 34.198410 8.179700 +v 71.035652 34.420193 3.799536 +v 55.323772 34.420193 6.822240 +v 46.451553 34.320305 8.344303 +v 46.309429 33.133568 7.605552 +v 55.147362 33.133568 5.905281 +v 61.075348 33.721729 4.959048 +v 46.345459 33.721729 7.792832 +v 61.274227 31.306643 6.079606 +v 55.414478 31.306643 7.206923 +v 61.181690 31.534010 5.511811 +v 55.289734 31.534010 6.645325 +v 70.941391 31.797485 3.309555 +v 70.969948 31.655691 3.457995 +v 46.391571 31.797485 8.032529 +v 55.258064 31.655691 6.480699 +v 61.235847 31.392776 5.793306 +v 71.072342 31.358046 3.990242 +v 46.522526 31.358046 8.713217 +v 55.376564 31.358046 7.009848 +v 61.274078 31.280785 6.288991 +v 55.418308 31.304075 7.226835 +v 46.560440 31.306643 8.910293 +v 70.895439 32.131989 3.070702 +v 46.366615 31.957125 7.902797 +v 46.345619 32.131989 7.793677 +v 55.183556 32.131989 6.093406 +v 70.856804 32.926773 2.869901 +v 46.309456 32.719955 7.605683 +v 56.676979 32.928387 8.958290 +v 46.927944 33.135204 10.820560 +v 71.470390 33.338730 6.059290 +v 60.891953 33.535702 8.029180 +v 46.920574 33.338730 10.782265 +v 60.946449 33.723171 7.928683 +v 71.367081 34.199467 5.522300 +v 46.845825 34.057674 10.393715 +v 61.258160 34.420887 7.111399 +v 71.335411 34.321148 5.357674 +v 55.812225 34.420887 8.159105 +v 46.785595 34.321148 10.080648 +v 71.441757 32.133430 5.910437 +v 56.384117 31.958406 8.694220 +v 56.476284 32.133430 8.789543 +v 71.470482 32.518005 6.059753 +v 60.814278 32.320908 8.044650 +v 46.920666 32.518005 10.782727 +v 56.570694 32.320908 8.861045 +v 60.751659 32.928387 8.174390 +v 71.335663 31.534855 5.358966 +v 61.176445 31.434967 7.128626 +v 46.751560 31.434967 9.903733 +v 55.776115 31.358620 7.971396 +v 71.188240 31.280876 4.592679 +v 61.270115 31.280876 6.500761 +v 61.266422 31.306923 6.710764 +v 56.582695 33.338730 8.923434 +v 71.420601 33.898132 5.800478 +v 61.088646 34.057674 7.653634 +v 71.395645 34.057674 5.670740 +v 61.060886 33.999672 7.707832 +v 46.817268 34.199467 10.245275 +v 56.036705 34.199467 8.471608 +v 55.928535 34.321148 8.321699 +v 71.264687 34.497074 4.990050 +v 55.613091 34.548508 7.796787 +v 46.676956 34.548508 9.515950 +v 61.303711 34.490608 6.923106 +v 55.693523 34.497074 7.985682 +v 71.187935 34.574375 4.591091 +v 46.638119 34.574375 9.314066 +v 71.458389 32.320908 5.996901 +v 60.868248 32.168861 7.961548 +v 46.908573 32.320908 10.719875 +v 71.477783 32.721592 6.097717 +v 60.770199 32.518005 8.118308 +v 56.674541 32.721592 8.945614 +v 56.638954 32.518005 8.913091 +v 61.266220 31.311365 6.728227 +v 55.655907 31.306923 7.790134 +v 46.677250 31.306923 9.517477 +v 55.662575 31.311365 7.806274 +v 45.754868 31.906664 4.722975 +v 46.792843 27.567686 6.719300 +v 70.601196 28.748648 1.541240 +v 60.484760 31.906664 1.889190 +v 54.592804 31.906664 3.022704 +v 51.725163 32.743256 35.756310 +v 76.274979 32.743256 31.033335 +v 75.544533 25.600410 33.281940 +v 52.123806 26.241262 37.828434 +v 72.279839 27.965517 28.017206 +v 71.904739 28.001427 27.503540 +v 71.559708 28.020782 27.254135 +v 71.366455 28.028603 27.163761 +v 70.953598 28.039904 27.058821 +v 70.527489 28.044874 27.059690 +v 67.170486 28.113617 26.584072 +v 69.045937 27.965517 28.639355 +v 69.125305 27.990200 28.221403 +v 69.203674 28.001427 28.023180 +v 69.306305 28.011675 27.836245 +v 69.577461 28.028603 27.507933 +v 69.921913 28.039904 27.257298 +v 69.741806 28.035006 27.371834 +v 70.114883 28.043211 27.166206 +v 72.439278 27.912836 28.845966 +v 72.412743 27.900188 29.057392 +v 72.359901 27.888153 29.263918 +v 72.178909 27.866678 29.649078 +v 71.743401 27.843346 30.113489 +v 71.370232 27.835142 30.319136 +v 71.167282 27.833479 30.385315 +v 69.073219 27.939177 29.063824 +v 68.226372 27.764736 32.072548 +v 69.205376 27.912836 29.468115 +v 70.531616 27.838449 30.426502 +v 70.744606 27.835142 30.439495 +v 70.321869 27.843346 30.386969 +v 69.925507 27.857569 30.231188 +v 69.745140 27.866678 30.117294 +v 69.580475 27.876926 29.981783 +v 69.434158 27.888153 29.826784 +v 67.195198 30.209538 26.712547 +v 71.908768 30.209538 25.805735 +v 72.964653 29.860657 31.294212 +v 72.943474 28.064154 31.184092 +v 68.251091 29.860657 32.201023 +v 68.569992 30.209538 26.448061 +v 71.887581 28.413034 25.695614 +v 57.682102 27.939177 31.255283 +v 57.321167 27.990200 30.492323 +v 57.174850 28.001427 30.337324 +v 57.010181 28.011675 30.201813 +v 56.829819 28.020782 30.087919 +v 56.433453 28.035006 29.932138 +v 55.797596 28.044874 29.893476 +v 55.588039 28.044874 29.933792 +v 55.385090 28.043211 29.999971 +v 54.316307 27.952452 31.686220 +v 54.316055 27.965517 31.473139 +v 54.395416 27.990200 31.055189 +v 54.576412 28.011675 30.670029 +v 54.701645 28.020782 30.497345 +v 55.011921 28.035006 30.205618 +v 55.192024 28.039904 30.091084 +v 57.709126 27.925900 31.466671 +v 57.709385 27.912836 31.679752 +v 57.630016 27.888153 32.097702 +v 57.551651 27.876926 32.295929 +v 59.584839 27.764736 33.735035 +v 57.449020 27.866678 32.482864 +v 56.640343 27.835142 33.152920 +v 56.437393 27.833479 33.219101 +v 56.227837 27.833479 33.259415 +v 54.343330 27.939177 31.897608 +v 50.574806 28.113617 29.776804 +v 54.578556 27.900188 32.488392 +v 55.015247 27.866678 32.951080 +v 58.234764 29.860657 34.127998 +v 53.840099 30.209538 29.281845 +v 50.599525 30.209538 29.905277 +v 53.818913 28.413034 29.171724 +v 58.528950 28.113617 28.246559 +v 57.157688 28.413034 28.529400 +v 58.553665 30.209538 28.375034 +v 58.213581 28.064154 34.017876 +v 59.609554 29.860657 33.863510 +v 51.655415 29.860657 35.393753 +v 69.604698 28.064154 31.826416 +v 72.962326 28.057924 31.282099 +v 69.623550 28.057924 31.924423 +v 69.104996 30.633932 29.229004 +v 68.780930 30.440424 27.544506 +v 69.422005 30.228603 30.876797 +v 69.625877 29.860657 31.936537 +v 68.548805 28.413034 26.337940 +v 69.648270 30.153843 32.052898 +v 72.760780 30.228603 30.234472 +v 71.893440 30.515184 25.726080 +v 68.554665 30.515184 26.368404 +v 68.529945 28.419264 26.239931 +v 71.868721 28.419264 25.597607 +v 72.119705 30.440424 26.902182 +v 72.443771 30.633932 28.586679 +v 72.987045 30.153843 31.410572 +v 72.764313 30.528021 30.252825 +v 72.471054 30.607592 29.011148 +v 72.470795 30.620655 28.798069 +v 72.391685 30.582907 29.429100 +v 72.210686 30.561432 29.814259 +v 72.313316 30.571682 29.627323 +v 71.775185 30.538101 30.278669 +v 71.595078 30.533205 30.393206 +v 69.425537 30.528021 30.895151 +v 69.776917 30.561432 30.282476 +v 69.340240 30.594944 29.819784 +v 72.123238 30.739841 26.920534 +v 72.390434 30.647207 28.380356 +v 72.208527 30.672918 27.995901 +v 71.771851 30.706430 27.533209 +v 71.398232 30.723356 27.328941 +v 71.195122 30.729761 27.263535 +v 70.772285 30.737967 27.211027 +v 68.784454 30.739841 27.562859 +v 69.773590 30.729761 27.537014 +v 69.609238 30.723356 27.673115 +v 69.235451 30.696182 28.188360 +v 69.338081 30.706430 28.001427 +v 69.077972 30.647207 29.017616 +v 58.232433 28.057924 34.115883 +v 54.051037 30.440424 30.378290 +v 53.824776 30.515184 29.202190 +v 53.800060 28.419264 29.073715 +v 54.692116 30.228603 33.710583 +v 54.893661 28.057924 34.758209 +v 54.895992 29.860657 34.770321 +v 54.918377 30.153843 34.886681 +v 54.874805 28.064154 34.660202 +v 58.257153 30.153843 34.244358 +v 57.389812 30.440424 29.735966 +v 57.163551 30.515184 28.559864 +v 57.138832 28.419264 28.431391 +v 57.178875 30.209538 28.639521 +v 57.661793 30.582907 32.262886 +v 57.583431 30.571682 32.461109 +v 57.355564 30.552324 32.820728 +v 58.034420 30.528021 33.086609 +v 56.672123 30.529896 33.318100 +v 54.695644 30.528021 33.728935 +v 55.420643 30.544506 33.320526 +v 55.047028 30.561432 33.116261 +v 54.736042 30.582907 32.825748 +v 54.882362 30.571682 32.980751 +v 54.507275 30.607592 32.467079 +v 54.375107 30.633932 32.062790 +v 57.581722 30.660273 31.016172 +v 55.829376 30.739630 30.058657 +v 57.393345 30.739841 29.754320 +v 55.223801 30.734657 30.256264 +v 54.374340 30.672918 31.426899 +v 54.054569 30.739841 30.396645 +v 54.348083 30.647207 31.851400 +v 58.030888 30.228603 33.068256 +v 72.358658 27.952452 28.215174 +v 72.411995 27.939177 28.421499 +v 69.126556 27.925900 29.270149 +v 69.067329 27.440147 29.033234 +v 71.161400 27.334450 30.354727 +v 70.957726 27.833479 30.425631 +v 70.951836 27.334450 30.395042 +v 69.919617 27.358541 30.200598 +v 69.739250 27.367649 30.086706 +v 69.780449 30.860849 30.300829 +v 69.431534 28.020782 27.663559 +v 69.777115 31.029179 27.555368 +v 69.302582 27.401161 29.624014 +v 69.469467 30.882324 30.010319 +v 70.947708 27.540874 27.028231 +v 71.157455 27.535978 27.067764 +v 70.985374 30.734657 27.224001 +v 70.988907 31.034075 27.242355 +v 69.072472 27.978165 28.427931 +v 69.157089 30.684956 28.386585 +v 69.104248 30.672918 28.593111 +v 69.735924 27.535978 27.341244 +v 69.953697 30.734657 27.422480 +v 70.150192 31.037384 27.349741 +v 71.907753 27.849751 29.977388 +v 70.315987 27.344318 30.356379 +v 70.563393 30.533205 30.591682 +v 70.154068 30.843924 30.505096 +v 69.960815 30.851742 30.414722 +v 69.957283 30.552324 30.396368 +v 69.199493 27.413807 29.437527 +v 69.308464 27.900188 29.654604 +v 72.311615 30.660273 28.182388 +v 71.740074 28.011675 27.368027 +v 71.734192 27.512646 27.337439 +v 71.591484 30.715538 27.419315 +v 71.898857 27.502398 27.472950 +v 72.045174 27.491171 27.627949 +v 72.051056 27.990200 27.658539 +v 71.936516 30.696182 27.668720 +v 72.082832 30.684956 27.823719 +v 70.734627 27.544182 27.015257 +v 70.740509 28.043211 27.045845 +v 70.775818 31.037384 27.229380 +v 70.317932 28.044874 27.100006 +v 70.349709 30.739630 27.265188 +v 70.559265 30.739630 27.224873 +v 71.163345 28.035006 27.098354 +v 71.401764 31.022774 27.347296 +v 69.040314 27.453424 28.821846 +v 69.081505 30.946625 29.035971 +v 69.160614 30.984373 28.404938 +v 69.040054 27.466488 28.608767 +v 69.077713 30.660273 28.804537 +v 69.046196 27.952452 28.852436 +v 69.463310 30.715538 27.828741 +v 69.466843 31.014956 27.847094 +v 69.612770 31.022774 27.691467 +v 70.108994 27.544182 27.135616 +v 70.146660 30.737967 27.331387 +v 71.402016 30.529896 30.484316 +v 71.202591 30.827650 30.568850 +v 71.199059 30.528233 30.550497 +v 71.778709 30.837519 30.297024 +v 72.047791 27.358541 29.791174 +v 72.085457 30.552324 29.986944 +v 72.088989 30.851742 30.005297 +v 71.943062 30.843924 30.160923 +v 71.939529 30.544506 30.142570 +v 72.173027 27.367649 29.618488 +v 72.316849 30.871098 29.645678 +v 72.444519 30.594944 29.222574 +v 70.118759 27.849751 30.321562 +v 70.112869 27.350721 30.290972 +v 70.353645 30.538101 30.552149 +v 70.150536 30.544506 30.486742 +v 70.566925 30.832623 30.610037 +v 70.989502 30.528233 30.590813 +v 70.776382 30.529896 30.604677 +v 69.465935 30.582907 29.991964 +v 69.574585 27.377897 29.951195 +v 69.612251 30.571682 30.146965 +v 69.120674 27.426872 29.239559 +v 69.158333 30.620655 29.435329 +v 69.240685 30.907009 29.651649 +v 69.237152 30.607592 29.633297 +v 72.170860 27.479136 27.800131 +v 72.176750 27.978165 27.830719 +v 72.212059 30.972336 28.014254 +v 71.775383 31.005848 27.551561 +v 70.562798 31.039047 27.243225 +v 71.563293 27.838449 30.228024 +v 71.598602 30.832623 30.411558 +v 72.053680 27.857569 29.821762 +v 72.275658 27.377897 29.431553 +v 72.281540 27.876926 29.462143 +v 72.439018 27.925900 28.632887 +v 57.622879 27.453424 31.018370 +v 57.676220 27.440147 31.224695 +v 54.396667 27.925900 32.103931 +v 56.469170 30.528233 33.384281 +v 56.263145 30.827650 33.442951 +v 56.259613 30.528233 33.424599 +v 55.189728 27.358541 33.034382 +v 55.227390 30.552324 33.230152 +v 54.736954 31.014956 30.680880 +v 55.006035 27.535978 30.175030 +v 55.227333 31.034075 30.274618 +v 55.043697 30.729761 30.370800 +v 54.704266 27.888153 32.660568 +v 54.610332 30.594944 32.653572 +v 56.223705 28.039904 29.892605 +v 56.217823 27.540874 29.862017 +v 56.427567 27.535978 29.901550 +v 56.468761 31.029179 30.115673 +v 56.255486 30.734657 30.057787 +v 56.465229 30.729761 30.097321 +v 56.630680 27.529573 29.966957 +v 54.342564 27.978165 31.261719 +v 54.336678 27.479136 31.231129 +v 54.427197 30.684956 31.220369 +v 54.847572 28.028603 30.341719 +v 54.841686 27.529573 30.311129 +v 54.879349 30.723356 30.506899 +v 55.186138 27.540874 30.060493 +v 55.379204 27.544182 29.969383 +v 55.416866 30.737967 30.165154 +v 57.013512 27.843346 32.947273 +v 57.213169 30.843924 32.994709 +v 57.048820 30.837519 33.130810 +v 57.209641 30.544506 32.976357 +v 55.586098 27.344318 33.190163 +v 55.833504 30.533205 33.425468 +v 55.627289 30.837519 33.404289 +v 55.623760 30.538101 33.385933 +v 55.195614 27.857569 33.064972 +v 55.230923 30.851742 33.248508 +v 54.469612 27.413807 32.271309 +v 54.510807 30.907009 32.485432 +v 57.549946 27.965517 30.850992 +v 57.628765 27.952452 31.048958 +v 57.660542 30.647207 31.214140 +v 57.585255 30.959690 31.034527 +v 57.356476 30.984373 30.675858 +v 57.478638 30.672918 30.829685 +v 56.861599 30.715538 30.253101 +v 57.041962 30.706430 30.366993 +v 57.210159 30.995600 30.520859 +v 57.352943 30.684956 30.657505 +v 55.619820 30.739630 30.098972 +v 56.636566 28.028603 29.997545 +v 56.668343 30.723356 30.162727 +v 54.347836 30.660273 31.638319 +v 54.467896 27.502398 30.826376 +v 54.389534 27.491171 31.024599 +v 54.505558 30.696182 31.022146 +v 54.473782 28.001427 30.856964 +v 54.608192 30.706430 30.835211 +v 54.310173 27.466488 31.442549 +v 54.351364 30.959690 31.656673 +v 54.695759 27.521755 30.466755 +v 54.733421 30.715538 30.662525 +v 55.623348 31.039047 30.117327 +v 57.713882 30.633932 31.420465 +v 57.740902 30.620655 31.631853 +v 56.431507 27.334450 33.188511 +v 56.833408 27.838449 33.061810 +v 57.045292 30.538101 33.112453 +v 56.865185 30.533205 33.226990 +v 57.177860 27.849751 32.811172 +v 57.359097 30.851742 32.839081 +v 57.682850 27.900188 31.891176 +v 57.714626 30.594944 32.056358 +v 57.676964 27.401161 31.860588 +v 57.744694 30.907009 31.863287 +v 57.718159 30.894361 32.074711 +v 55.591980 27.843346 33.220753 +v 55.388866 27.849751 33.155346 +v 55.424175 30.843924 33.338882 +v 55.801727 27.838449 33.260288 +v 56.014816 27.835142 33.273262 +v 56.050125 30.829313 33.456795 +v 56.046593 30.529896 33.438442 +v 54.850582 27.876926 32.815567 +v 54.844696 27.377897 32.784981 +v 55.050560 30.860849 33.134613 +v 54.475498 27.912836 32.301899 +v 54.428448 30.620655 32.269115 +v 57.440971 27.479136 30.633915 +v 57.446857 27.978165 30.664505 +v 57.206627 30.696182 30.502504 +v 55.791714 27.545847 29.862886 +v 56.010616 28.043211 29.879631 +v 56.004734 27.544182 29.849041 +v 56.045925 31.037384 30.063166 +v 56.042397 30.737967 30.044811 +v 56.675652 30.829313 33.336456 +v 57.323788 27.857569 32.655548 +v 57.317902 27.358541 32.624958 +v 57.480797 30.561432 32.648045 +v 57.484329 30.860849 32.666397 +v 57.545769 27.377897 32.265339 +v 57.703499 27.413807 31.649162 +v 57.741161 30.607592 31.844933 +v 73.133041 25.948370 28.069304 +v 68.864822 26.065239 26.983849 +v 72.354019 27.389124 29.233330 +v 72.406853 27.401161 29.026802 +v 73.155472 25.886189 29.079416 +v 72.433388 27.413807 28.815378 +v 72.352768 27.453424 28.184586 +v 71.360573 27.529573 27.133171 +v 71.414162 26.080296 26.247744 +v 71.026947 26.090002 26.163887 +v 71.110840 26.088047 26.179663 +v 72.418747 25.812611 30.421471 +v 73.194214 25.904634 28.771019 +v 72.678169 26.012682 27.107624 +v 72.920380 25.986547 27.487391 +v 71.983307 26.058466 26.494383 +v 69.803406 26.094831 26.320511 +v 70.099487 26.097260 26.223911 +v 70.521606 27.545847 27.029102 +v 70.312042 27.545847 27.069418 +v 70.405281 26.097260 26.165081 +v 68.272964 25.948370 29.004301 +v 68.243927 25.981487 28.469589 +v 68.442665 26.029385 27.649981 +v 68.359734 26.017492 27.859955 +v 69.119423 27.491171 28.190815 +v 70.525734 27.339420 30.395912 +v 70.496414 25.794794 31.081974 +v 71.032852 25.788862 31.075527 +v 68.517754 25.899576 29.753220 +v 68.305092 25.937754 29.171307 +v 72.244850 25.805826 30.565607 +v 72.179062 25.803261 30.620136 +v 72.814415 25.837296 29.942667 +v 71.901863 27.350721 29.946800 +v 72.964188 25.852247 29.669930 +v 73.099586 25.873442 29.298115 +v 73.078400 25.868631 29.380653 +v 72.414482 26.033876 26.812588 +v 72.348557 26.037975 26.758394 +v 72.472954 26.029385 26.874619 +v 72.627869 26.017492 27.038837 +v 72.174210 26.048828 26.614885 +v 71.332840 26.082863 26.221514 +v 71.553825 27.521755 27.223545 +v 69.449699 26.088047 26.499239 +v 69.916031 27.540874 27.226709 +v 69.884560 26.095497 26.294022 +v 70.941620 26.091330 26.158655 +v 68.244370 25.962425 28.780510 +v 69.425652 27.521755 27.632971 +v 68.473946 26.033876 27.570681 +v 69.197784 27.502398 27.992590 +v 68.282661 25.999935 28.161194 +v 69.066589 27.479136 28.397341 +v 70.738724 27.336113 30.408907 +v 69.428268 27.389124 29.796194 +v 68.444992 25.909863 29.599417 +v 71.364349 27.336113 30.288546 +v 71.634727 25.791292 30.920099 +v 71.839088 25.794794 30.823664 +v 71.557411 27.339420 30.197435 +v 71.916382 25.796120 30.787148 +v 71.737518 27.344318 30.082899 +v 73.193764 25.923698 28.460100 +v 72.406113 27.440147 28.390909 +v 73.154411 25.943062 28.151785 +v 72.433128 27.426872 28.602299 +v 72.961609 25.981487 27.561985 +v 72.273949 27.466488 27.986618 +v 69.193283 26.080296 26.675001 +v 69.571579 27.529573 27.477345 +v 69.019287 26.073511 26.819159 +v 69.300415 27.512646 27.805655 +v 70.722069 25.791292 31.095680 +v 72.573311 25.820885 30.256760 +v 72.681854 25.827658 30.125401 +v 69.259071 26.082863 26.620474 +v 58.424519 25.943062 30.985571 +v 57.258545 25.798077 33.575157 +v 55.597404 25.798077 33.894730 +v 58.348507 25.868631 32.214439 +v 58.453705 25.899576 31.689375 +v 57.544060 27.466488 30.820402 +v 58.346668 25.962425 30.684662 +v 56.602951 26.082863 29.055300 +v 56.380947 26.088047 29.013447 +v 55.369595 26.097260 29.057695 +v 55.382980 27.350721 33.124756 +v 57.171974 27.350721 32.780586 +v 58.190487 25.986547 30.321175 +v 57.004299 27.512646 30.171223 +v 57.253414 26.058466 29.328167 +v 55.073513 26.094831 29.154295 +v 53.744095 26.033876 30.404459 +v 53.608658 26.012682 30.776279 +v 53.629807 26.017492 30.693748 +v 53.896976 25.886189 32.784428 +v 54.337444 27.440147 31.867020 +v 54.310421 27.453424 31.655630 +v 53.543034 25.948370 31.838093 +v 53.575165 25.937754 32.005100 +v 54.390781 27.426872 32.073345 +v 53.631645 25.923698 32.223526 +v 57.449173 25.803261 33.453922 +v 57.007629 27.344318 32.916683 +v 57.443134 27.367649 32.452274 +v 58.084530 25.837296 32.776451 +v 57.624130 27.389124 32.067116 +v 58.425587 25.886189 31.913200 +v 58.464214 25.909863 31.519552 +v 57.315281 27.491171 30.461735 +v 57.743065 26.029385 29.708405 +v 57.168964 27.502398 30.306734 +v 56.823933 27.521755 30.057331 +v 55.154671 26.095497 29.127806 +v 55.582157 27.545847 29.903202 +v 56.211830 26.091330 28.992420 +v 56.297058 26.090002 28.997673 +v 55.900986 26.095497 28.984228 +v 53.893787 26.048828 30.131733 +v 53.785137 26.037975 30.329685 +v 54.134899 26.065239 29.817640 +v 53.524647 25.986547 31.218805 +v 55.795841 27.339420 33.229698 +v 53.787865 25.899576 32.587002 +v 54.572670 27.401161 32.457802 +v 54.698380 27.389124 32.629978 +v 54.030071 25.873442 32.966770 +v 54.235268 25.856739 33.199780 +v 54.293797 25.852247 33.261799 +v 55.009365 27.367649 32.920490 +v 54.724979 25.827658 33.580006 +v 53.715111 25.909863 32.433201 +v 53.746639 25.904634 32.512405 +v 53.553837 25.943062 31.922609 +v 57.901814 25.824013 33.028290 +v 56.634460 27.336113 33.122330 +v 57.186497 25.796120 33.620930 +v 56.827522 27.339420 33.031219 +v 58.234299 25.852247 32.503712 +v 57.703239 27.426872 31.436083 +v 58.231724 25.981487 30.395771 +v 57.897980 26.017492 29.872623 +v 54.289398 26.073511 29.652943 +v 54.570530 27.512646 30.639441 +v 56.221951 27.334450 33.228828 +v 56.008930 27.336113 33.242672 +v 55.992176 25.791292 33.929466 +v 57.843426 25.820885 33.090546 +v 55.294083 25.805826 33.826653 +v 72.689682 32.936100 28.577045 +v 69.957222 31.034075 27.440832 +v 69.108528 30.933350 29.247358 +v 68.913231 32.922829 29.520058 +v 68.855606 32.944691 29.174501 +v 68.873070 32.936100 29.311298 +v 72.655136 32.944691 28.443537 +v 69.657249 32.846298 30.625416 +v 72.717247 32.884747 29.409529 +v 69.370560 33.026978 27.733027 +v 69.653427 33.039387 27.476149 +v 72.393967 30.946625 28.398708 +v 69.237755 32.876957 30.205971 +v 72.448051 30.894361 29.240927 +v 69.547142 33.035244 27.564184 +v 69.770012 33.042557 27.401987 +v 70.803802 32.810127 30.994938 +v 72.447304 30.933350 28.605034 +v 72.474327 30.920073 28.816422 +v 72.656609 32.870937 29.646420 +v 71.832893 32.816372 30.695118 +v 69.081245 30.959690 28.822889 +v 68.855453 32.953144 29.036612 +v 68.886566 32.929462 29.416945 +v 69.238983 30.995600 28.206715 +v 69.234924 33.018517 27.897144 +v 70.353241 31.039047 27.283541 +v 70.314301 33.050713 27.164234 +v 72.134529 33.000874 27.627140 +v 71.415482 33.035244 27.204746 +v 70.454575 32.816372 30.960283 +v 70.357178 30.837519 30.570503 +v 69.864136 32.835857 30.755957 +v 69.468384 32.858055 30.469965 +v 69.038193 32.899254 29.880606 +v 69.343773 30.894361 29.838139 +v 72.474586 30.907009 29.029501 +v 72.747452 32.905785 29.060493 +v 72.605949 32.863670 29.774776 +v 72.214218 30.860849 29.832613 +v 72.566765 32.858055 29.873888 +v 72.395218 30.882324 29.447453 +v 72.682976 32.876957 29.543167 +v 69.107780 30.972336 28.611465 +v 69.341614 31.005848 28.019779 +v 69.297539 33.023071 27.810801 +v 70.661209 33.049877 27.111113 +v 71.198654 31.029179 27.281889 +v 70.779915 30.829313 30.623030 +v 69.615784 30.871098 30.165318 +v 72.137970 32.826893 30.464764 +v 71.949486 32.819542 30.620956 +v 72.055763 32.823685 30.532921 +v 72.367989 32.840412 30.199961 +v 72.086365 30.984373 27.842073 +v 71.595016 31.014956 27.437670 +v 71.738770 33.023071 27.341148 +v 71.945656 33.012627 27.471689 +v 71.043518 33.045006 27.117043 +v 71.148338 33.042557 27.136820 +v 69.161865 30.920073 29.453684 +v 70.559395 32.813923 30.980062 +v 69.550751 32.852936 30.537661 +v 71.405540 30.829313 30.502670 +v 71.646355 32.812271 30.797882 +v 71.390076 32.809052 30.899731 +v 71.048218 32.808216 30.979116 +v 72.305374 32.835857 30.286304 +v 72.315147 30.959690 28.200741 +v 71.940048 30.995600 27.687075 +v 70.941696 32.809052 30.985991 +v 70.993034 30.827650 30.609165 +v 54.357590 33.005993 30.963581 +v 54.156677 32.929462 32.250729 +v 54.378639 30.933350 32.081142 +v 54.431976 30.920073 32.287468 +v 57.635265 32.981972 30.724920 +v 54.143177 32.936100 32.145084 +v 57.874226 32.953144 31.149218 +v 57.987358 32.884747 32.243313 +v 54.817253 33.035244 30.397968 +v 55.047230 31.029179 30.389153 +v 56.685593 33.035244 30.038530 +v 54.426495 32.884747 32.928364 +v 58.004532 32.892925 32.106560 +v 54.189991 32.981972 31.387732 +v 54.138645 32.966003 31.658104 +v 55.040123 33.042557 30.235771 +v 55.584408 33.050713 29.998020 +v 55.824802 33.050713 29.951773 +v 55.832905 31.039047 30.077011 +v 56.211807 32.809052 33.819775 +v 54.739574 30.882324 32.844105 +v 54.570660 32.870937 33.125854 +v 57.717411 30.933350 31.438818 +v 57.744434 30.920073 31.650206 +v 54.125698 32.944691 32.008289 +v 54.351616 30.946625 31.869755 +v 54.216408 32.987988 31.284470 +v 54.267094 32.995258 31.156109 +v 54.430725 30.984373 31.238724 +v 54.505032 33.018517 30.730928 +v 54.611721 31.005848 30.853563 +v 54.882881 31.022774 30.525253 +v 56.069214 33.048801 29.935953 +v 56.671875 31.022774 30.181080 +v 55.134247 32.835857 33.589741 +v 54.885891 30.871098 32.999104 +v 54.820824 32.852936 33.371452 +v 54.613865 30.894361 32.671925 +v 54.359818 32.892925 32.807743 +v 54.308266 32.899254 32.714397 +v 58.017681 32.899254 32.000771 +v 56.660187 32.809052 33.733517 +v 57.103004 32.816372 33.528904 +v 57.785500 32.852936 32.801102 +v 57.665325 30.882324 32.281239 +v 57.586960 30.871098 32.479462 +v 57.836872 32.858055 32.707672 +v 57.953087 32.876957 32.376953 +v 57.926720 32.870937 32.480206 +v 54.125408 32.959675 31.763912 +v 54.377872 30.972336 31.445253 +v 54.509090 30.995600 31.040499 +v 54.567650 33.023071 30.644585 +v 55.420399 31.037384 30.183506 +v 57.572468 32.987988 30.638821 +v 57.045490 31.005848 30.385347 +v 56.259014 31.034075 30.076139 +v 55.931320 33.049877 29.944899 +v 54.217865 32.914238 32.487358 +v 55.837036 30.832623 33.443821 +v 55.936039 32.812271 33.820282 +v 55.829506 32.813923 33.813847 +v 55.230915 32.831951 33.634872 +v 54.738510 32.858055 33.303745 +v 57.719166 32.846298 32.922104 +v 57.477795 32.995258 30.538424 +v 57.783360 32.966003 30.956921 +v 57.482166 30.972336 30.848040 +v 56.912216 33.026978 30.129803 +v 56.865128 31.014956 30.271454 +v 57.008881 33.023071 30.174932 +v 56.313625 33.045006 29.950829 +v 56.418449 33.042557 29.970606 +v 55.457539 32.823685 33.726143 +v 55.355911 32.826893 33.693352 +v 56.868717 30.832623 33.245342 +v 57.013000 32.813923 33.586163 +v 56.916462 32.812271 33.631668 +v 56.472702 30.827650 33.402634 +v 56.318329 32.808216 33.812901 +v 57.834824 32.959675 31.050283 +v 57.664074 30.946625 31.232492 +v 57.404636 33.000874 30.460924 +v 66.344604 39.697979 25.731829 +v 45.726643 38.559273 20.456078 +v 68.557976 38.559273 16.063711 +v 59.756031 34.946129 14.619476 +v 57.905457 27.120533 11.876236 +v 65.778191 34.374424 22.787634 +v 65.364769 34.357391 23.145042 +v 55.329880 34.385426 24.618225 +v 64.885788 34.768574 16.529219 +v 65.773750 35.392830 22.519989 +v 65.713318 35.397808 22.450407 +v 64.642441 35.766628 16.639479 +v 61.281570 34.189182 26.674751 +v 64.857506 34.777916 16.382204 +v 61.639938 26.055202 28.537531 +v 47.359364 27.719242 12.310676 +v 50.426838 26.801964 29.007669 +v 69.590553 27.567686 2.333402 +v 70.554688 33.050713 27.117987 +v 70.212830 33.049877 27.197374 +v 70.081459 33.048801 27.240231 +v 69.956558 33.046658 27.299223 +v 69.860016 33.045006 27.344728 +v 69.464935 33.032036 27.632341 +v 57.215771 33.012627 30.305473 +v 69.153854 33.012627 28.008785 +v 68.919930 32.981972 28.553938 +v 68.855339 32.959675 28.930120 +v 57.986450 32.929462 31.513945 +v 57.925243 32.944691 31.277321 +v 68.885658 32.974182 28.687576 +v 57.716671 32.974182 30.836304 +v 68.946297 32.987988 28.450686 +v 69.036140 33.000874 28.223217 +v 69.087517 33.005993 28.129789 +v 57.959789 32.936100 31.410831 +v 68.868484 32.966003 28.824327 +v 68.996964 32.995258 28.322329 +v 57.322266 33.005993 30.393229 +v 57.099064 33.018517 30.231880 +v 56.787216 33.032036 30.071323 +v 56.554234 33.039387 29.996216 +v 70.387024 33.147514 25.571024 +v 56.207088 33.046658 29.944391 +v 55.226669 33.046658 30.133007 +v 54.155811 32.974182 31.521353 +v 54.125526 32.953144 31.870403 +v 50.501671 33.147514 29.396635 +v 54.306274 33.000874 31.056999 +v 54.423965 33.012627 30.842569 +v 54.735046 33.032036 30.466125 +v 54.640671 33.026978 30.566811 +v 54.923534 33.039387 30.309935 +v 55.130127 33.045006 30.178513 +v 55.351574 33.048801 30.074015 +v 55.482941 33.049877 30.031158 +v 72.564713 32.959675 28.216497 +v 72.604118 32.953144 28.315432 +v 72.446564 32.974182 28.002518 +v 72.513252 32.966003 28.123137 +v 72.302361 32.987988 27.805037 +v 72.365158 32.981972 27.891134 +v 75.218338 33.092384 25.540936 +v 72.207687 32.995258 27.704639 +v 72.052155 33.005993 27.559443 +v 71.828957 33.018517 27.398094 +v 71.642105 33.026978 27.296019 +v 71.517105 33.032036 27.237537 +v 70.553871 33.092384 26.438301 +v 71.284119 33.039387 27.162430 +v 70.936981 33.046658 27.110605 +v 70.799103 33.048801 27.102167 +v 72.729843 32.922829 28.785805 +v 72.716339 32.929462 28.680161 +v 72.747299 32.914238 28.922602 +v 72.734421 32.892925 29.272776 +v 72.747574 32.899254 29.166985 +v 72.515388 32.852936 29.967316 +v 72.449051 32.846298 30.088320 +v 72.232353 32.831951 30.364079 +v 71.742889 32.813923 30.752377 +v 71.521446 32.810127 30.856874 +v 71.288612 32.808216 30.932869 +v 57.575481 32.835857 33.120090 +v 57.999950 32.922829 31.619591 +v 68.947769 32.914238 29.653568 +v 58.017410 32.914238 31.756388 +v 68.998795 32.905785 29.781673 +v 58.017563 32.905785 31.894278 +v 69.089661 32.892925 29.973969 +v 69.156349 32.884747 30.094587 +v 69.300552 32.870937 30.292068 +v 69.395226 32.863670 30.392466 +v 57.876057 32.863670 32.608562 +v 57.638096 32.840412 33.033745 +v 69.773956 32.840412 30.699009 +v 69.960800 32.831951 30.801086 +v 57.502460 32.831951 33.197865 +v 70.085800 32.826893 30.859566 +v 70.187431 32.823685 30.892359 +v 57.325878 32.823685 33.366707 +v 70.318787 32.819542 30.934675 +v 57.408081 32.826893 33.298550 +v 57.219593 32.819542 33.454739 +v 70.665932 32.812271 30.986498 +v 56.791557 32.810127 33.690659 +v 56.558720 32.808216 33.766655 +v 54.507816 32.876957 33.039764 +v 54.183338 32.922829 32.353844 +v 54.268864 32.905785 32.615463 +v 56.073917 32.810127 33.828720 +v 55.724682 32.816372 33.794067 +v 55.588898 32.819542 33.768459 +v 55.044064 32.840412 33.532795 +v 54.927361 32.846298 33.459202 +v 54.665352 32.863670 33.226246 +v 47.791321 32.554409 23.248281 +v 48.639805 32.274055 27.658665 +v 47.329750 34.144943 21.907705 +v 47.370945 37.638145 22.121828 +v 48.502254 37.264343 28.002338 +v 48.698654 37.264343 27.964556 +v 47.567341 37.638145 22.084045 +v 48.443409 32.274055 27.696447 +v 69.065254 32.274055 23.729149 +v 67.933945 32.647858 17.848639 +v 69.124100 37.264343 24.035040 +v 69.320503 37.264343 23.997257 +v 68.189186 37.638145 18.116745 +v 69.261650 32.274055 23.691366 +v 67.992790 37.638145 18.154530 +v 68.130341 32.647858 17.810854 +v 59.409355 27.286600 8.877703 +v 55.492302 31.280785 7.401309 +v 55.574558 31.280876 7.596490 +v 55.372860 27.449877 6.990583 +v 55.496300 37.917774 7.632237 +v 55.570572 37.917866 7.808144 +v 55.531143 34.574375 7.603197 +v 55.739853 37.864441 8.207960 +v 55.700066 34.490608 8.001154 +v 61.077744 27.375963 7.098868 +v 61.134232 31.534855 7.321551 +v 61.127060 31.557924 7.355211 +v 61.165653 34.199467 7.484885 +v 61.200024 37.745140 7.734471 +v 56.505417 33.498474 8.885402 +v 56.646233 33.135204 8.950924 +v 56.489738 32.168861 8.803900 +v 56.142513 31.656748 8.452569 +v 56.272274 31.798721 8.581312 +v 56.432846 27.344353 8.508171 +v 55.975113 27.375963 8.080529 +v 56.553570 37.581589 9.135686 +v 56.316475 37.683620 8.918522 +v 56.170826 34.057674 8.599740 +v 56.297031 33.898132 8.710000 +v 55.313728 27.469414 6.683230 +v 55.359997 31.392776 6.923721 +v 55.436939 37.917618 7.323677 +v 58.218220 37.150532 9.707748 +v 57.562119 27.298674 9.036112 +v 57.679352 37.239975 9.645487 +v 56.969593 27.318205 8.831476 +v 58.184422 27.286600 9.113360 +v 55.894501 31.434967 8.144784 +v 56.005283 31.534855 8.308274 +v 56.024429 31.557924 8.336872 +v 56.097393 37.745140 8.716132 +v 56.055264 34.172634 8.497147 +v 58.924877 37.087070 9.660819 +v 60.630196 37.452759 8.683191 +v 60.826279 33.338730 8.107039 +v 60.777477 33.135204 8.156141 +v 60.432880 27.318205 8.165197 +v 60.749218 32.721592 8.161714 +v 61.236313 31.358046 5.882530 +v 61.296688 37.917618 6.196359 +v 61.060333 31.656748 7.506463 +v 61.157894 34.172634 7.515486 +v 60.075809 37.239975 9.184449 +v 59.958572 27.298674 8.575074 +v 59.525467 37.132675 9.481241 +v 61.278057 31.304075 6.099518 +v 61.220230 27.411655 6.489185 +v 61.352345 37.917866 6.695827 +v 61.308647 34.548508 6.701057 +v 61.304043 34.497074 6.906312 +v 61.343494 37.864441 7.129913 +v 61.293682 37.839470 7.341565 +v 61.210480 34.321148 7.305542 +v 61.207512 34.312927 7.317633 +v 61.215351 37.760574 7.670162 +v 61.222050 31.358620 6.923690 +v 61.191738 27.404522 6.611038 +v 61.200790 31.394100 7.019108 +v 60.864689 27.350676 7.552396 +v 60.811356 27.344353 7.665818 +v 60.875416 32.133430 7.943223 +v 60.932079 37.581589 8.293333 +v 61.012363 33.898132 7.802849 +v 60.883930 33.498474 8.043050 +v 61.071674 37.683620 8.003700 +v 55.154667 33.337154 5.943245 +v 55.204365 33.896751 6.201581 +v 55.229305 34.056538 6.331215 +v 55.289486 34.320305 6.644032 +v 54.950108 37.147907 4.879951 +v 55.164299 37.682098 5.993318 +v 55.147388 32.719955 5.905413 +v 55.154755 32.516430 5.943708 +v 55.166855 32.319458 6.006606 +v 55.204548 31.957125 6.202527 +v 55.000202 27.567686 5.140342 +v 55.229507 31.797485 6.332258 +v 55.324051 31.434301 6.823695 +v 61.058716 33.534252 4.872584 +v 61.046623 33.337154 4.809731 +v 60.525677 35.376511 2.101882 +v 60.584129 35.864510 2.405715 +v 61.039345 32.719955 4.771899 +v 61.075512 32.131989 4.959892 +v 61.121464 31.797485 5.198744 +v 61.096504 31.957125 5.069013 +v 61.150021 31.655691 5.347185 +v 61.189579 27.469414 5.552814 +v 61.216007 31.434301 5.690181 +v 61.121262 34.056538 5.197701 +v 60.746132 36.778572 3.247786 +v 61.149776 34.198410 5.345915 +v 60.842064 37.147907 3.746436 +v 61.181442 34.320305 5.510518 +v 60.945972 37.450668 4.286564 +v 61.056255 37.682098 4.859804 +v 61.252117 34.496571 5.877890 +v 61.215725 34.420193 5.688726 +v 61.219742 31.392776 5.796404 +v 55.343891 31.392776 6.926820 +v 55.297623 27.469414 6.686328 +v 55.396927 34.524712 7.115701 +v 55.380825 34.524712 7.118799 +v 44.786377 31.056692 15.568623 +v 48.252270 30.884907 17.704353 +v 45.306290 30.884907 18.271109 +v 44.413383 27.719242 12.877433 +v 47.231552 28.717300 12.398706 +v 44.285572 28.717300 12.965464 +v 47.480862 26.801964 29.574427 +v 47.982811 26.201115 28.784519 +v 47.594925 32.554409 23.286064 +v 47.526150 34.144943 21.869921 +v 66.210762 37.917587 5.186333 +v 61.326370 37.917694 6.350639 +v 66.291702 37.917820 5.650479 +v 55.466621 37.917694 7.477957 +v 50.989876 37.917587 8.114577 +v 51.086922 37.917820 8.575625 +v 65.612869 36.563549 2.078520 +v 50.391983 36.563549 5.006763 +v 65.911079 37.566383 3.628589 +v 50.690193 37.566383 6.556833 +v 66.139931 37.878059 4.818144 +v 50.919044 37.878059 7.746388 +v 65.464867 35.620510 1.309204 +v 50.243980 35.620510 4.237448 +v 66.376831 37.761543 6.673365 +v 61.207687 37.752857 7.702316 +v 61.185539 37.738213 7.764773 +v 66.265823 37.683620 7.004432 +v 51.387402 37.761543 9.557081 +v 51.613319 37.683620 9.823330 +v 66.023651 37.760330 4.213748 +v 50.802765 37.760330 7.141993 +v 65.704056 36.963242 2.552516 +v 50.483173 36.963242 5.480761 +v 65.803986 37.299286 3.071905 +v 50.583092 37.299286 6.000150 +v 65.531876 36.106518 1.657485 +v 50.310982 36.106518 4.585728 +v 50.383705 39.574478 28.783466 +v 50.382244 39.198650 28.775875 +v 50.472374 38.791084 29.244370 +v 48.883270 40.194908 20.984259 +v 48.775272 39.377975 20.422897 +v 48.773239 39.002335 20.412317 +v 48.785156 38.504982 20.474270 +v 48.820061 38.038948 20.655716 +v 48.823151 35.931602 20.671766 +v 48.888027 35.566311 21.008991 +v 49.266525 35.948097 22.976391 +v 49.352234 35.734871 23.421919 +v 49.442326 35.580185 23.890221 +v 49.535370 35.486481 24.373871 +v 49.629894 35.455254 24.865189 +v 50.459534 38.169350 29.177628 +v 50.312088 37.749374 28.411201 +v 50.399311 37.490997 28.864588 +v 50.347046 35.428085 28.592913 +v 49.570679 34.457073 24.557388 +v 49.724419 35.486977 25.356527 +v 49.817448 35.581158 25.840084 +v 49.907501 35.736313 26.308191 +v 50.274094 35.108337 28.213696 +v 45.775330 33.641586 4.829321 +v 46.322777 33.435703 7.674942 +v 46.337143 33.627991 7.749600 +v 46.081108 34.636631 6.418759 +v 46.245750 30.014559 7.274553 +v 46.308212 33.030170 7.599214 +v 46.313080 33.235359 7.624534 +v 46.308224 32.823364 7.599279 +v 46.313141 32.618195 7.624831 +v 46.322872 32.417946 7.675428 +v 46.337273 32.225723 7.750277 +v 46.356117 32.044556 7.848237 +v 46.379093 31.877304 7.967663 +v 46.435966 31.594851 8.263283 +v 46.468956 31.484156 8.434780 +v 46.504318 31.396173 8.618591 +v 46.541481 31.332344 8.811754 +v 46.579857 31.293713 9.011235 +v 46.618851 31.280830 9.213915 +v 46.660690 28.046349 9.431399 +v 47.226501 28.289183 12.372464 +v 46.657837 31.293900 9.416565 +v 46.733364 31.396793 9.809153 +v 46.768700 31.484911 9.992837 +v 47.331383 31.992538 12.917617 +v 47.072456 37.301643 11.571739 +v 46.924316 32.619797 10.801710 +v 47.172447 36.966125 12.091484 +v 46.914619 32.419456 10.751301 +v 47.263699 36.566910 12.565804 +v 47.312149 31.595062 12.817638 +v 46.696209 31.332771 9.616024 +v 46.801678 31.595802 10.164242 +v 46.831772 31.727734 10.320654 +v 46.858498 31.878563 10.459578 +v 46.881451 32.045918 10.578901 +v 47.454453 35.246040 13.557343 +v 47.344788 36.110298 12.987311 +v 47.414421 35.603493 13.349266 +v 47.857475 33.019241 15.652243 +v 48.426811 31.700232 18.611607 +v 46.148113 35.122639 6.767040 +v 46.203804 35.417557 7.056528 +v 46.266029 35.673157 7.379961 +v 46.333817 35.885487 7.732326 +v 46.406101 36.051147 8.108049 +v 46.481731 36.167564 8.501171 +v 46.559532 36.232880 8.905572 +v 46.598518 36.245964 9.108223 +v 46.677254 36.233185 9.517482 +v 46.755058 36.168274 9.921923 +v 46.830723 36.052254 10.315209 +v 46.768440 34.371017 9.991465 +v 46.801430 34.260307 10.162962 +v 46.831547 34.128571 10.319494 +v 46.858307 33.977905 10.458584 +v 46.881279 33.810654 10.578010 +v 46.900124 33.629436 10.675966 +v 46.914524 33.437218 10.750816 +v 46.924259 33.236969 10.801413 +v 46.929176 33.031796 10.826964 +v 50.587166 29.161577 29.841042 +v 51.102753 27.939177 32.521042 +v 51.643059 28.812696 35.329514 +v 51.275322 26.521614 33.418053 +v 52.135628 27.243563 37.889874 +v 51.936302 30.494560 36.853813 +v 50.464256 29.974739 29.202152 +v 51.162346 31.476397 32.830795 +v 51.182911 37.878670 8.969482 +v 55.802246 37.851955 8.296991 +v 66.348701 37.878670 6.051837 +v 61.318588 37.851955 7.235739 +v 75.183838 31.650961 25.361618 +v 76.486122 30.494560 32.130836 +v 75.746658 32.917820 28.287136 +v 70.606079 34.356834 1.566617 +v 70.872589 33.435703 2.951968 +v 70.886963 33.627991 3.026626 +v 70.905762 33.809242 3.124368 +v 70.928719 33.976646 3.243695 +v 71.031548 36.167564 3.778196 +v 70.955444 34.127472 3.382618 +v 70.985535 34.259357 3.539026 +v 70.900772 35.935432 3.098455 +v 71.109344 36.232880 4.182598 +v 71.053848 34.458382 3.894118 +v 71.188080 36.246071 4.591826 +v 71.227066 36.233185 4.794507 +v 71.304871 36.168274 5.198948 +v 71.380539 36.052254 5.592235 +v 71.452850 35.886955 5.968088 +v 71.520676 35.674999 6.320664 +v 71.582954 35.419697 6.644365 +v 71.638687 35.125118 6.934073 +v 71.687012 34.795830 7.185299 +v 70.673080 34.842842 1.914898 +v 70.764267 35.242531 2.388895 +v 70.325150 33.641586 0.106346 +v 70.858025 33.030170 2.876239 +v 70.858032 32.823364 2.876305 +v 70.862953 32.618195 2.901856 +v 70.872688 32.417946 2.952453 +v 70.887085 32.225723 3.027302 +v 70.610558 31.931894 1.589911 +v 70.928909 31.877304 3.244689 +v 70.955673 31.726589 3.383775 +v 70.985779 31.594851 3.540308 +v 71.018776 31.484156 3.711806 +v 71.054138 31.396173 3.895617 +v 71.091293 31.332344 4.088780 +v 71.129669 31.293713 4.288260 +v 71.168671 31.280830 4.490941 +v 71.207657 31.293900 4.693590 +v 71.246025 31.332771 4.893049 +v 71.283180 31.396793 5.086177 +v 71.318520 31.484911 5.269862 +v 71.727051 34.438377 7.393374 +v 71.464340 33.437218 6.027841 +v 71.474075 33.236969 6.078438 +v 71.478989 33.031796 6.103990 +v 71.479004 32.824989 6.104055 +v 71.474136 32.619797 6.078735 +v 71.464432 32.419456 6.028327 +v 72.295868 35.040855 10.350090 +v 72.696762 34.592869 12.433889 +v 71.450073 32.227169 5.953669 +v 71.431274 32.045918 5.855927 +v 72.936409 33.972191 13.679592 +v 71.408310 31.878563 5.736604 +v 71.381584 31.727734 5.597680 +v 73.184525 33.452927 14.969294 +v 71.351494 31.595802 5.441268 +v 73.439575 33.038315 16.295025 +v 72.084915 30.803101 9.253565 +v 73.699982 32.730942 17.648582 +v 73.964134 32.532646 19.021666 +v 74.230423 32.444660 20.405840 +v 74.497192 32.467533 21.792480 +v 74.762794 32.601105 23.173067 +v 75.025604 32.844612 24.539139 +v 75.136978 29.161577 25.118065 +v 76.430016 27.252514 31.839178 +v 76.192871 28.812696 30.606541 +v 73.640350 27.744471 17.338646 +v 59.737595 35.153580 11.238645 +v 57.033558 37.429771 9.417791 +v 57.383957 37.323380 9.547683 +v 57.948784 37.195251 9.676618 +v 66.152237 37.301643 7.901110 +v 60.313831 37.323380 8.984024 +v 65.636391 37.150532 8.280617 +v 63.723949 36.029697 9.904218 +v 59.561989 37.141602 9.461723 +v 59.482620 37.129417 9.494074 +v 59.182327 37.106617 9.583863 +v 58.879730 37.090370 9.664869 +v 52.760487 36.957199 11.028932 +v 59.495354 36.781719 9.979478 +v 61.001877 37.632607 8.148517 +v 60.781136 37.517174 8.488262 +v 56.435020 37.632607 9.027103 +v 53.517529 37.495701 9.940955 +v 63.791565 35.515163 10.255713 +v 48.386688 27.028641 15.206619 +v 61.211090 27.459644 5.708040 +v 61.226418 27.430767 6.176226 +v 61.205986 27.408089 6.550112 +v 61.134743 27.390244 6.854953 +v 60.971214 27.363319 7.325632 +v 60.838020 27.347515 7.609107 +v 60.622116 27.331280 7.915508 +v 60.195724 27.308439 8.370136 +v 62.185196 27.013065 12.806107 +v 59.683964 27.292637 8.726389 +v 59.169296 27.284966 8.950566 +v 58.869247 27.282921 9.041628 +v 58.559311 27.284145 9.081285 +v 58.246895 27.286190 9.108051 +v 57.960625 27.218948 10.260080 +v 57.265854 27.308439 8.933794 +v 56.701218 27.331280 8.669824 +v 52.922104 27.154799 12.275953 +v 56.203979 27.360157 8.294350 +v 55.343292 27.459644 6.836906 +v 55.795853 27.393810 7.823881 +v 51.655807 27.319469 9.833126 +v 52.696091 26.140705 28.863274 +v 52.477608 26.129791 29.083374 +v 52.356964 26.119545 29.273724 +v 52.320770 26.115250 29.350765 +v 53.619232 26.015087 30.735014 +v 53.580688 26.006310 30.885633 +v 53.538681 25.993240 31.106895 +v 53.519344 25.984016 31.261089 +v 53.514099 25.978874 31.346001 +v 53.514297 25.969343 31.501465 +v 53.548435 25.945717 31.880352 +v 53.564499 25.940407 31.963856 +v 53.528740 25.955399 31.726196 +v 50.701912 26.263660 27.241035 +v 51.045235 27.518551 6.702814 +v 68.942055 26.069374 26.901505 +v 68.835625 26.063675 27.014977 +v 68.781357 26.060287 27.080654 +v 68.690002 26.053646 27.206573 +v 68.569382 26.043402 27.396919 +v 68.494499 26.035927 27.533287 +v 68.458305 26.031631 27.610331 +v 65.226677 26.045692 28.002647 +v 65.188141 26.036915 28.153267 +v 65.168770 26.027691 28.307465 +v 65.168991 26.018158 28.462925 +v 65.156036 26.200050 25.498055 +v 65.188667 26.008476 28.617083 +v 73.143723 25.945717 28.110544 +v 73.159790 25.940407 28.194046 +v 73.179466 25.930725 28.348206 +v 73.193932 25.916780 28.572933 +v 73.194153 25.907249 28.728394 +v 73.188904 25.902105 28.813305 +v 73.169533 25.892883 28.967503 +v 73.127533 25.879814 29.188766 +v 73.088989 25.871037 29.339384 +v 73.036934 25.862686 29.485641 +v 72.979828 25.854492 29.630280 +v 72.943634 25.850197 29.707321 +v 74.179474 25.718853 31.612305 +v 74.088120 25.712212 31.738224 +v 73.981644 25.706511 31.851706 +v 73.861801 25.701836 31.951038 +v 71.952408 25.797098 30.764259 +v 71.877731 25.795456 30.805405 +v 71.736908 25.793043 30.871881 +v 71.594147 25.790958 30.933344 +v 71.446106 25.789745 30.981644 +v 71.185745 25.788862 31.046112 +v 70.920059 25.789745 31.082848 +v 70.764664 25.790958 31.092922 +v 70.609238 25.793043 31.088827 +v 70.453796 25.795456 31.079348 +v 70.369240 25.797098 31.068832 +v 70.216293 25.800669 31.040020 +v 66.496857 25.701836 33.367931 +v 70.064636 25.804543 31.005981 +v 69.706635 25.816748 30.875748 +v 63.728554 25.842720 31.602112 +v 68.333328 25.930725 29.280519 +v 68.403282 25.916780 29.494576 +v 65.285072 25.989264 28.911983 +v 68.497139 25.902105 29.715921 +v 68.572334 25.892883 29.851927 +v 68.693436 25.879814 30.041809 +v 65.225098 25.961918 29.369652 +v 63.656845 25.862686 31.290215 +v 63.644615 25.854492 31.426220 +v 63.608421 25.850197 31.503262 +v 69.359375 25.832478 30.685974 +v 69.490936 25.825836 30.769009 +v 69.565697 25.822449 30.809864 +v 57.482063 25.804543 33.426659 +v 59.748825 26.008476 29.663620 +v 58.429901 25.940407 31.027832 +v 58.449577 25.930725 31.181988 +v 58.464043 25.916780 31.406719 +v 58.464268 25.907249 31.562180 +v 58.459015 25.902105 31.647091 +v 58.439644 25.892883 31.801289 +v 58.397640 25.879814 32.022552 +v 58.359100 25.871037 32.173172 +v 58.018246 25.832478 32.867821 +v 57.926888 25.825836 32.993736 +v 57.872620 25.822449 33.059418 +v 57.766144 25.816748 33.172901 +v 57.601906 25.809219 33.327324 +v 69.916443 25.809219 30.958214 +v 57.222519 25.797098 33.598045 +v 64.145676 25.600410 35.474892 +v 57.147896 25.795456 33.639179 +v 57.007069 25.793043 33.705658 +v 56.864262 25.790958 33.767128 +v 56.716221 25.789745 33.815430 +v 56.455864 25.788862 33.879898 +v 56.190170 25.789745 33.916634 +v 55.879349 25.793043 33.922611 +v 55.639351 25.797098 33.902618 +v 54.020454 25.703117 35.747246 +v 53.810631 25.710648 35.664783 +v 53.640411 25.718853 35.563675 +v 54.055202 25.871037 33.001167 +v 53.842422 25.892883 32.685715 +v 53.603405 25.930725 32.114311 +v 51.837807 25.900763 32.942802 +v 53.673378 25.916780 32.328362 +v 53.730873 25.907249 32.472801 +v 53.767250 25.902105 32.549706 +v 53.963524 25.879814 32.875599 +v 54.157799 25.862686 33.117672 +v 53.520309 25.726330 35.464821 +v 54.761059 25.825836 33.602791 +v 55.486404 25.800669 33.873802 +v 55.723911 25.795456 33.913132 +v 56.034775 25.790958 33.926708 +v 73.104797 25.955399 27.960091 +v 72.567543 26.584049 17.807671 +v 73.034851 25.969343 27.746035 +v 72.977371 25.978874 27.601589 +v 72.940994 25.984016 27.524689 +v 72.865799 25.993240 27.388683 +v 72.744690 26.006310 27.198799 +v 72.653015 26.015087 27.073231 +v 72.550415 26.023438 26.956728 +v 72.443718 26.031631 26.843603 +v 72.381516 26.035927 26.785492 +v 72.261383 26.043402 26.686640 +v 72.078758 26.053646 26.554634 +v 71.947205 26.060287 26.471600 +v 71.872437 26.063675 26.430744 +v 71.731453 26.069374 26.364870 +v 71.521645 26.076904 26.282406 +v 71.373505 26.081579 26.234629 +v 71.221840 26.085455 26.200588 +v 71.068893 26.089024 26.171776 +v 70.984283 26.090666 26.161270 +v 70.828842 26.093081 26.151791 +v 70.673470 26.095165 26.147686 +v 70.518082 26.096378 26.157763 +v 69.158974 26.211733 24.537340 +v 69.010933 26.210518 24.585640 +v 68.870102 26.208103 24.652115 +v 68.738770 26.204535 24.735622 +v 69.106285 26.076904 26.747080 +v 67.398262 26.597212 18.587399 +v 68.084305 27.217953 8.328716 +v 65.597847 27.286371 7.690868 +v 61.040871 27.518551 4.779821 +v 59.709900 26.018158 29.513165 +v 59.652428 26.027691 29.368719 +v 59.577229 26.036915 29.232714 +v 59.485558 26.045692 29.107145 +v 57.713829 26.031631 29.677387 +v 57.651630 26.035927 29.619274 +v 57.531494 26.043402 29.520424 +v 57.348866 26.053646 29.388420 +v 57.217308 26.060287 29.305386 +v 57.142548 26.063675 29.264530 +v 57.001564 26.069374 29.198654 +v 56.791756 26.076904 29.116190 +v 56.643612 26.081579 29.068413 +v 56.491951 26.085455 29.034374 +v 56.339005 26.089024 29.005560 +v 56.254444 26.090666 28.995047 +v 56.099003 26.093081 28.985567 +v 55.943581 26.095165 28.981472 +v 55.788185 26.096378 28.991547 +v 54.145035 26.208767 27.474152 +v 54.070412 26.207127 27.515287 +v 57.247070 26.200050 27.019609 +v 54.395302 26.211733 27.377625 +v 55.114090 26.095165 29.141050 +v 54.496288 26.081579 29.481522 +v 52.531845 26.133177 29.017700 +v 69.957443 35.061996 10.766566 +v 66.685387 34.773247 16.106777 +v 62.306770 34.862022 15.500840 +v 59.330925 34.960144 14.472578 +v 56.864216 34.890060 16.090534 +v 50.802345 34.965748 16.021910 +v 66.124504 34.259998 24.587757 +v 63.323170 34.273285 24.909897 +v 54.735939 34.581673 21.530956 +v 58.492126 34.145573 27.922846 +v 58.730881 34.273285 25.793377 +v 55.755035 34.371407 24.765114 +v 65.089661 42.940437 21.855305 +v 65.018768 42.916645 21.486801 +v 64.496414 37.986919 18.771645 +v 65.488335 42.255898 23.927607 +v 64.648743 36.878342 19.563429 +v 65.007645 35.985512 21.428986 +v 64.858185 42.706097 20.652138 +v 65.543083 42.016376 24.212179 +v 65.344055 42.684242 23.177662 +v 64.783478 36.370399 20.263779 +v 64.437698 39.194977 18.466452 +v 64.847763 36.210472 20.597946 +v 64.937859 36.055786 21.066250 +v 65.543564 36.880680 24.214684 +v 64.466370 40.495289 18.615473 +v 64.583458 41.648415 19.224106 +v 64.535385 41.291088 18.974182 +v 64.772522 42.492416 20.206810 +v 64.925735 42.822464 21.003244 +v 65.184174 42.909203 22.346622 +v 64.535728 37.600716 18.975994 +v 64.470230 38.336449 18.635548 +v 64.437653 39.696312 18.466190 +v 64.186829 39.258270 19.809036 +v 64.186790 39.634270 19.808865 +v 64.194595 38.982391 19.849438 +v 64.208847 38.713844 19.923496 +v 64.194504 39.910194 19.848999 +v 64.229324 38.456863 20.029991 +v 64.255753 38.215500 20.167309 +v 64.208710 40.178814 19.922775 +v 64.287674 37.993568 20.333267 +v 64.324585 37.794567 20.525131 +v 64.229149 40.435905 20.029053 +v 64.365936 37.621628 20.740044 +v 64.411057 37.477478 20.974575 +v 64.459213 37.364395 21.224934 +v 64.509689 37.284168 21.487295 +v 64.561661 37.238045 21.757450 +v 64.595924 37.226723 21.935532 +v 64.648575 37.238281 22.209225 +v 64.700546 37.284657 22.479345 +v 64.750992 37.365158 22.741570 +v 64.799149 37.478485 22.991886 +v 64.844238 37.622871 23.226246 +v 64.885536 37.796024 23.440922 +v 64.922432 37.995224 23.632694 +v 64.954315 38.217316 23.798422 +v 64.255516 40.677399 20.166105 +v 64.287399 40.899498 20.331833 +v 64.324295 41.098694 20.523603 +v 64.365593 41.271851 20.738281 +v 64.410675 41.416233 20.972641 +v 64.458832 41.529564 21.222958 +v 64.509285 41.610062 21.485182 +v 64.561249 41.656445 21.755302 +v 64.595520 41.667942 21.933407 +v 64.648163 41.656673 22.207077 +v 64.700134 41.610554 22.477234 +v 64.750610 41.530323 22.739594 +v 64.798782 41.417244 22.989952 +v 64.843903 41.273090 23.224483 +v 64.885239 41.100151 23.439394 +v 64.922157 40.901154 23.631260 +v 64.954086 40.679214 23.797218 +v 64.980499 40.437859 23.934534 +v 65.000992 40.180882 24.041031 +v 65.015236 39.912327 24.115089 +v 65.023010 39.636444 24.155491 +v 64.980682 38.458817 23.935474 +v 65.001129 38.715904 24.041752 +v 65.015320 38.984528 24.115528 +v 65.023041 39.260452 24.155663 +v 65.608368 37.246300 24.551502 +v 65.479256 36.603928 23.880411 +v 65.333633 36.188622 23.123470 +v 65.266083 36.072254 22.772364 +v 65.721588 40.558273 25.140060 +v 65.745186 40.072754 25.262705 +v 64.501190 40.961510 18.796457 +v 64.712570 42.290787 19.895197 +v 65.253967 42.838928 22.709358 +v 64.446640 38.821968 18.512901 +v 64.591431 37.195278 19.265549 +v 64.444290 40.007515 18.500713 +v 64.850441 40.316677 17.965229 +v 64.703491 36.638817 19.848001 +v 65.690636 37.933212 24.979151 +v 65.173058 35.978073 22.288807 +v 65.600388 41.699448 24.510059 +v 65.695412 40.907799 25.003962 +v 64.648254 42.014042 19.560925 +v 65.408348 42.524315 23.511829 +v 65.754913 39.261070 25.313259 +v 65.743088 38.763664 25.251778 +v 65.725449 38.399429 25.160135 +v 65.656448 37.603630 24.801426 +v 65.419312 36.402306 23.568798 +v 65.102158 35.954281 21.920303 +v 65.656097 41.294003 24.799614 +v 65.753387 39.762222 25.305311 +v 66.341087 39.574478 25.713531 +v 67.550522 43.439468 21.413597 +v 67.034348 42.356140 18.730499 +v 67.272919 43.158401 19.970592 +v 67.187241 42.944725 19.525265 +v 66.868332 41.145340 17.867582 +v 66.833511 40.679123 17.686600 +v 66.969543 41.990517 18.393681 +v 68.302063 39.825142 25.320065 +v 67.362976 43.313560 20.438702 +v 67.456001 43.407745 20.922256 +v 67.107300 42.675888 19.109715 +v 68.778412 40.316677 17.209553 +v 66.913918 41.584785 18.104534 +v 67.645050 43.408234 21.904913 +v 67.738091 43.314533 22.388565 +v 68.066895 42.358826 24.097664 +v 67.828186 43.159847 22.856865 +v 68.268051 40.682858 25.143246 +v 68.233139 41.148895 24.961798 +v 67.993881 42.678200 23.718168 +v 68.291641 40.197338 25.265890 +v 68.131775 41.993538 24.434887 +v 68.187477 41.588097 24.724442 +v 67.913895 42.946617 23.302391 +v 65.674019 35.426590 22.246143 +v 65.723358 35.394489 22.502598 +v 65.735397 35.346016 22.565155 +v 65.757790 34.837643 22.681578 +v 65.761017 35.517830 22.698362 +v 65.843590 35.101654 23.127579 +v 66.269852 35.256065 25.343250 +v 66.416916 38.169350 26.107695 +v 66.382095 37.703129 25.926710 +v 66.492859 36.014885 26.502462 +v 66.269112 35.984661 25.339407 +v 66.428741 38.666756 26.169178 +v 64.613541 37.162949 16.733797 +v 64.730621 39.002335 17.342384 +v 64.742538 38.504982 17.404335 +v 64.760979 36.642624 17.500193 +v 64.816681 36.237186 17.789747 +v 64.889694 35.917809 18.169243 +v 64.975403 35.704582 18.614769 +v 65.068451 35.610878 19.098419 +v 64.732651 39.377975 17.352962 +v 64.840652 40.194908 17.914326 +v 68.306084 39.698807 25.340954 +v 66.799286 40.196674 17.508701 +v 69.246307 39.717827 19.641657 +v 69.254829 39.893875 19.685951 +v 69.267456 40.062881 19.751556 +v 69.283974 40.222176 19.837425 +v 69.304146 40.369247 19.942272 +v 69.401505 42.101158 20.448341 +v 69.327637 40.501785 20.064381 +v 69.354073 40.617699 20.201805 +v 69.295464 41.912445 19.897152 +v 69.414116 40.792618 20.513897 +v 69.465240 42.155445 20.779654 +v 69.529755 42.167023 21.114988 +v 69.564011 42.155701 21.293070 +v 69.627754 42.101746 21.624390 +v 69.689720 42.005928 21.946501 +v 69.748917 41.869766 22.254183 +v 69.804428 41.695389 22.542747 +v 69.855377 41.485569 22.807545 +v 69.900948 41.243603 23.044445 +v 69.940445 40.973309 23.249748 +v 68.936630 38.506760 18.031940 +v 68.665970 39.376205 16.625072 +v 69.030823 40.362648 18.521603 +v 69.076416 40.802090 18.758553 +v 69.246361 39.175495 19.641916 +v 69.141212 41.167717 19.095371 +v 69.183998 41.898907 19.317736 +v 69.242020 39.537514 19.619389 +v 69.242035 39.355782 19.619465 +v 69.254913 38.999493 19.686373 +v 69.267563 38.830551 19.752121 +v 69.284119 38.671345 19.838196 +v 69.327827 38.391960 20.065372 +v 68.940742 36.495640 18.053347 +v 68.775116 33.038315 17.192390 +v 69.151268 35.496304 19.147636 +v 69.414360 38.101585 20.515175 +v 69.315269 35.313622 20.000111 +v 69.480743 38.011539 20.860268 +v 69.482346 35.230610 20.868580 +v 69.549271 38.011723 21.216454 +v 69.649551 35.248451 21.737675 +v 69.615646 38.102104 21.561440 +v 69.646698 38.179569 21.722876 +v 69.675690 38.277023 21.873531 +v 69.702126 38.392933 22.010956 +v 69.725616 38.525467 22.133064 +v 69.745789 38.672543 22.237911 +v 70.106247 38.399223 24.111582 +v 69.774925 39.000851 22.389387 +v 69.783447 39.176891 22.433680 +v 69.973213 40.678951 23.420073 +v 69.998764 40.365173 23.552853 +v 70.016678 40.036911 23.645977 +v 70.027100 39.664719 23.700153 +v 69.787735 39.357204 22.455948 +v 70.204453 35.195450 24.622036 +v 70.357727 38.791084 25.418762 +v 70.414948 35.515854 25.716198 +v 70.361145 32.844612 25.436504 +v 70.522758 33.043049 26.276573 +v 67.908577 34.949272 12.688181 +v 68.032288 34.592869 13.331255 +v 68.271942 33.972191 14.576957 +v 68.520065 33.452927 15.866659 +v 47.518276 29.974739 29.768909 +v 49.062492 36.014885 29.855774 +v 49.523602 33.480202 29.605947 +v 60.444347 33.147514 27.483829 +v 66.600204 37.162949 16.351595 +v 66.577850 35.267597 16.235415 +v 44.535973 29.886997 14.267043 +v 45.046333 30.970798 16.919865 +v 45.691010 35.537415 20.270847 +v 46.452789 38.671345 24.230560 +v 46.180412 40.000202 22.814762 +v 45.829292 39.377975 20.989655 +v 46.072453 38.822670 22.253582 +v 46.415031 39.175495 24.034283 +v 46.410694 39.537514 24.011753 +v 46.199501 40.362648 22.913969 +v 46.224991 40.676567 23.046459 +v 46.257706 40.971096 23.216526 +v 46.410709 39.355782 24.011831 +v 46.436234 38.830551 24.144489 +v 46.472984 38.524376 24.335537 +v 46.496498 38.391960 24.457741 +v 46.522964 38.276184 24.595325 +v 46.551964 38.178879 24.746048 +v 46.583031 38.101585 24.907541 +v 46.615692 38.045509 25.077320 +v 46.649422 38.011539 25.252636 +v 47.111088 35.573845 27.652359 +v 46.717945 38.011723 25.608820 +v 46.751671 38.045860 25.784130 +v 46.784313 38.102104 25.953808 +v 47.193069 35.685493 28.078493 +v 47.196854 39.573856 28.098166 +v 47.046555 42.946617 27.316921 +v 46.952118 39.176891 26.826046 +v 47.126541 42.678200 27.732697 +v 47.187263 39.306942 28.048302 +v 47.199551 42.358826 28.112190 +v 46.930977 38.831844 26.716148 +v 47.264427 41.993538 28.449417 +v 46.914459 38.672543 26.630280 +v 47.320133 41.588097 28.738972 +v 46.894287 38.525467 26.525433 +v 47.365799 41.148895 28.976326 +v 46.870792 38.392933 26.403322 +v 47.400703 40.682858 29.157772 +v 45.480831 31.700232 19.178362 +v 47.424301 40.197338 29.280418 +v 46.844353 38.277023 26.265900 +v 45.878170 27.331514 21.243702 +v 47.526398 38.791084 29.811127 +v 46.297134 41.241596 23.421474 +v 46.342674 41.483795 23.658192 +v 46.393585 41.693878 23.922800 +v 46.449059 41.868538 24.211163 +v 46.508240 42.005016 24.518784 +v 46.570171 42.101158 24.840708 +v 46.633911 42.155445 25.172020 +v 46.668175 42.166943 25.350122 +v 46.732685 42.155701 25.685436 +v 46.796425 42.101746 26.016754 +v 46.858391 42.005928 26.338867 +v 46.815136 40.715843 26.114021 +v 46.844131 40.618538 26.264744 +v 46.870602 40.502762 26.402330 +v 46.894112 40.370346 26.524532 +v 46.914307 40.223373 26.629509 +v 46.930866 40.064163 26.715582 +v 46.943516 39.895233 26.781330 +v 46.952072 39.719231 26.825788 +v 46.956390 39.538940 26.848240 +v 48.911728 39.698807 29.072104 +v 47.415627 40.193138 21.295424 +v 47.675732 37.295708 20.000786 +v 47.628216 33.266563 19.753813 +v 48.156170 43.439468 25.144745 +v 47.639988 42.356140 22.461651 +v 48.477951 43.067505 26.817360 +v 47.878563 43.158401 23.701742 +v 47.792885 42.944725 23.256416 +v 47.473976 41.145340 21.598732 +v 47.439159 40.679123 21.417747 +v 47.575188 41.990517 22.124830 +v 48.343739 43.314533 26.119713 +v 48.907711 39.825142 29.051216 +v 50.346680 40.682858 28.591015 +v 47.968620 43.313560 24.169849 +v 48.061646 43.407745 24.653408 +v 47.712944 42.675888 22.840866 +v 48.893063 40.316677 21.035164 +v 47.519562 41.584785 21.835682 +v 48.250694 43.408234 25.636063 +v 50.145531 42.358826 27.545433 +v 49.906822 43.159847 26.304638 +v 50.072521 42.678200 27.165939 +v 50.370277 40.197338 28.713661 +v 50.210403 41.993538 27.882660 +v 50.266113 41.588097 28.172215 +v 50.311775 41.148895 28.409569 +v 49.991524 42.940437 24.759933 +v 50.086044 42.909203 25.251251 +v 50.155830 42.838928 25.613989 +v 49.550606 36.878342 22.468058 +v 50.930565 39.260826 27.656063 +v 50.445427 36.880680 27.119312 +v 50.801670 41.356995 26.986065 +v 50.202412 36.119190 25.856136 +v 50.381119 36.603928 26.785040 +v 50.577362 37.762726 27.805096 +v 50.627316 38.399429 28.064766 +v 49.674377 42.492416 23.111441 +v 49.437592 37.600716 21.880623 +v 50.310211 42.524315 26.416458 +v 50.612438 40.737396 27.987434 +v 49.955616 39.910194 22.588322 +v 49.946747 39.537140 22.542234 +v 49.969810 40.178814 22.662100 +v 49.990257 40.435905 22.768379 +v 49.947929 39.258270 22.548363 +v 50.016624 40.677399 22.905432 +v 50.048508 40.899498 23.071156 +v 49.955704 38.982391 22.588764 +v 50.085400 41.098694 23.262928 +v 50.126701 41.271851 23.477606 +v 49.969948 38.713844 22.662821 +v 50.171787 41.416233 23.711967 +v 50.219940 41.529564 23.962282 +v 50.270393 41.610062 24.224506 +v 50.322357 41.656445 24.494629 +v 50.375011 41.667992 24.768320 +v 50.409271 41.656673 24.946400 +v 50.461243 41.610554 25.216558 +v 50.511719 41.530323 25.478918 +v 50.559883 41.417244 25.729279 +v 50.605003 41.273090 25.963810 +v 50.646351 41.100151 26.178719 +v 50.576584 40.370346 25.816086 +v 50.715187 40.679214 26.536543 +v 50.741608 40.437859 26.673859 +v 49.990437 38.456863 22.769318 +v 50.016853 38.215500 22.906635 +v 50.048782 37.993568 23.072594 +v 50.085693 37.794567 23.264458 +v 50.127037 37.621628 23.479366 +v 50.172157 37.477478 23.713902 +v 50.220322 37.364395 23.964260 +v 50.270798 37.284168 24.226620 +v 50.304527 37.250198 24.401936 +v 50.357033 37.226723 24.674858 +v 50.409683 37.238281 24.948549 +v 50.461651 37.284657 25.218670 +v 50.512100 37.365158 25.480896 +v 50.560257 37.478485 25.731211 +v 50.605343 37.622871 25.965572 +v 50.646645 37.796024 26.180248 +v 50.683540 37.995224 26.372021 +v 50.715420 38.217316 26.537746 +v 50.741787 38.458817 26.674799 +v 50.762230 38.715904 26.781078 +v 50.776428 38.984528 26.854855 +v 50.762093 40.180882 26.780357 +v 50.776344 39.912327 26.854416 +v 50.784111 39.636444 26.894815 +v 50.638874 39.357204 26.139870 +v 50.532898 37.391502 27.573999 +v 49.485321 41.648415 22.128735 +v 49.760056 42.706097 23.556768 +v 49.339561 39.194977 21.371080 +v 49.372093 38.336449 21.540176 +v 49.493294 37.195278 22.170177 +v 49.605350 36.638817 22.752630 +v 49.716534 36.279732 23.330555 +v 50.366314 42.510319 26.046408 +v 50.659489 41.160637 27.570328 +v 50.039612 35.954376 25.009912 +v 50.321167 36.402306 26.473427 +v 50.400372 37.067734 27.546759 +v 50.644947 38.763664 28.156406 +v 49.368233 40.495289 21.520103 +v 49.614429 42.290787 22.799826 +v 49.348503 38.821968 21.417532 +v 49.874199 36.009033 24.150084 +v 50.418999 42.145252 26.981943 +v 50.653732 39.887733 28.202087 +v 50.747757 39.657757 28.029158 +v 49.403049 40.961510 21.701086 +v 49.338776 39.633644 21.366978 +v 49.550121 42.014042 22.465553 +v 49.398277 37.986919 21.676275 +v 49.437241 41.291088 21.878811 +v 49.920631 42.916645 24.391430 +v 49.827599 42.822464 23.907873 +v 49.346153 40.007515 21.405342 +v 71.016159 34.956318 12.079202 +v 71.431984 32.480591 22.621675 +v 70.634117 32.688740 18.474390 +v 70.898766 32.508747 19.850039 +v 71.165276 32.439228 21.235346 +v 71.697258 32.632572 24.000555 +v 73.622032 33.026604 25.624420 +v 67.059769 40.894501 21.509285 +v 48.524654 40.894501 25.075130 +v 65.968369 39.386066 20.247313 +v 48.866055 39.325497 23.541418 +v 67.332733 39.538940 22.928169 +v 49.411007 39.569225 26.374060 +v 66.885178 40.564144 20.601799 +v 48.337540 40.501785 24.102524 +v 66.885391 38.329666 20.602873 +v 48.325188 38.454250 24.038315 +v 65.991806 38.858006 20.369154 +v 48.893658 38.803097 23.684891 +v 67.220482 40.618538 22.344675 +v 49.303757 40.600708 25.816574 +v 66.943146 40.759056 20.903112 +v 48.424019 40.792618 24.552040 +v 65.983292 39.922745 20.324879 +v 48.264740 39.893875 23.724094 +v 65.974068 39.747639 20.276953 +v 48.256218 39.717827 23.679800 +v 66.013733 40.247807 20.483109 +v 48.293877 40.222176 23.875568 +v 66.928314 38.178879 20.825979 +v 49.001919 38.193485 24.247637 +v 66.085625 38.258358 20.856817 +v 48.973309 38.294010 24.098902 +v 66.992035 38.045509 21.157251 +v 49.065121 38.053043 24.576147 +v 66.849335 38.524376 20.415468 +v 65.979828 39.028351 20.306885 +v 48.264816 38.999493 23.724516 +v 67.328415 39.719231 22.905720 +v 49.405975 39.749039 26.347891 +v 66.991776 40.848862 21.155872 +v 48.456665 40.848862 24.721718 +v 67.025497 40.883003 21.331181 +v 48.490391 40.883003 24.897026 +v 48.363979 40.617699 24.239948 +v 66.104713 40.700520 20.956072 +v 48.392960 40.715153 24.390606 +v 65.969078 39.567806 20.250975 +v 48.865322 39.507221 23.537600 +v 65.996582 40.090347 20.393955 +v 48.277359 40.062881 23.789700 +v 66.034492 40.392639 20.590998 +v 48.314049 40.369247 23.980415 +v 66.146385 38.090427 21.172623 +v 49.032681 38.112743 24.407526 +v 67.042831 38.000175 21.421257 +v 49.098713 38.015327 24.750772 +v 66.007729 38.696960 20.451933 +v 48.910847 38.645729 23.774256 +v 65.971977 39.205303 20.266043 +v 48.871090 39.145687 23.567587 +v 67.094025 40.883179 21.687366 +v 48.558914 40.883179 25.253212 +v 67.246948 40.502762 22.482262 +v 49.329762 40.481998 25.951767 +v 66.448647 38.502075 22.743797 +v 49.352921 38.548859 26.072130 +v 67.307213 40.064163 22.795513 +v 49.383404 40.091618 26.230585 +v 66.247437 38.000233 21.697876 +v 48.524918 38.000221 25.076492 +v 67.290657 40.223373 22.709440 +v 49.366215 40.248989 26.141220 +v 67.234619 38.330574 22.418133 +v 49.329952 38.413719 25.952730 +v 66.347664 38.113293 22.218864 +v 49.233967 38.090916 25.453814 +v 67.319870 39.895233 22.861263 +v 49.396725 39.924091 26.299814 +v 67.160416 40.793137 22.032459 +v 48.625305 40.793137 25.598307 +v 67.191483 40.715843 22.193954 +v 49.275143 40.701237 25.667837 +v 67.127754 40.849213 21.862682 +v 48.592644 40.849213 25.428528 +v 67.270462 40.370346 22.604462 +v 66.514061 39.326916 23.083822 +v 48.796566 39.266331 26.488522 +v 66.509064 39.147079 23.057844 +v 48.671680 38.223469 25.839373 +v 66.378418 38.194202 22.378723 +v 49.265331 38.164936 25.616827 +v 67.128021 38.045860 21.864059 +v 49.201099 38.038300 25.282965 +v 66.281654 38.015545 21.875753 +v 49.167236 38.007904 25.106951 +v 67.307327 38.831844 22.796080 +v 48.779526 38.914249 26.399937 +v 66.469406 38.646912 22.851690 +v 49.372509 38.698177 26.173935 +v 66.499847 38.971985 23.009918 +v 49.400349 39.029720 26.318655 +v 66.232704 34.574276 5.343794 +v 66.250427 34.574375 5.540986 +v 51.084633 34.574375 8.458632 +v 51.027924 34.574276 8.268940 +v 65.949280 33.133568 3.827172 +v 55.146141 33.030170 5.898943 +v 60.991566 31.791014 7.665103 +v 61.027931 31.720028 7.581254 +v 56.207394 31.727734 8.516941 +v 61.135521 34.127472 5.271808 +v 50.824596 34.127472 7.255457 +v 66.006470 31.957125 4.124418 +v 55.217026 31.877304 6.267393 +v 61.052761 32.417946 4.841643 +v 55.160805 32.417946 5.975157 +v 66.125687 34.420193 4.744131 +v 50.923000 34.458382 7.766957 +v 61.067032 33.627991 4.915816 +v 55.175076 33.627991 6.049330 +v 61.108791 33.976646 5.132885 +v 50.797867 33.976646 7.116533 +v 61.198849 31.484156 5.600996 +v 55.306892 31.484156 6.734510 +v 61.067162 32.225723 4.916492 +v 55.175205 32.225723 6.050006 +v 61.043030 32.618195 4.791047 +v 55.151070 32.618195 5.924561 +v 66.202370 33.629436 6.962537 +v 56.448875 33.629436 8.838947 +v 60.956619 31.878563 7.747334 +v 56.328194 31.878563 8.637766 +v 66.172409 34.510643 4.986992 +v 61.273354 34.536453 6.031681 +v 55.405556 34.536453 7.160548 +v 50.987167 34.548199 8.057079 +v 50.960152 34.522385 7.960084 +v 66.211365 34.561234 5.232846 +v 55.433598 34.561234 7.306308 +v 61.165611 34.259357 5.428216 +v 50.854687 34.259357 7.411866 +v 61.198586 34.370247 5.599622 +v 50.887661 34.370247 7.583271 +v 61.052670 33.435703 4.841157 +v 55.160713 33.435703 5.974671 +v 61.042969 33.235359 4.790749 +v 55.151016 33.235359 5.924263 +v 61.085835 33.809242 5.013558 +v 50.774910 33.809242 6.997207 +v 61.255272 31.332344 5.981068 +v 55.395523 31.332344 7.108386 +v 61.165855 31.594851 5.429498 +v 50.839096 31.655691 7.330834 +v 61.135742 31.726589 5.272964 +v 55.243786 31.726589 6.406479 +v 61.225929 31.413540 5.741743 +v 61.228027 31.375412 5.839467 +v 64.514832 31.369623 5.222026 +v 55.333969 31.413540 6.875257 +v 55.368279 31.375412 6.966784 +v 52.414326 31.369623 7.549962 +v 61.276142 31.305359 6.089562 +v 61.276070 31.292431 6.194254 +v 55.455307 31.292431 7.314072 +v 55.416393 31.305359 7.216879 +v 61.086006 32.044556 5.014452 +v 55.194054 32.044556 6.147966 +v 61.038113 32.823364 4.765495 +v 55.146156 32.823364 5.899009 +v 60.764568 33.031796 8.165266 +v 51.803696 32.928387 9.895829 +v 60.887939 33.517090 8.036115 +v 60.855103 33.418602 8.075045 +v 56.499119 33.517090 8.880450 +v 56.544056 33.418602 8.904418 +v 66.226974 33.810654 6.856220 +v 56.350979 33.810654 8.756198 +v 66.280647 34.128571 6.577813 +v 66.242142 34.057674 6.662187 +v 56.113045 34.115154 8.548443 +v 56.045982 34.186050 8.484377 +v 66.296783 34.371017 6.234537 +v 55.870380 34.371017 8.240402 +v 60.900528 32.045918 7.881865 +v 51.684113 32.133430 9.711477 +v 60.792236 32.419456 8.081479 +v 56.604824 32.419456 8.887068 +v 66.113503 32.721592 7.129715 +v 51.801254 32.721592 9.883153 +v 61.155338 31.484911 7.225088 +v 55.949890 31.484911 8.226529 +v 61.211418 31.376360 6.971399 +v 61.188618 31.414534 7.073867 +v 55.835308 31.396793 8.058090 +v 61.268269 31.293900 6.605762 +v 55.615234 31.293900 7.693312 +v 61.272095 31.280830 6.394876 +v 55.533432 31.280830 7.498899 +v 60.801880 33.236969 8.131590 +v 56.614464 33.236969 8.937180 +v 61.074768 34.028671 7.680733 +v 61.036625 33.948902 7.755341 +v 56.233929 33.977905 8.654870 +v 66.288780 34.260307 6.413921 +v 61.186584 34.256195 7.401259 +v 55.982620 34.260307 8.396654 +v 66.286667 34.522789 5.845553 +v 51.163979 34.522789 8.754906 +v 66.302574 34.458981 6.042809 +v 61.280937 34.455750 7.017253 +v 55.756145 34.455750 8.080130 +v 51.222404 34.458981 8.943981 +v 61.310783 34.561440 6.595968 +v 55.572117 34.561440 7.699992 +v 60.841263 32.244884 8.003099 +v 60.871834 32.151146 7.952385 +v 56.530216 32.244884 8.832472 +v 56.483009 32.151146 8.796721 +v 61.130646 31.546391 7.338381 +v 61.093697 31.607336 7.430837 +v 56.014854 31.546391 8.322573 +v 56.083473 31.607336 8.394720 +v 61.266319 31.309143 6.719496 +v 61.244133 31.334991 6.825958 +v 55.659241 31.309143 7.798204 +v 55.719345 31.334991 7.888835 +v 50.692825 29.737175 4.871002 +v 46.215881 29.235327 5.986284 +v 69.947617 29.737175 1.166701 +v 60.688461 29.737175 2.948009 +v 54.613262 33.641586 3.129050 +v 60.505219 33.641586 1.995536 +v 76.120895 26.923138 33.255138 +v 74.296257 27.851955 29.481903 +v 72.385330 27.945814 28.318336 +v 72.319244 27.958984 28.116190 +v 72.228294 27.971840 27.923962 +v 72.113907 27.984182 27.744629 +v 71.977898 27.995813 27.581039 +v 71.822403 28.006550 27.435783 +v 71.649887 28.016228 27.311081 +v 71.463081 28.024693 27.208948 +v 71.264900 28.031803 27.131058 +v 71.058472 28.037455 27.078587 +v 70.847054 28.041557 27.052334 +v 70.634003 28.044043 27.052769 +v 71.147552 28.113617 25.818951 +v 70.422714 28.044874 27.079849 +v 69.059708 27.945814 28.958130 +v 69.046066 27.958984 28.745895 +v 69.059204 27.971840 28.533642 +v 69.098892 27.984182 28.324667 +v 69.164490 27.995813 28.122292 +v 69.254990 28.006550 27.929712 +v 69.368919 28.016228 27.749901 +v 69.504501 28.024693 27.585747 +v 69.659637 28.031803 27.439884 +v 69.831863 28.037455 27.314566 +v 70.018402 28.041557 27.211752 +v 70.216408 28.044043 27.133106 +v 72.439148 27.919369 28.739426 +v 72.426010 27.906513 28.951679 +v 72.386322 27.894171 29.160656 +v 72.320724 27.882540 29.363029 +v 72.230225 27.871803 29.555611 +v 72.116295 27.862123 29.735420 +v 71.980713 27.853661 29.899574 +v 71.825577 27.846548 30.045439 +v 71.653351 27.840897 30.170757 +v 71.466766 27.836796 30.273579 +v 71.268753 27.834311 30.352226 +v 71.062500 27.833479 30.405472 +v 70.851166 27.834311 30.432564 +v 67.698425 27.939177 29.328310 +v 69.099884 27.932537 29.166986 +v 69.165970 27.919369 29.369133 +v 72.203445 27.764736 31.307426 +v 70.638107 27.836796 30.432999 +v 70.426743 27.840897 30.406734 +v 70.220314 27.846548 30.354265 +v 70.022133 27.853661 30.276375 +v 69.835327 27.862123 30.174240 +v 69.662811 27.871803 30.049538 +v 69.507317 27.882540 29.904284 +v 69.371307 27.894171 29.740694 +v 69.256920 27.906513 29.561359 +v 67.182846 29.161577 26.648310 +v 68.410538 30.035097 29.324543 +v 74.056992 30.035097 28.238258 +v 72.954063 28.962406 31.239151 +v 68.927895 28.962406 32.013718 +v 71.274086 28.064154 31.505253 +v 68.559402 29.311287 26.393002 +v 70.218193 28.413034 26.016777 +v 71.898178 29.311287 25.750675 +v 59.056892 27.939177 30.990797 +v 57.655434 27.945814 31.152121 +v 57.589355 27.958984 30.949974 +v 57.498402 27.971840 30.757748 +v 57.384010 27.984182 30.578415 +v 57.248009 27.995813 30.414824 +v 57.092514 28.006550 30.269569 +v 56.919998 28.016228 30.144867 +v 56.733192 28.024693 30.042732 +v 56.535011 28.031803 29.964842 +v 56.328579 28.037455 29.912373 +v 56.117161 28.041557 29.886118 +v 55.904106 28.044043 29.886555 +v 55.692818 28.044874 29.913635 +v 54.551880 28.113617 29.011681 +v 55.486565 28.044043 29.966881 +v 54.329819 27.945814 31.791914 +v 54.316181 27.958984 31.579679 +v 54.329308 27.971840 31.367428 +v 54.368988 27.984182 31.158455 +v 54.434601 27.995813 30.956078 +v 54.525097 28.006550 30.763496 +v 54.639030 28.016228 30.583687 +v 54.774609 28.024693 30.419533 +v 54.929749 28.031803 30.273668 +v 55.101974 28.037455 30.148350 +v 55.288559 28.041557 30.045528 +v 57.695614 27.932537 31.360977 +v 57.709255 27.919369 31.573212 +v 57.696117 27.906513 31.785465 +v 57.656433 27.894171 31.994438 +v 57.590836 27.882540 32.196815 +v 57.500336 27.871803 32.389397 +v 57.386406 27.862123 32.569206 +v 57.250824 27.853661 32.733360 +v 57.095688 27.846548 32.879223 +v 56.923462 27.840897 33.004539 +v 56.736877 27.836796 33.107365 +v 56.538868 27.834311 33.186012 +v 56.332615 27.833479 33.239258 +v 56.121326 27.834311 33.266338 +v 55.908272 27.836796 33.266777 +v 54.369999 27.932537 32.000771 +v 54.436081 27.919369 32.202915 +v 54.527027 27.906513 32.395145 +v 54.641411 27.894171 32.574478 +v 54.777424 27.882540 32.738068 +v 55.607769 27.764736 34.500156 +v 55.696854 27.840897 33.240520 +v 55.490425 27.846548 33.188049 +v 55.292240 27.853661 33.110161 +v 55.105431 27.862123 33.008026 +v 54.932915 27.871803 32.883324 +v 58.394215 30.035097 31.251516 +v 52.747757 30.035097 32.337799 +v 53.829506 29.311287 29.226784 +v 57.855675 29.311287 28.452217 +v 55.488300 28.413034 28.850563 +v 59.597198 28.812696 33.799271 +v 58.224174 28.962406 34.072937 +v 56.544193 28.064154 34.339039 +v 54.885399 28.962406 34.715263 +v 71.292938 28.057924 31.603260 +v 70.199333 28.419264 25.918770 +v 68.942963 30.537178 28.386755 +v 68.562332 30.362362 26.408234 +v 69.423767 30.378311 30.885975 +v 68.996002 30.219070 28.662430 +v 69.535141 30.191223 31.464848 +v 69.624710 28.959290 31.930481 +v 72.873917 30.191223 30.822521 +v 70.337189 30.477804 26.635292 +v 69.659355 29.816544 26.111471 +v 71.901108 30.362362 25.765907 +v 72.281738 30.537178 27.744431 +v 72.334778 30.219070 28.020103 +v 72.974686 29.105885 31.346336 +v 72.762543 30.378311 30.243649 +v 72.457283 30.627293 28.692375 +v 72.470924 30.614124 28.904610 +v 72.457787 30.601269 29.116861 +v 72.418106 30.588924 29.325836 +v 72.352501 30.577293 29.528212 +v 72.262001 30.566557 29.720791 +v 72.148071 30.556877 29.900600 +v 72.012497 30.548416 30.064758 +v 71.857361 30.541304 30.210621 +v 71.685135 30.535652 30.335938 +v 71.498550 30.531551 30.438761 +v 71.300537 30.529064 30.517406 +v 71.094284 30.528233 30.570656 +v 70.207520 30.528126 30.742981 +v 70.669891 30.531551 30.598179 +v 70.458519 30.535652 30.571915 +v 70.252090 30.541304 30.519445 +v 70.053909 30.548416 30.441555 +v 69.867096 30.556877 30.339422 +v 69.694580 30.566557 30.214722 +v 69.539093 30.577293 30.069466 +v 69.403091 30.588924 29.905874 +v 69.288696 30.601269 29.726540 +v 69.197739 30.614124 29.534313 +v 69.131668 30.627293 29.332167 +v 72.417099 30.640570 28.483517 +v 72.351028 30.653740 28.281372 +v 72.260071 30.666595 28.089146 +v 72.145676 30.678936 27.909809 +v 72.009674 30.690569 27.746220 +v 71.854187 30.701305 27.600964 +v 71.681671 30.710983 27.476261 +v 71.494858 30.719448 27.374128 +v 71.296677 30.726559 27.296238 +v 71.090248 30.732208 27.243767 +v 70.878830 30.736313 27.217514 +v 70.665771 30.738798 27.217949 +v 70.454483 30.739630 27.245029 +v 70.453842 30.739841 27.241695 +v 70.248184 30.738798 27.298286 +v 70.050179 30.736313 27.376934 +v 69.863647 30.732208 27.479748 +v 69.691414 30.726559 27.605064 +v 69.536270 30.719448 27.750927 +v 69.400696 30.710983 27.915085 +v 69.286766 30.701305 28.094894 +v 69.196274 30.690569 28.287472 +v 69.130669 30.678936 28.489849 +v 69.090981 30.666595 28.698824 +v 69.077843 30.653740 28.911076 +v 69.091484 30.640570 29.123310 +v 71.007957 30.540230 27.122408 +v 56.563049 28.057924 34.437046 +v 55.469444 28.419264 28.752552 +v 54.213074 30.537178 31.220539 +v 53.832436 30.362362 29.242018 +v 54.693878 30.378311 33.719757 +v 54.266106 30.219070 31.496214 +v 54.805244 30.191223 34.298630 +v 54.902679 29.357473 34.805073 +v 58.144020 30.191223 33.656307 +v 55.607292 30.477804 29.469078 +v 54.929462 29.816544 28.945257 +v 57.158852 29.314400 28.535456 +v 57.284344 30.324982 29.187744 +v 57.551849 30.537178 30.578217 +v 57.814846 30.099600 31.945259 +v 58.244793 29.105885 34.180122 +v 58.032654 30.378311 33.077431 +v 57.727394 30.627293 31.526159 +v 57.741032 30.614124 31.738392 +v 57.727894 30.601269 31.950645 +v 57.688210 30.588924 32.159622 +v 57.622612 30.577293 32.362000 +v 57.532112 30.566557 32.554577 +v 57.418182 30.556877 32.734386 +v 57.282600 30.548416 32.898544 +v 57.127464 30.541304 33.044403 +v 56.955238 30.535652 33.169724 +v 56.768654 30.531551 33.272545 +v 56.570648 30.529064 33.351189 +v 56.364391 30.528233 33.404442 +v 55.477631 30.528126 33.576767 +v 55.940048 30.531551 33.431953 +v 55.728630 30.535652 33.405701 +v 55.522202 30.541304 33.353230 +v 55.324017 30.548416 33.275337 +v 55.137207 30.556877 33.173206 +v 54.964695 30.566557 33.048508 +v 54.809204 30.577293 32.903252 +v 54.673187 30.588924 32.739662 +v 54.558804 30.601269 32.560326 +v 54.467861 30.614124 32.368095 +v 54.401779 30.627293 32.165955 +v 57.687210 30.640570 31.317303 +v 57.621132 30.653740 31.115156 +v 57.530182 30.666595 30.922928 +v 57.415791 30.678936 30.743595 +v 57.279785 30.690569 30.580006 +v 57.124294 30.701305 30.434750 +v 56.951782 30.710983 30.310047 +v 56.764969 30.719448 30.207914 +v 56.566788 30.726559 30.130024 +v 56.360359 30.732208 30.077553 +v 56.148941 30.736313 30.051300 +v 55.935886 30.738798 30.051735 +v 55.724598 30.739630 30.078815 +v 55.723957 30.739841 30.075481 +v 55.518341 30.738798 30.132063 +v 55.320335 30.736313 30.210709 +v 55.133751 30.732208 30.313532 +v 54.961525 30.726559 30.438850 +v 54.806385 30.719448 30.584713 +v 54.670807 30.710983 30.748867 +v 54.556877 30.701305 30.928679 +v 54.466377 30.690569 31.121258 +v 54.400768 30.678936 31.323635 +v 54.361088 30.666595 31.532608 +v 54.347961 30.653740 31.744860 +v 54.361595 30.640570 31.957096 +v 56.278065 30.540230 29.956192 +v 72.420639 30.939987 28.501871 +v 72.379440 27.446785 28.287746 +v 69.135193 30.926712 29.350521 +v 69.094002 27.433510 29.136396 +v 71.056618 27.334450 30.374886 +v 71.097809 30.827650 30.589008 +v 69.742195 27.617165 30.102001 +v 69.870636 30.856297 30.357777 +v 69.363037 27.517200 27.719313 +v 69.339844 30.856140 28.010603 +v 69.825974 27.538425 27.283978 +v 69.867172 31.031628 27.498100 +v 69.365425 27.395142 29.710104 +v 69.406616 30.888344 29.924229 +v 71.052582 27.538425 27.047997 +v 71.093781 31.031628 27.262123 +v 71.457199 27.525665 27.178358 +v 71.498390 31.018864 27.392483 +v 69.093002 27.485153 28.294079 +v 69.134201 30.978355 28.508202 +v 69.653748 27.532776 27.409294 +v 69.694946 31.025976 27.623417 +v 70.012512 27.542528 27.181164 +v 70.053711 31.035728 27.395287 +v 71.819687 27.347519 30.014851 +v 71.860886 30.840721 30.228973 +v 70.420860 27.341869 30.376144 +v 70.462051 30.835072 30.590271 +v 70.016243 27.354631 30.245785 +v 70.057442 30.847832 30.459909 +v 69.251038 27.407484 29.530769 +v 69.292229 30.900684 29.744894 +v 72.313354 27.459957 28.085602 +v 72.354553 30.953157 28.299725 +v 72.108017 27.485153 27.714039 +v 72.149216 30.978355 27.928164 +v 71.737137 27.762161 27.352734 +v 71.685196 31.010403 27.494616 +v 71.972015 27.496784 27.550449 +v 72.013206 30.989986 27.764574 +v 70.841171 27.542528 27.021744 +v 70.882362 31.035728 27.235867 +v 70.416824 27.545847 27.049259 +v 70.458023 31.039047 27.263382 +v 71.259018 27.532776 27.100468 +v 71.300209 31.025976 27.314592 +v 69.040184 27.459957 28.715305 +v 69.081375 30.953157 28.929430 +v 69.158600 27.496784 28.091702 +v 69.199799 30.989986 28.305826 +v 69.249100 27.507523 27.899122 +v 69.053322 27.472813 28.503054 +v 69.094513 30.966013 28.717178 +v 69.095016 30.939987 29.141665 +v 69.053818 27.446785 28.927540 +v 69.498611 27.525665 27.555157 +v 69.539810 31.018864 27.769279 +v 70.210518 27.545013 27.102516 +v 70.251717 31.038216 27.316641 +v 71.262878 27.335281 30.321636 +v 71.304062 30.828482 30.535759 +v 71.647461 27.341869 30.140167 +v 71.688660 30.835072 30.354290 +v 71.974823 27.354631 29.868988 +v 72.016022 30.847832 30.083111 +v 72.224342 27.372772 29.525021 +v 72.212448 30.711140 29.823437 +v 72.380432 27.395142 29.130066 +v 72.421631 30.888344 29.344189 +v 72.420120 27.407484 28.921089 +v 72.461319 30.900684 29.135214 +v 72.460815 30.926712 28.710728 +v 72.422562 27.683025 28.511898 +v 70.214432 27.347519 30.323675 +v 70.255623 30.840721 30.537800 +v 70.632233 27.337767 30.402409 +v 70.778152 30.679604 30.613853 +v 70.845276 27.335281 30.401974 +v 70.886475 30.828482 30.616096 +v 69.501427 27.383511 29.873695 +v 69.542625 30.876711 30.087818 +v 69.698120 30.865974 30.233074 +v 69.160080 27.420340 29.338543 +v 69.201279 30.913540 29.552666 +v 72.222404 27.472813 27.893375 +v 72.263603 30.966013 28.107498 +v 71.857712 31.000725 27.619318 +v 70.628113 27.545013 27.022179 +v 70.669312 31.038216 27.236301 +v 71.460876 27.337767 30.242990 +v 71.502075 30.830967 30.457115 +v 72.110413 27.363094 29.704830 +v 72.314835 27.383511 29.332441 +v 72.356033 30.876711 29.546566 +v 72.433258 27.420340 28.708839 +v 72.474457 30.913540 28.922962 +v 57.690742 30.939987 31.335655 +v 57.649551 27.446785 31.121532 +v 54.405308 30.926712 32.184303 +v 54.364113 27.433510 31.970182 +v 56.326729 27.334450 33.208672 +v 56.367924 30.827650 33.422791 +v 55.012306 27.617165 32.935783 +v 55.140739 30.856297 33.191559 +v 54.633144 27.517200 30.553097 +v 54.609955 30.856140 30.844387 +v 55.096085 27.538425 30.117762 +v 55.137283 31.031628 30.331886 +v 54.635525 27.395142 32.543892 +v 54.676720 30.888344 32.758015 +v 56.322693 27.538425 29.881783 +v 56.363888 31.031628 30.095905 +v 56.727306 27.525665 30.012144 +v 56.768501 31.018864 30.226267 +v 54.363106 27.485153 31.127865 +v 54.404297 30.978355 31.341988 +v 54.923859 27.532776 30.243080 +v 54.965057 31.025976 30.457203 +v 55.282669 27.542528 30.014938 +v 55.323868 31.035728 30.229061 +v 57.089802 27.347519 32.848633 +v 57.130997 30.840721 33.062759 +v 55.690971 27.341869 33.209930 +v 55.732162 30.835072 33.424057 +v 55.286354 27.354631 33.079567 +v 55.327549 30.847832 33.293694 +v 54.521141 27.407484 32.364555 +v 54.562336 30.900684 32.578678 +v 57.583469 27.459957 30.919386 +v 57.624664 30.953157 31.133511 +v 57.378128 27.485153 30.547825 +v 57.419319 30.978355 30.761948 +v 57.007240 27.762161 30.186518 +v 56.955307 31.010403 30.328400 +v 57.242123 27.496784 30.384235 +v 57.283318 30.989986 30.598358 +v 56.111279 27.542528 29.855530 +v 56.152470 31.035728 30.069653 +v 55.686935 27.545847 29.883045 +v 55.728127 31.039047 30.097168 +v 56.529121 27.532776 29.934254 +v 56.570320 31.025976 30.148376 +v 54.310295 27.459957 31.549089 +v 54.351490 30.953157 31.763214 +v 54.428715 27.496784 30.925488 +v 54.469910 30.989986 31.139610 +v 54.519211 27.507523 30.732908 +v 54.323425 27.472813 31.336838 +v 54.364616 30.966013 31.550964 +v 54.365128 30.939987 31.975449 +v 54.323933 27.446785 31.761326 +v 54.768723 27.525665 30.388943 +v 54.809917 31.018864 30.603065 +v 55.480682 27.545013 29.936293 +v 55.521873 31.038216 30.150417 +v 57.730923 30.926712 31.544512 +v 57.689728 27.433510 31.330389 +v 56.532982 27.335281 33.155418 +v 56.574177 30.828482 33.369545 +v 56.917576 27.341869 32.973953 +v 56.958771 30.835072 33.188076 +v 57.244938 27.354631 32.702774 +v 57.286133 30.847832 32.916893 +v 57.494453 27.372772 32.358807 +v 57.482563 30.711140 32.657219 +v 57.650547 27.395142 31.963852 +v 57.691742 30.888344 32.177975 +v 57.690231 27.407484 31.754875 +v 57.731426 30.900684 31.968998 +v 55.484539 27.347519 33.157459 +v 55.525734 30.840721 33.371586 +v 55.902386 27.337767 33.236183 +v 56.048359 30.679604 33.447617 +v 56.115440 27.335281 33.235748 +v 56.156635 30.828482 33.449875 +v 54.771538 27.383511 32.707481 +v 54.812733 30.876711 32.921604 +v 54.968224 30.865974 33.066856 +v 54.430199 27.420340 32.172325 +v 54.471390 30.913540 32.386452 +v 57.492516 27.472813 30.727158 +v 57.533710 30.966013 30.941284 +v 57.127823 31.000725 30.453102 +v 55.898224 27.545013 29.855965 +v 55.939415 31.038216 30.070087 +v 56.730991 27.337767 33.076775 +v 56.772186 30.830967 33.290901 +v 57.380516 27.363094 32.538616 +v 57.584949 27.383511 32.166229 +v 57.626144 30.876711 32.380348 +v 57.703369 27.420340 31.542622 +v 57.744564 30.913540 31.756746 +v 71.968338 26.315218 30.481468 +v 70.098694 26.579535 26.529116 +v 69.414413 26.607899 30.284462 +v 72.160309 26.581665 30.184135 +v 71.733040 27.032171 27.003265 +v 69.956200 26.819508 26.728064 +v 70.342125 26.580122 26.472696 +v 68.642342 26.707924 28.801178 +v 68.660568 26.732841 28.391180 +v 68.739578 26.754332 28.025385 +v 69.299118 26.615072 30.189610 +v 68.675522 26.691605 29.061028 +v 72.108589 26.319937 30.377516 +v 72.493721 26.602472 29.780579 +v 72.015846 27.016306 27.207657 +v 69.592812 26.812012 26.920242 +v 69.678917 26.574068 26.699045 +v 68.562599 26.497252 28.167009 +v 68.509338 26.474747 28.544397 +v 69.119263 26.628878 29.998981 +v 69.728638 26.850067 30.359697 +v 68.707001 26.409433 29.571854 +v 72.668144 26.934923 28.409842 +v 72.417603 26.334480 30.080812 +v 72.495529 26.339773 29.979467 +v 69.341316 26.564245 26.924273 +v 54.651581 26.609947 33.091145 +v 58.167965 26.443861 31.037786 +v 57.984772 26.707924 30.851517 +v 57.238449 26.315218 33.315254 +v 54.565544 26.801542 29.982037 +v 58.079407 26.371498 32.235325 +v 58.013012 26.474747 30.563715 +v 55.368805 26.579535 29.362902 +v 54.993431 26.591278 33.329979 +v 55.230995 26.581665 33.441048 +v 54.771683 26.602472 33.190002 +v 57.761147 26.739536 30.378838 +v 57.003143 27.032171 29.837051 +v 55.107254 26.576782 29.458143 +v 53.974915 26.521887 30.571533 +v 53.910694 26.512682 30.734039 +v 57.818161 26.607899 32.515388 +v 57.606628 26.754332 30.167179 +v 57.285950 27.016306 30.041443 +v 54.862923 26.812012 29.754026 +v 54.949017 26.574068 29.532829 +v 55.612232 26.580122 29.306482 +v 54.232159 26.780737 30.385586 +v 54.302399 26.549704 30.054762 +v 54.033253 26.528168 30.457861 +v 54.457920 26.362295 33.082184 +v 58.015217 26.362295 32.397823 +v 57.954395 26.482391 30.450287 +v 54.224499 26.544409 30.156107 +v 55.350819 26.319937 33.601429 +v 72.581818 31.931406 28.642597 +v 69.247734 32.009239 28.014282 +v 69.908287 33.045830 27.321976 +v 72.610809 31.917156 28.869511 +v 68.997543 31.931406 29.332151 +v 72.329315 31.973259 28.008385 +v 68.968552 31.945658 29.105236 +v 72.524551 31.945658 28.421124 +v 69.718849 31.853573 30.463123 +v 72.582649 31.889553 29.325228 +v 69.417747 33.029507 27.682684 +v 69.600281 33.037315 27.520166 +v 71.349800 33.037315 27.183588 +v 69.250061 31.889553 29.966362 +v 72.740997 32.896088 29.219879 +v 72.040512 31.835407 30.312843 +v 68.996719 31.973259 28.649521 +v 69.773560 32.035866 27.478678 +v 69.506042 33.033638 27.598263 +v 70.333771 32.044880 27.223888 +v 70.872749 32.809589 30.990463 +v 69.385010 31.876631 30.151194 +v 72.525909 31.876631 29.546936 +v 71.864098 31.828529 30.458990 +v 68.855392 32.956409 28.983366 +v 69.053452 31.986179 28.427811 +v 69.137558 31.998238 28.214966 +v 69.382187 32.019012 27.828947 +v 72.171112 32.998066 27.665890 +v 70.787460 32.043091 27.165773 +v 71.459435 32.027405 27.292416 +v 70.386681 32.817955 30.947479 +v 70.253113 32.821613 30.913517 +v 69.819046 32.838135 30.727482 +v 69.509567 32.855495 30.503813 +v 69.063927 32.896088 29.927288 +v 72.747513 32.902519 29.113739 +v 71.455765 32.809589 30.878304 +v 71.787888 32.815147 30.723747 +v 72.331635 31.853573 29.960466 +v 72.441803 31.864576 29.759783 +v 72.541077 32.855495 29.920601 +v 68.861908 32.962837 28.877224 +v 69.061829 33.003433 28.176502 +v 69.266235 33.020794 27.853973 +v 70.019012 33.047729 27.269726 +v 70.147141 33.049339 27.218803 +v 72.194366 31.986179 27.823555 +v 71.860519 32.009239 27.511625 +v 70.990250 33.045830 27.113823 +v 71.173492 32.035866 27.209354 +v 70.607948 33.050293 27.114550 +v 69.054817 31.917156 29.553627 +v 70.722923 31.820793 30.804764 +v 70.612663 32.813095 30.983280 +v 70.119934 31.835407 30.682331 +v 69.912468 32.833904 30.778522 +v 69.431808 32.860863 30.431215 +v 71.783859 33.020794 27.369621 +v 71.690437 33.025024 27.318584 +v 69.018494 32.902519 29.831139 +v 70.506989 32.815147 30.970173 +v 71.694626 32.813095 30.775129 +v 71.583900 32.811199 30.827377 +v 71.339340 32.808632 30.916300 +v 71.168411 32.808216 30.955994 +v 72.259796 31.848354 30.059460 +v 72.268860 32.833904 30.325191 +v 72.538986 32.962837 28.169817 +v 72.584412 32.956409 28.265965 +v 72.093338 33.003433 27.593292 +v 70.994957 32.808632 30.982553 +v 57.851929 31.931406 31.476381 +v 54.517845 32.009239 30.848066 +v 55.178398 33.045830 30.155760 +v 57.880920 31.917156 31.703297 +v 54.267658 31.931406 32.165936 +v 57.599419 31.973259 30.842171 +v 54.238655 31.945658 31.939022 +v 57.794659 31.945658 31.254906 +v 54.988960 31.853573 33.296906 +v 57.852760 31.889553 32.159012 +v 54.808964 32.027405 30.495689 +v 54.985382 32.034283 30.349545 +v 56.511497 32.034283 30.055944 +v 54.520180 31.889553 32.800144 +v 58.011108 32.896088 32.053665 +v 57.310623 31.835407 33.146629 +v 54.266842 31.973259 31.483303 +v 55.085125 33.043781 30.207142 +v 55.704605 33.050713 29.974895 +v 56.142860 32.809589 33.824249 +v 54.655117 31.876631 32.984978 +v 57.796021 31.876631 32.380722 +v 57.161301 32.817955 33.491821 +v 54.125465 32.956409 31.817158 +v 54.323566 31.986179 31.261597 +v 54.407684 31.998238 31.048748 +v 54.652302 32.019012 30.662731 +v 55.533676 33.050293 30.014589 +v 57.441216 32.998066 30.499674 +v 56.057571 32.043091 29.999559 +v 56.729546 32.027405 30.126202 +v 55.608093 31.828529 33.586372 +v 55.089157 32.838135 33.561268 +v 54.779667 32.855495 33.337601 +v 54.334042 32.896088 32.761070 +v 58.017624 32.902519 31.947525 +v 56.725872 32.809589 33.712090 +v 57.058002 32.815147 33.557533 +v 57.601746 31.853573 32.794250 +v 57.711914 31.864576 32.593567 +v 57.811188 32.855495 32.754387 +v 54.132027 32.962837 31.711008 +v 54.331932 33.003433 31.010290 +v 54.536339 33.020794 30.687756 +v 55.385986 32.043091 30.128761 +v 57.464470 31.986179 30.657339 +v 57.130630 32.009239 30.345409 +v 56.260357 33.045830 29.947609 +v 55.878059 33.050293 29.948336 +v 54.324921 31.917156 32.387413 +v 55.993080 31.820793 33.638538 +v 55.882774 32.813095 33.817062 +v 55.390045 31.835407 33.516117 +v 55.182579 32.833904 33.612305 +v 54.701931 32.860863 33.264996 +v 57.272736 32.821613 33.410721 +v 57.053970 33.020794 30.203407 +v 56.960548 33.025024 30.152367 +v 56.366035 33.043781 29.960716 +v 54.288567 32.902519 32.664932 +v 55.777092 32.815147 33.803955 +v 56.964729 32.813095 33.608917 +v 56.854012 32.811199 33.661163 +v 56.609451 32.808632 33.750084 +v 56.438522 32.808216 33.789780 +v 57.529907 31.848354 32.893242 +v 57.538971 32.833904 33.158978 +v 57.809090 32.962837 31.003601 +v 57.854523 32.956409 31.099751 +v 57.363449 33.003433 30.427076 +v 56.265068 32.808632 33.816338 +v 67.710693 39.093384 25.775009 +v 58.390137 29.744638 12.561989 +v 53.301613 30.837086 17.513102 +v 66.998322 30.350315 22.819202 +v 53.510498 29.193283 22.495529 +v 65.717789 30.823067 15.353117 +v 65.329773 35.080704 19.524605 +v 65.208099 35.579727 19.579735 +v 65.675636 35.415257 22.173010 +v 61.460754 30.122192 27.606140 +v 47.649055 27.454153 12.493173 +v 47.290409 27.790154 12.328449 +v 70.123520 33.096260 26.457876 +v 70.434082 33.071548 26.801268 +v 69.936821 33.076488 26.816385 +v 69.685043 33.065853 27.038284 +v 69.770439 33.080070 26.789906 +v 63.151642 33.009308 29.217632 +v 63.290779 32.984978 29.587803 +v 63.429760 32.932781 30.412621 +v 63.407700 32.940395 30.292667 +v 63.390350 32.948917 30.156967 +v 63.334511 32.970093 29.822248 +v 63.318298 32.978077 29.695122 +v 63.284714 32.991623 29.480576 +v 65.170296 32.997131 29.027990 +v 61.567287 33.059551 28.702791 +v 63.587120 33.089775 27.821175 +v 63.470627 33.093452 27.783619 +v 60.887775 33.080994 28.483789 +v 54.167400 33.082066 29.759163 +v 52.926620 33.098160 29.735325 +v 52.659462 33.091377 29.897301 +v 52.462818 33.080070 30.119602 +v 52.384384 33.071388 30.276371 +v 52.328743 33.060848 30.458994 +v 52.313683 33.046104 30.702461 +v 52.329174 33.038490 30.823681 +v 52.969357 33.039158 30.689613 +v 52.571171 33.087246 29.981724 +v 53.488445 33.076488 29.980782 +v 73.954010 33.014244 27.058990 +v 73.492531 32.996738 27.433302 +v 73.832451 33.033283 26.771727 +v 73.760345 33.040184 26.672985 +v 73.582001 33.052505 26.506313 +v 73.367722 33.062210 26.389236 +v 71.162155 33.053223 26.960196 +v 70.851105 33.067471 26.787560 +v 70.676483 33.070595 26.770233 +v 74.502411 32.833042 29.909569 +v 73.923241 32.854427 29.672142 +v 74.496109 32.814003 30.221432 +v 74.465790 32.807098 30.339878 +v 73.815895 32.821659 30.227333 +v 74.362015 32.794777 30.560827 +v 73.649445 32.806507 30.506533 +v 74.206474 32.785072 30.749050 +v 74.112228 32.781399 30.827145 +v 63.571854 32.884747 31.168949 +v 63.482590 32.914238 30.704979 +v 63.449841 32.926147 30.517002 +v 65.286980 32.919964 30.264404 +v 61.677921 32.908604 31.144112 +v 65.416847 32.890198 30.725039 +v 63.595421 32.876957 31.291462 +v 63.613636 32.870937 31.386137 +v 61.732666 32.866093 31.827078 +v 61.727100 32.859928 31.928732 +v 63.721375 32.849617 31.713259 +v 63.746559 32.843353 31.810556 +v 61.692570 32.837376 32.303265 +v 63.794128 32.829422 32.028717 +v 65.893768 32.825825 31.683489 +v 61.684250 32.823376 32.533310 +v 66.160912 32.812107 31.855865 +v 66.533264 32.776691 32.361996 +v 53.116489 32.810104 34.398037 +v 52.954250 32.833042 34.055077 +v 53.403961 32.854427 33.619709 +v 53.503826 32.840313 33.830807 +v 53.653728 32.825954 34.036137 +v 54.578373 32.788551 34.468437 +v 53.657028 32.781399 34.762383 +v 53.540535 32.785072 34.724831 +v 53.326263 32.794777 34.607758 +v 48.117363 32.414230 25.472364 +v 47.936600 37.451244 25.062084 +v 47.462337 33.349678 22.596886 +v 48.472832 34.769199 27.849392 +v 48.132996 37.451244 25.024300 +v 47.448547 35.891544 21.995876 +v 47.658737 33.349678 22.559101 +v 48.669228 34.769199 27.811611 +v 68.597794 32.460957 20.770002 +v 68.529022 34.956100 20.941839 +v 68.656647 37.451244 21.075893 +v 68.725418 34.956100 20.904057 +v 69.192871 34.769199 23.863203 +v 68.061569 35.143002 17.982693 +v 55.493916 28.714106 7.319708 +v 55.513721 36.246075 7.617717 +v 55.613857 36.785557 7.873100 +v 55.716690 36.180756 8.096821 +v 61.174526 35.372417 7.578281 +v 56.715851 36.162281 9.156989 +v 56.882771 35.167583 9.204084 +v 56.822067 30.019897 8.888545 +v 56.431080 30.478729 8.663978 +v 56.156376 36.533798 8.710601 +v 55.448944 35.688725 7.386063 +v 58.242477 32.209637 9.415129 +v 57.847332 33.890442 9.466166 +v 55.887363 36.691738 8.367769 +v 60.789288 36.162281 8.373327 +v 60.650536 35.064186 8.472656 +v 60.650768 30.852600 8.148406 +v 61.226772 28.741980 5.832928 +v 61.276680 36.221165 6.092371 +v 61.304726 36.245945 6.238131 +v 61.139194 35.320530 7.654732 +v 59.853283 33.890442 9.080255 +v 61.334484 36.246075 6.497900 +v 61.323769 36.180756 7.018112 +v 61.336254 36.785557 6.772207 +v 61.212914 36.040863 7.487852 +v 61.270916 36.696884 7.311611 +v 61.101208 36.471527 7.835536 +v 60.939262 35.652382 8.111008 +v 55.129189 35.673710 5.810829 +v 55.272560 36.103405 6.556059 +v 55.155918 35.824539 5.949753 +v 55.188896 35.935432 6.121159 +v 55.388840 36.221134 7.160454 +v 55.282745 35.532955 6.608987 +v 54.923420 34.600830 4.741237 +v 55.004421 35.057861 5.162272 +v 55.052929 35.978577 5.414425 +v 54.940838 30.423944 4.831768 +v 55.182667 30.252462 6.088789 +v 55.207291 28.823801 6.216789 +v 60.840225 34.880630 3.736892 +v 60.853653 32.655666 3.806683 +v 60.900909 31.887091 4.052315 +v 60.842358 35.727879 3.747977 +v 60.912659 36.041630 4.113379 +v 60.989826 36.306293 4.514506 +v 61.135990 36.051147 5.274265 +v 61.164520 36.103405 5.422544 +v 61.280792 36.221134 6.026940 +v 61.204659 29.431095 5.674609 +v 55.328812 29.431095 6.805025 +v 61.294086 36.786648 6.124968 +v 55.408882 36.221165 7.221238 +v 46.259369 31.056692 15.285244 +v 44.349480 28.218271 12.921448 +v 44.634125 27.525064 13.077703 +v 49.204826 26.501539 28.896095 +v 45.588505 27.542517 12.810178 +# 3660 vertices, 0 vertices normals + +f 90 1792 1 1782 +f 2 1784 83 1782 +f 1783 3 1784 2 +f 3 3624 1716 1784 +f 1785 1690 3655 5 +f 1786 73 1882 4 +f 1787 1667 1785 5 +f 4 1903 1668 1787 +f 1786 5 3635 6 +f 1759 1804 18 1788 +f 97 1808 23 1788 +f 69 1805 20 1789 +f 24 1809 25 1789 +f 7 1918 99 1790 +f 1763 3649 1764 1790 +f 1739 3634 22 1791 +f 8 1879 17 1791 +f 90 1919 89 1792 +f 9 3651 1 1792 +f 6 3635 16 1793 +f 70 1881 73 1793 +f 1750 1808 10 1794 +f 11 1932 1749 1794 +f 47 1809 12 1795 +f 13 2847 45 1795 +f 94 1923 93 1796 +f 1796 1720 3628 1723 +f 1797 1676 1798 94 +f 1798 14 1799 94 +f 15 1904 82 1800 +f 76 1801 1699 1800 +f 1801 1685 3611 1699 +f 89 1915 7 1802 +f 1764 3650 9 1802 +f 16 3632 1739 1803 +f 17 1880 70 1803 +f 19 1931 18 1804 +f 1759 3647 1761 1804 +f 1738 3638 20 1805 +f 69 1877 21 1805 +f 99 1931 19 1806 +f 1761 3648 1763 1806 +f 22 3639 1738 1807 +f 21 1878 8 1807 +f 1810 27 2494 26 +f 1811 612 1812 40 +f 510 2615 29 1813 +f 1814 28 1813 29 +f 1815 31 1814 29 +f 1815 616 2590 590 +f 31 1815 590 1816 +f 1816 30 2591 606 +f 31 1816 606 1817 +f 33 1818 32 1817 +f 1818 607 2592 593 +f 32 1818 593 1819 +f 1819 34 2593 36 +f 1820 35 1821 32 +f 1821 531 1822 32 +f 1822 608 1823 32 +f 1823 527 1829 32 +f 1824 526 1830 43 +f 586 2600 37 1825 +f 1825 40 1826 38 +f 614 2587 39 1827 +f 1827 40 2424 43 +f 1828 603 2599 614 +f 43 1833 585 1828 +f 1830 42 1831 43 +f 1831 41 1832 43 +f 1832 602 1833 43 +f 1833 601 2530 585 +f 1834 859 1840 44 +f 1835 753 1836 45 +f 1836 784 1837 45 +f 46 1875 47 1837 +f 1838 791 1846 859 +f 1839 781 1840 859 +f 1841 48 1839 859 +f 1842 805 1841 859 +f 1843 49 1842 859 +f 1844 760 1843 859 +f 1845 802 1844 859 +f 1846 801 1845 859 +f 1838 54 1847 50 +f 1848 51 1847 54 +f 1849 52 1848 54 +f 1850 795 1849 54 +f 1851 799 1850 54 +f 1852 53 1851 54 +f 1853 56 1854 55 +f 1854 1776 1855 55 +f 1856 57 1865 1776 +f 1857 828 1856 1776 +f 60 2805 61 1858 +f 1859 75 1894 60 +f 1860 823 1862 59 +f 1861 62 1859 60 +f 1862 850 1864 59 +f 1863 63 1861 60 +f 1865 857 1855 1776 +f 1866 58 1857 1776 +f 1867 64 1866 1776 +f 1868 768 1867 1776 +f 1869 65 1868 1776 +f 66 1869 1776 1864 +f 1870 140 1872 60 +f 1871 67 1863 60 +f 1872 132 1871 60 +f 59 1873 255 1858 +f 1874 493 2497 32 +f 1873 1773 1874 32 +f 1875 25 1809 47 +f 1875 46 2738 68 +f 1876 69 1789 25 +f 1876 68 2728 777 +f 1877 777 2757 780 +f 1878 780 2759 751 +f 1879 751 2734 750 +f 1880 750 2753 72 +f 1881 72 2752 71 +f 73 1881 71 1882 +f 1882 847 1883 4 +f 1884 82 1883 843 +f 1885 76 1884 74 +f 1886 817 1887 75 +f 1887 838 1888 75 +f 1888 813 1889 75 +f 1889 77 1890 75 +f 1890 78 1891 75 +f 1891 763 1892 75 +f 1892 810 1893 75 +f 1893 807 1894 75 +f 1885 79 1886 75 +f 1895 938 1901 1547 +f 1896 933 1895 1547 +f 1897 80 1899 81 +f 1898 867 1899 80 +f 1900 864 1902 945 +f 938 2966 945 1902 +f 864 3539 1578 1902 +f 82 1904 1670 1903 +f 93 1922 83 1905 +f 1905 1716 3626 1719 +f 1906 1720 1796 93 +f 121 1974 120 1907 +f 1907 1593 1909 84 +f 1908 123 1977 84 +f 1909 865 1908 84 +f 98 2722 742 1910 +f 1911 98 1931 99 +f 1912 85 1911 99 +f 1913 752 1912 99 +f 1914 86 1913 99 +f 1915 749 1920 7 +f 1916 756 1914 99 +f 1917 87 1916 99 +f 778 2733 88 1918 +f 1919 91 1915 89 +f 1920 778 1918 7 +f 1919 90 1921 773 +f 1921 83 1922 846 +f 92 2719 846 1922 +f 1923 841 2823 92 +f 1924 95 2825 841 +f 1924 94 1799 138 +f 1925 815 2798 95 +f 1925 138 1983 129 +f 1926 812 2820 815 +f 1926 129 1985 134 +f 1927 836 2794 812 +f 1927 134 1992 133 +f 1928 834 2795 836 +f 1928 133 1986 139 +f 1929 139 1997 125 +f 1929 96 2792 834 +f 11 1794 10 1910 +f 1930 98 1910 10 +f 97 1788 18 1930 +f 1931 98 1930 18 +f 1932 11 1910 742 +f 1933 804 1934 101 +f 1934 761 1935 101 +f 1935 748 1936 101 +f 1936 759 1937 101 +f 1937 800 1938 101 +f 100 1939 861 1938 +f 1939 789 1940 861 +f 1940 790 1941 861 +f 1941 102 1942 861 +f 1942 103 1943 861 +f 1943 794 1944 861 +f 1944 105 1945 861 +f 1945 104 1946 861 +f 1946 830 1947 861 +f 1947 106 1948 861 +f 1948 108 1949 861 +f 1949 107 1968 861 +f 1950 826 1967 115 +f 96 1929 125 1951 +f 126 1952 109 1951 +f 1953 808 1952 126 +f 1954 110 1953 126 +f 1955 111 1954 126 +f 1956 851 1955 126 +f 1957 821 1956 126 +f 1958 112 1959 848 +f 1959 629 1960 848 +f 126 2196 628 1958 +f 1961 818 1960 629 +f 1962 113 1963 766 +f 1964 746 1963 113 +f 1965 630 1966 114 +f 1969 116 1970 117 +f 1970 633 1971 117 +f 1971 118 1972 117 +f 1972 631 1975 117 +f 1973 119 1974 121 +f 1968 115 1969 117 +f 1976 122 1978 117 +f 1978 861 1968 117 +f 124 1977 123 1976 +f 1870 255 1979 125 +f 1979 126 1951 125 +f 1859 62 1980 127 +f 1981 128 1980 62 +f 1982 1693 1981 62 +f 1983 1702 1994 130 +f 1984 1711 1985 129 +f 1871 132 1986 133 +f 132 1872 139 1986 +f 1985 131 1987 134 +f 1987 1713 1988 134 +f 1988 135 1989 134 +f 1989 1701 1992 134 +f 1990 137 3613 136 +f 1861 63 1991 1691 +f 1991 136 1982 1691 +f 63 1992 1701 1991 +f 1992 63 1863 133 +f 1863 67 1871 133 +f 1799 1733 1993 138 +f 1993 1730 1994 138 +f 1994 1702 1983 138 +f 1801 76 1885 75 +f 1995 1685 1801 75 +f 75 1859 127 1996 +f 1872 140 1997 139 +f 140 1870 125 1997 +f 142 2034 1779 1998 +f 141 3512 149 1998 +f 1999 1756 2169 1548 +f 2000 143 1999 1548 +f 2001 1715 2000 1548 +f 2002 1725 2168 231 +f 2003 1672 2002 231 +f 2004 1727 2003 231 +f 2005 1728 2004 231 +f 2006 1705 2005 231 +f 2007 146 2008 1712 +f 2009 1663 2008 146 +f 2010 144 2009 146 +f 2011 145 2010 146 +f 2012 147 2011 146 +f 1692 3613 1695 2013 +f 2013 146 3503 1535 +f 2014 1692 2013 1535 +f 2015 1694 2014 1535 +f 2016 149 2017 1682 +f 2018 1666 2035 1769 +f 1535 3504 142 2016 +f 2019 1683 2017 149 +f 2020 1666 3602 148 +f 2020 149 3512 860 +f 156 2034 252 2021 +f 2022 156 2195 150 +f 151 3351 1345 2022 +f 2023 156 2022 1345 +f 1346 3353 1319 2023 +f 2024 156 2023 1319 +f 152 3344 1321 2024 +f 2025 1320 2026 156 +f 2026 153 2027 156 +f 2027 1348 2028 156 +f 2028 154 2029 156 +f 2029 155 2030 156 +f 2030 157 2033 156 +f 2031 1360 2032 156 +f 2032 1326 2126 156 +f 2033 1325 2031 156 +f 860 2843 1744 2035 +f 2036 1226 2037 166 +f 2037 158 2038 166 +f 2038 160 2039 166 +f 2039 159 2047 166 +f 2040 161 2041 162 +f 2041 1276 2042 162 +f 2042 1248 2043 162 +f 1249 3312 163 2043 +f 2044 162 2043 163 +f 1278 3311 164 2044 +f 2045 162 2044 164 +f 1247 3321 165 2045 +f 2046 162 2045 165 +f 2046 1274 3310 1246 +f 2048 162 2046 1246 +f 2049 1225 2137 866 +f 2050 1291 2049 866 +f 2051 167 2050 866 +f 2052 1289 2051 866 +f 2053 168 2052 866 +f 2054 1237 2053 866 +f 2055 169 2054 866 +f 2056 1229 2055 866 +f 2057 1261 2056 866 +f 2058 1262 2057 866 +f 2059 170 2058 866 +f 2060 1260 2059 866 +f 1258 3316 171 2061 +f 2061 866 2062 1301 +f 172 3327 1301 2062 +f 2062 866 2063 1300 +f 1236 3306 1300 2063 +f 2063 866 2064 1256 +f 1257 3315 1256 2064 +f 2064 866 2065 173 +f 2066 1287 2065 866 +f 2067 1285 2066 866 +f 2068 1284 2067 866 +f 2069 174 2068 866 +f 2070 175 2069 866 +f 2071 1253 2070 866 +f 2072 176 2071 866 +f 2073 1299 2072 866 +f 2074 1252 2073 866 +f 2075 177 2074 866 +f 2076 178 2075 866 +f 2077 1329 2078 179 +f 2079 200 2107 1374 +f 2080 180 2090 188 +f 2048 181 3314 1255 +f 2081 182 2082 162 +f 2082 1282 2083 162 +f 2084 1254 2085 1545 +f 2085 183 2086 1545 +f 2086 184 2087 1545 +f 2088 1306 2087 197 +f 2089 185 2088 186 +f 2090 187 3305 188 +f 2091 198 2104 1332 +f 2092 190 2105 199 +f 2093 191 2079 1374 +f 2094 203 2109 202 +f 2083 192 2084 1545 +f 2095 234 3511 1545 +f 2089 187 2090 1365 +f 2096 1303 2095 1545 +f 2097 193 2096 1545 +f 2098 194 2097 1545 +f 2099 1335 2098 1545 +f 2100 195 2099 1545 +f 2101 1307 2100 1545 +f 2102 1334 2101 1545 +f 2103 196 2102 1545 +f 180 3345 1365 2090 +f 2080 189 2091 1332 +f 2106 1361 2105 190 +f 2108 201 2107 200 +f 2077 866 2110 1304 +f 2111 213 2112 1363 +f 2113 204 2112 213 +f 2114 205 2113 213 +f 2115 206 2114 213 +f 2116 207 2115 213 +f 2117 208 2116 213 +f 2118 1373 2136 213 +f 2119 219 2135 213 +f 1375 3358 210 2120 +f 211 3338 217 2121 +f 2122 213 2132 214 +f 2123 212 2131 213 +f 2124 1322 2130 213 +f 2125 1328 2127 213 +f 2127 1358 2128 213 +f 2128 1359 2129 213 +f 2129 1350 2124 213 +f 2130 1353 2123 213 +f 2131 1354 2132 213 +f 2132 1355 3329 214 +f 2122 215 3339 1357 +f 2133 216 2121 213 +f 213 2121 217 2120 +f 2134 1305 2119 213 +f 2135 218 2118 213 +f 2136 209 2117 213 +f 2137 220 2138 866 +f 2139 221 2140 1548 +f 2140 1293 2141 1548 +f 2141 1239 2142 1548 +f 2142 222 2143 1548 +f 2143 1238 2144 1548 +f 2144 1266 2145 1548 +f 2145 1265 2146 1548 +f 2146 1263 2167 1548 +f 2147 1264 2148 230 +f 2148 1267 2149 230 +f 2149 1240 2150 230 +f 2150 223 2166 230 +f 2151 224 2152 166 +f 2152 225 2153 166 +f 2153 1233 2154 166 +f 2154 1268 2155 166 +f 2155 1235 2156 166 +f 2156 1234 2157 166 +f 2157 1273 2158 166 +f 2158 226 2159 166 +f 2159 227 2160 166 +f 2160 1245 2161 166 +f 1242 3304 1272 2161 +f 2162 166 2161 1272 +f 1241 3308 228 2162 +f 2163 166 2162 228 +f 229 3318 1270 2163 +f 2164 166 2163 1270 +f 1302 3328 1295 2164 +f 2165 1297 2036 166 +f 2167 230 3507 231 +f 1548 2167 231 2168 +f 2169 232 2846 1548 +f 143 3619 233 1999 +f 2095 1303 3330 235 +f 234 2095 235 2170 +f 2170 1309 3331 236 +f 234 2170 236 2171 +f 2171 1367 3356 1315 +f 234 2171 1315 2172 +f 2172 237 3340 238 +f 234 2172 238 2173 +f 2173 1368 3346 1337 +f 2174 240 2175 234 +f 2175 239 2176 234 +f 2176 241 2177 234 +f 2177 1317 2178 234 +f 2178 242 2179 234 +f 2179 243 2180 234 +f 2180 245 2181 234 +f 2181 244 2182 234 +f 2182 1310 2191 234 +f 2183 1311 2184 252 +f 2184 1343 2185 252 +f 2185 1342 2186 252 +f 2186 246 2187 252 +f 2187 1344 2188 252 +f 2188 247 2192 252 +f 248 3349 249 2189 +f 250 3348 253 2190 +f 251 3333 1369 2021 +f 2192 1312 3336 1340 +f 2193 1318 2189 252 +f 252 2189 249 2190 +f 2194 251 2021 252 +f 1347 3352 150 2195 +f 2195 156 2021 1369 +f 254 2616 628 2196 +f 2197 439 2419 254 +f 2198 1544 2197 254 +f 2196 126 1979 257 +f 2199 1534 2198 254 +f 2200 257 1979 255 +f 2201 255 1873 32 +f 2201 258 3504 256 +f 1544 3507 1539 2197 +f 2202 1536 3505 1537 +f 2203 1543 2205 453 +f 2204 258 2201 32 +f 2206 259 2207 43 +f 2207 1538 1829 43 +f 260 2227 272 2208 +f 2209 363 2319 372 +f 2210 262 2236 283 +f 2211 312 2269 264 +f 2212 338 2291 400 +f 263 2294 279 2212 +f 2213 271 2353 386 +f 2214 347 2312 365 +f 2215 349 2211 264 +f 2216 379 2326 378 +f 2217 289 2294 265 +f 2210 266 2349 267 +f 269 2230 268 2218 +f 398 2290 282 2218 +f 2219 275 2352 270 +f 2220 288 2219 270 +f 2221 356 2253 325 +f 353 2297 342 2215 +f 2222 273 2287 369 +f 344 2299 345 2223 +f 331 2223 330 2224 +f 332 2214 365 2225 +f 261 2209 372 2226 +f 332 2261 306 2214 +f 2216 334 2267 311 +f 2227 333 2323 272 +f 2228 267 2349 336 +f 2229 335 2290 399 +f 2228 337 2237 262 +f 2222 375 2293 277 +f 269 2232 339 2230 +f 274 2347 268 2230 +f 275 2219 324 2217 +f 2231 276 2232 269 +f 2233 685 2231 282 +f 2232 276 2663 671 +f 2234 284 2233 335 +f 2235 277 2292 339 +f 2236 285 2234 283 +f 2235 671 2641 650 +f 2237 646 2236 262 +f 2238 273 2222 277 +f 2239 684 2237 337 +f 2238 650 2639 278 +f 2240 286 2239 338 +f 2241 280 2287 273 +f 2241 278 2665 281 +f 2242 645 2240 279 +f 2243 654 2242 289 +f 2244 287 2243 324 +f 2245 290 2244 288 +f 2245 340 2246 681 +f 2246 292 2247 291 +f 727 2685 291 2247 +f 2247 292 2306 293 +f 2248 724 2710 727 +f 2248 293 2296 294 +f 2249 725 2708 724 +f 2249 294 2284 341 +f 2250 295 2691 725 +f 2250 341 2283 296 +f 2251 718 2706 295 +f 2251 296 2305 357 +f 2252 700 2689 718 +f 2252 357 2282 325 +f 2253 297 2689 700 +f 2254 298 2681 297 +f 2254 356 2281 299 +f 2255 299 2304 319 +f 2255 300 2714 298 +f 2256 331 2224 280 +f 2256 281 2643 677 +f 2257 344 2223 331 +f 2257 677 2667 302 +f 2258 301 2299 344 +f 2258 302 2629 643 +f 2259 303 2288 301 +f 2259 643 2629 304 +f 2260 332 2225 303 +f 2260 304 2661 305 +f 2261 305 2637 667 +f 2262 261 2226 306 +f 2262 667 2656 307 +f 2263 308 2209 261 +f 2263 307 2658 637 +f 308 2263 637 2264 +f 2264 636 2265 260 +f 2266 350 2265 309 +f 2267 334 2266 310 +f 2268 311 2267 705 +f 2269 312 2268 317 +f 2270 264 2269 318 +f 2271 353 2270 313 +f 2272 354 2271 711 +f 2273 343 2272 314 +f 2274 360 2273 315 +f 2275 323 2274 666 +f 2276 361 2275 316 +f 2277 732 2712 300 +f 2277 319 2295 321 +f 2278 735 2716 732 +f 2278 321 2303 320 +f 2279 716 2704 735 +f 2279 320 2302 322 +f 2280 713 2702 716 +f 2280 322 2301 351 +f 2276 713 2280 351 +f 2219 288 2244 324 +f 2281 356 2221 394 +f 358 2340 326 2282 +f 327 2339 390 2283 +f 2284 294 2296 389 +f 327 2283 341 2284 +f 323 2286 329 2285 +f 362 2330 329 2286 +f 2287 346 2314 369 +f 2288 366 2320 373 +f 2225 366 2288 303 +f 334 2216 378 2289 +f 2290 398 2348 399 +f 266 2210 283 2229 +f 2291 337 2228 336 +f 405 2355 274 2292 +f 2293 405 2292 277 +f 2294 289 2242 279 +f 2294 263 2351 265 +f 271 2213 340 2220 +f 2213 292 2246 340 +f 393 2342 392 2295 +f 326 2340 394 2221 +f 359 2334 389 2296 +f 354 2307 382 2297 +f 2285 328 2298 360 +f 2298 328 2328 384 +f 2299 373 2311 345 +f 2224 346 2287 280 +f 2226 347 2214 306 +f 2208 363 2209 308 +f 2211 349 2329 348 +f 312 2211 348 2300 +f 2289 333 2227 350 +f 2227 260 2265 350 +f 335 2233 282 2290 +f 397 2309 351 2301 +f 322 2302 352 2301 +f 355 2341 352 2302 +f 2303 355 2302 320 +f 393 2295 319 2304 +f 2305 296 2283 390 +f 359 2296 293 2306 +f 2306 292 2213 386 +f 2307 384 2332 382 +f 2307 354 2272 343 +f 2300 379 2216 311 +f 2303 321 2295 392 +f 395 2304 299 2281 +f 358 2282 357 2305 +f 2298 343 2273 360 +f 361 2276 351 2308 +f 396 2317 362 2308 +f 2309 396 2308 351 +f 2286 323 2275 361 +f 417 2369 411 2310 +f 363 2208 272 2310 +f 373 2320 374 2311 +f 364 2316 345 2311 +f 433 2366 406 2312 +f 406 2384 432 2313 +f 366 2225 365 2313 +f 346 2322 367 2314 +f 429 2315 369 2314 +f 368 2321 375 2315 +f 376 2322 330 2316 +f 370 2330 362 2317 +f 396 2356 371 2317 +f 412 2363 433 2318 +f 347 2226 372 2318 +f 411 2368 412 2319 +f 432 2383 374 2320 +f 2321 404 2293 375 +f 418 2370 417 2323 +f 377 2371 418 2324 +f 333 2289 378 2324 +f 383 2375 423 2325 +f 349 2215 342 2325 +f 420 2372 377 2326 +f 380 2328 328 2327 +f 329 2330 381 2327 +f 385 2332 384 2328 +f 423 2374 421 2329 +f 383 2325 342 2331 +f 382 2332 426 2331 +f 421 2373 420 2333 +f 379 2300 348 2333 +f 359 2306 386 2334 +f 2334 1541 2335 389 +f 2335 387 2338 389 +f 2336 388 2337 327 +f 2337 1536 2339 327 +f 327 2284 389 2338 +f 2339 1536 2202 358 +f 2202 453 2340 358 +f 453 2344 394 2340 +f 391 2345 352 2341 +f 2341 355 2303 392 +f 391 2341 392 2342 +f 2342 393 2304 395 +f 2343 391 2342 395 +f 453 2343 395 2344 +f 2309 397 2345 1531 +f 2345 397 2301 352 +f 391 3502 1531 2345 +f 2346 401 2349 266 +f 2347 455 2348 398 +f 2348 455 2346 266 +f 2349 401 2350 336 +f 400 2291 336 2350 +f 2350 401 2351 263 +f 2351 401 2352 275 +f 2352 401 2353 271 +f 2354 455 2347 274 +f 2355 402 2354 274 +f 403 2415 371 2356 +f 396 2309 1531 2356 +f 2357 402 2355 404 +f 2355 405 2293 404 +f 2358 651 2385 404 +f 2359 430 2384 406 +f 2360 674 2359 406 +f 2361 407 2360 406 +f 2362 678 2361 406 +f 2363 413 2367 433 +f 2364 408 2362 406 +f 2365 642 2364 406 +f 410 2637 409 2366 +f 2367 410 2366 433 +f 2368 668 2363 412 +f 2368 411 2369 414 +f 2369 417 2370 416 +f 415 2677 416 2370 +f 2371 419 2699 415 +f 2372 706 2695 419 +f 2373 649 2697 706 +f 2374 422 2635 649 +f 2375 424 2679 422 +f 2376 427 2701 424 +f 2376 383 2331 426 +f 2377 426 2332 385 +f 2377 425 2687 427 +f 2378 435 2390 439 +f 1533 2357 428 2379 +f 2379 404 2382 663 +f 2321 368 2380 430 +f 429 2314 367 2380 +f 2381 430 2380 367 +f 376 2316 364 2381 +f 2383 430 2381 364 +f 2385 434 2386 404 +f 2386 431 2382 404 +f 2387 664 2388 1533 +f 2388 647 2389 1533 +f 2389 661 2378 1533 +f 2390 436 2391 439 +f 2392 438 2422 439 +f 440 2393 437 2391 +f 655 2394 441 2393 +f 2394 659 2395 441 +f 2396 696 2397 632 +f 2395 658 2396 632 +f 2398 443 2399 442 +f 2400 702 2401 634 +f 2401 719 2414 634 +f 2402 720 2403 444 +f 2403 691 2404 444 +f 2404 445 2405 444 +f 2405 731 2406 444 +f 2397 728 2398 442 +f 2407 736 2408 371 +f 2408 712 2413 371 +f 2409 446 2683 425 +f 2409 385 2328 380 +f 2410 448 2693 446 +f 2410 380 2327 381 +f 2406 447 2407 371 +f 2411 449 2654 448 +f 2411 381 2330 370 +f 449 2411 370 2412 +f 371 2413 640 2412 +f 2415 444 2406 371 +f 2399 701 2400 634 +f 444 2416 450 2414 +f 2416 1573 2417 450 +f 2418 635 2417 1573 +f 2420 451 2419 439 +f 2421 627 2420 439 +f 2422 452 2421 439 +f 2423 1778 1901 1578 +f 43 2424 467 2425 +f 2424 475 2454 467 +f 2343 453 2416 444 +f 2205 43 2425 1578 +f 2426 1573 2416 453 +f 1542 2346 455 2427 +f 2427 1533 2428 454 +f 439 2197 1539 2428 +f 2429 1777 3657 456 +f 2430 1772 2429 456 +f 2431 492 2445 457 +f 2432 464 2442 1532 +f 2433 459 2438 462 +f 2434 1532 2435 463 +f 1532 2442 461 2435 +f 2436 687 2441 463 +f 2437 652 2433 463 +f 2438 501 2504 462 +f 2438 459 2640 676 +f 2439 676 2666 653 +f 2440 653 2644 679 +f 2440 511 2513 512 +f 2439 512 2503 501 +f 2441 672 2437 463 +f 686 2436 463 2435 +f 2443 458 2432 1532 +f 2444 465 2443 1532 +f 2446 657 2445 492 +f 2447 466 2446 492 +f 2448 680 2447 492 +f 2449 683 2448 492 +f 697 2449 492 2450 +f 467 2451 729 2450 +f 2452 726 2451 467 +f 2453 703 2452 467 +f 2454 721 2705 468 +f 477 2703 717 2455 +f 2456 491 2493 477 +f 2457 469 2459 505 +f 2458 470 2456 477 +f 2460 514 2458 477 +f 2461 734 2463 475 +f 2462 472 2460 477 +f 2463 692 2465 475 +f 2464 521 2462 477 +f 2465 474 2467 475 +f 2466 473 2464 477 +f 2467 722 2471 475 +f 2468 523 2466 477 +f 2469 1774 2472 1778 +f 505 2507 504 2455 +f 2470 476 2468 477 +f 2471 721 2454 475 +f 2423 467 2450 492 +f 2459 471 2461 475 +f 2473 478 2494 505 +f 2474 479 2505 511 +f 2474 679 2668 644 +f 2475 507 2499 479 +f 2475 644 2630 669 +f 2476 508 2511 507 +f 2476 669 2660 670 +f 2477 497 2502 508 +f 2477 670 2662 480 +f 2478 498 2501 497 +f 2478 480 2638 481 +f 2479 483 2509 498 +f 2479 481 2657 482 +f 2480 494 2510 483 +f 2480 482 2659 484 +f 494 2480 484 2481 +f 2481 485 2482 513 +f 2483 503 2482 486 +f 2484 515 2483 707 +f 2485 708 2486 491 +f 2486 487 2487 491 +f 2487 689 2488 491 +f 2488 488 2489 491 +f 2489 489 2490 491 +f 2490 695 2491 491 +f 2491 490 2492 491 +f 2492 641 2493 491 +f 2484 710 2485 491 +f 1813 28 1814 460 +f 2495 510 1813 460 +f 1532 2434 31 2496 +f 2431 1532 2496 32 +f 525 2522 495 2498 +f 494 2481 513 2498 +f 507 2511 621 2499 +f 605 2505 479 2499 +f 2500 491 2456 496 +f 2456 470 2517 496 +f 620 2589 617 2501 +f 617 2533 622 2502 +f 512 2513 499 2503 +f 500 2504 501 2503 +f 509 2512 462 2504 +f 502 2513 511 2505 +f 528 2524 597 2506 +f 503 2483 515 2506 +f 518 2518 504 2507 +f 505 2494 27 2507 +f 2468 476 2508 524 +f 506 2614 620 2509 +f 495 2613 506 2510 +f 622 2602 621 2511 +f 2512 510 2495 462 +f 2495 463 2433 462 +f 597 2523 525 2514 +f 513 2482 503 2514 +f 2515 609 2458 514 +f 2460 472 2462 519 +f 2516 528 2506 515 +f 2500 516 2516 515 +f 2470 504 2518 517 +f 2462 521 2519 519 +f 2464 473 2520 520 +f 2466 523 2521 522 +f 2522 550 2552 625 +f 2523 597 2524 552 +f 527 1823 569 1824 +f 34 1819 593 2525 +f 2526 583 1810 612 +f 1811 37 2600 604 +f 2527 577 2599 603 +f 1823 608 2604 569 +f 2528 600 2515 519 +f 2529 530 2574 529 +f 2530 584 2527 585 +f 42 1830 570 1831 +f 41 1831 570 2529 +f 2531 580 2580 587 +f 2532 586 1825 38 +f 33 2611 560 2534 +f 1821 35 2594 595 +f 2516 516 2595 599 +f 550 2522 525 2523 +f 532 2556 598 2535 +f 2536 524 2508 578 +f 575 2561 556 2536 +f 2537 533 2538 615 +f 2538 534 2609 615 +f 2539 542 2537 624 +f 2538 533 2664 673 +f 2540 543 2539 535 +f 2541 619 2590 534 +f 2542 675 2540 536 +f 2541 673 2626 688 +f 2543 537 2542 589 +f 2544 540 2603 619 +f 2545 538 2543 618 +f 2544 688 2676 665 +f 2546 546 2545 539 +f 2547 558 2591 540 +f 2547 665 2653 559 +f 2548 541 2546 545 +f 2549 544 2548 623 +f 2550 547 2549 548 +f 2551 549 2550 626 +f 2551 625 2552 638 +f 2552 550 2553 639 +f 551 2678 639 2553 +f 2553 550 2523 552 +f 2554 553 2700 551 +f 2554 552 2524 599 +f 2555 554 2696 553 +f 2555 599 2595 598 +f 2556 709 2698 554 +f 2557 555 2636 709 +f 2557 532 2605 610 +f 2558 690 2680 555 +f 2558 610 2605 600 +f 2559 690 2558 600 +f 2528 557 2560 698 +f 2560 699 2688 698 +f 2560 557 2596 556 +f 2561 694 2684 699 +f 2562 560 2611 558 +f 2562 559 2634 648 +f 2563 592 2534 560 +f 2563 648 2674 662 +f 2564 591 2592 592 +f 2564 662 2632 563 +f 2565 561 2525 591 +f 2565 563 2632 562 +f 2566 594 2593 561 +f 2566 562 2648 564 +f 2567 596 2594 594 +f 2567 564 2646 656 +f 2568 595 2594 596 +f 2568 656 2670 565 +f 2569 566 2604 595 +f 2569 565 2650 660 +f 566 2569 660 2570 +f 2570 682 2571 569 +f 2572 568 2571 567 +f 2573 570 2572 730 +f 2574 530 2573 571 +f 2575 529 2574 572 +f 2576 573 2575 723 +f 2577 584 2576 574 +f 2578 577 2577 693 +f 2579 613 2578 579 +f 2580 580 2579 733 +f 2581 587 2580 581 +f 2582 588 2581 582 +f 2583 704 2694 694 +f 2583 575 2536 578 +f 2584 576 2655 704 +f 2584 578 2606 611 +f 2585 715 2628 576 +f 2585 611 2607 583 +f 2586 715 2585 583 +f 2526 604 2582 714 +f 601 2598 573 2530 +f 41 2529 529 1832 +f 39 2531 587 1826 +f 2587 613 2531 39 +f 587 2581 588 2532 +f 2588 618 2543 589 +f 2589 620 2614 548 +f 2593 594 1820 36 +f 34 2525 561 2593 +f 1822 531 1821 595 +f 2517 609 2605 532 +f 2535 516 2500 496 +f 518 2606 578 2518 +f 2508 517 2518 578 +f 2521 524 2536 556 +f 2520 522 2596 557 +f 2519 520 2520 557 +f 526 2597 570 1830 +f 2597 568 2572 570 +f 2598 529 2575 573 +f 2598 601 1833 602 +f 586 2532 588 2600 +f 2588 502 2505 605 +f 509 2504 500 2601 +f 539 2610 621 2602 +f 2603 540 2591 30 +f 2534 592 2592 607 +f 35 1820 594 2594 +f 2604 566 2570 569 +f 608 1822 595 2604 +f 2605 609 2515 600 +f 2606 518 2507 27 +f 27 1810 583 2607 +f 526 1824 569 2597 +f 614 2599 577 2587 +f 535 2601 500 2608 +f 616 1815 29 2609 +f 623 2533 617 2589 +f 2603 590 2590 619 +f 606 2591 558 2611 +f 2608 499 2612 536 +f 2612 499 2513 502 +f 626 2614 506 2613 +f 618 2588 605 2610 +f 2602 622 2533 545 +f 2533 623 2548 545 +f 2612 589 2542 536 +f 615 2609 29 2615 +f 2512 509 2601 624 +f 2615 510 2512 624 +f 2601 535 2539 624 +f 2613 495 2522 625 +f 2614 626 2550 548 +f 1962 629 2420 627 +f 2421 452 1965 113 +f 1967 630 2422 438 +f 2392 437 1969 115 +f 112 1958 628 2616 +f 2419 451 1959 112 +f 1972 118 2620 442 +f 442 2399 634 2617 +f 437 2393 441 2618 +f 1970 116 2618 441 +f 441 2395 632 2619 +f 1971 633 2619 632 +f 632 2397 442 2620 +f 1974 119 2621 450 +f 2417 635 2622 120 +f 1973 631 2617 634 +f 634 2414 450 2621 +f 1593 1907 120 2622 +f 2418 1599 3548 1593 +f 414 2369 416 2623 +f 636 2264 637 2623 +f 638 2552 639 2624 +f 485 2481 484 2624 +f 434 2385 276 2625 +f 2386 434 2625 685 +f 2441 687 2626 673 +f 687 2436 688 2626 +f 316 2654 449 2627 +f 640 2413 713 2627 +f 641 2492 576 2628 +f 2493 641 2628 715 +f 2364 642 2629 302 +f 546 2660 669 2630 +f 644 2668 538 2630 +f 2390 435 2651 286 +f 2631 436 2390 286 +f 2632 465 2444 562 +f 2444 457 2648 562 +f 664 2387 285 2633 +f 2388 664 2633 646 +f 2442 464 2634 559 +f 464 2432 648 2634 +f 318 2269 317 2635 +f 2486 708 2698 709 +f 2367 413 2656 667 +f 2637 410 2367 667 +f 544 2549 547 2638 +f 674 2360 278 2639 +f 2359 674 2639 650 +f 543 2666 676 2640 +f 459 2642 542 2640 +f 430 2359 650 2641 +f 2358 430 2641 671 +f 652 2437 533 2642 +f 678 2362 677 2643 +f 2361 678 2643 281 +f 537 2668 679 2644 +f 653 2666 675 2644 +f 655 2393 440 2645 +f 654 2243 287 2645 +f 2446 466 2670 656 +f 2631 645 2647 440 +f 645 2242 654 2647 +f 2445 657 2646 564 +f 658 2395 659 2649 +f 290 2245 681 2649 +f 2448 683 2672 660 +f 661 2389 684 2651 +f 2443 465 2632 662 +f 663 2382 284 2652 +f 2387 663 2652 285 +f 665 2676 686 2653 +f 461 2442 559 2653 +f 666 2693 448 2654 +f 490 2491 704 2655 +f 2492 490 2655 576 +f 413 2363 668 2656 +f 547 2550 549 2657 +f 668 2368 414 2658 +f 549 2551 638 2659 +f 2365 409 2661 304 +f 2629 642 2365 304 +f 546 2546 541 2660 +f 2637 305 2661 409 +f 541 2548 544 2662 +f 651 2358 671 2663 +f 2385 651 2663 276 +f 2437 672 2664 533 +f 672 2441 673 2664 +f 407 2361 281 2665 +f 2360 407 2665 278 +f 408 2364 302 2667 +f 2362 408 2667 677 +f 2394 655 2645 287 +f 287 2244 290 2669 +f 2447 680 2650 565 +f 2396 658 2649 681 +f 2671 696 2396 681 +f 682 2570 660 2672 +f 2449 697 2686 682 +f 647 2388 646 2673 +f 2389 647 2673 684 +f 2432 458 2674 648 +f 458 2443 662 2674 +f 431 2386 685 2675 +f 2382 431 2675 284 +f 2436 686 2676 688 +f 309 2265 636 2677 +f 486 2482 485 2678 +f 313 2270 318 2679 +f 2487 487 2636 555 +f 2404 691 2681 298 +f 691 2403 297 2681 +f 692 2463 579 2682 +f 2465 692 2682 693 +f 314 2687 425 2683 +f 446 2693 315 2683 +f 489 2489 699 2684 +f 2490 489 2684 694 +f 2671 291 2685 728 +f 567 2571 682 2686 +f 697 2450 729 2686 +f 711 2701 427 2687 +f 488 2488 698 2688 +f 2489 488 2688 699 +f 2403 720 2689 297 +f 474 2465 693 2690 +f 2467 474 2690 574 +f 2400 701 2708 725 +f 572 2574 571 2692 +f 2453 468 2707 572 +f 695 2490 694 2694 +f 2491 695 2694 704 +f 705 2267 310 2695 +f 710 2484 707 2696 +f 317 2268 705 2697 +f 2485 710 2696 554 +f 310 2266 309 2699 +f 707 2483 486 2700 +f 313 2679 424 2701 +f 2559 698 2488 689 +f 2413 712 2702 713 +f 712 2408 716 2702 +f 2586 714 2703 477 +f 2408 736 2704 716 +f 736 2407 735 2704 +f 2703 714 2582 582 +f 2457 717 2703 582 +f 2402 719 2706 718 +f 2689 720 2402 718 +f 2705 721 2471 574 +f 2471 722 2467 574 +f 2401 702 2691 295 +f 723 2575 572 2707 +f 2705 723 2707 468 +f 701 2399 443 2708 +f 571 2573 730 2709 +f 2452 703 2692 571 +f 2398 728 2685 727 +f 730 2572 567 2711 +f 2451 726 2709 730 +f 732 2716 447 2712 +f 731 2405 300 2712 +f 2713 581 2580 733 +f 2461 471 2713 733 +f 2405 445 2714 300 +f 445 2404 298 2714 +f 734 2461 733 2715 +f 2463 734 2715 579 +f 2407 447 2716 735 +f 469 2457 582 2717 +f 2713 471 2459 469 +f 2718 773 1921 846 +f 2719 738 3624 737 +f 737 3621 772 2718 +f 2720 847 2721 739 +f 71 2755 775 2721 +f 2722 741 3644 740 +f 1933 742 2722 740 +f 1839 48 2723 782 +f 2724 745 1963 746 +f 1964 114 2836 1709 +f 2725 744 2724 746 +f 2726 1680 1867 768 +f 2727 1758 1916 87 +f 747 2757 777 2728 +f 68 2738 1736 2728 +f 2729 1754 3645 1753 +f 1939 100 2729 1753 +f 1846 791 2730 1743 +f 2731 758 1936 748 +f 1843 760 2732 1741 +f 1920 749 2749 1765 +f 2733 778 1920 1765 +f 779 3636 771 2734 +f 2735 1747 1912 752 +f 1836 753 2736 754 +f 2737 757 1914 756 +f 46 2765 1735 2738 +f 1942 102 2768 787 +f 2739 1757 1943 103 +f 2740 788 1847 51 +f 1848 52 2775 1746 +f 2741 1752 1937 759 +f 1844 802 2742 1742 +f 2743 762 1935 761 +f 1842 49 2744 1740 +f 811 2792 96 2745 +f 109 1952 809 2745 +f 2746 764 1892 763 +f 1891 78 2793 765 +f 2747 767 1961 766 +f 2748 1681 1868 65 +f 2749 749 1915 91 +f 769 2749 91 2750 +f 774 3620 770 2750 +f 769 3650 1765 2749 +f 2751 776 2752 1771 +f 2752 72 2753 1771 +f 771 3632 1771 2753 +f 91 1919 773 2754 +f 772 3621 774 2754 +f 2752 776 2755 71 +f 1917 88 2758 1762 +f 2756 1760 2727 87 +f 747 3633 1737 2757 +f 2733 1766 2758 88 +f 1737 3634 779 2759 +f 2760 1748 1911 85 +f 1835 44 2761 755 +f 2762 741 2722 98 +f 1840 781 2763 1734 +f 2764 783 1913 86 +f 784 1836 754 2765 +f 1944 794 2774 1707 +f 2766 785 2778 105 +f 2767 796 2777 795 +f 1850 799 2781 786 +f 1941 790 2770 1755 +f 2769 50 1847 788 +f 1940 789 1939 1753 +f 2771 1745 2730 791 +f 2769 792 2771 791 +f 2772 793 2774 794 +f 2773 1707 2774 793 +f 2776 1689 3653 1768 +f 1849 795 2777 1768 +f 2778 1714 1945 105 +f 2779 797 1946 104 +f 1851 53 2780 798 +f 2782 1754 2729 800 +f 2729 100 1938 800 +f 1845 801 2783 803 +f 2784 1751 1934 804 +f 1841 805 2785 743 +f 1954 111 2786 1704 +f 2787 60 1894 1678 +f 1894 807 2816 1678 +f 1952 808 2788 809 +f 2789 1732 2788 808 +f 2790 1677 2791 810 +f 2791 833 1893 810 +f 1731 2818 834 2792 +f 1890 77 2819 1687 +f 1675 2821 812 2794 +f 2794 836 2795 1710 +f 2795 835 3622 1710 +f 2796 1686 1889 813 +f 2797 1700 2796 813 +f 1888 838 2822 839 +f 814 2826 95 2798 +f 815 2820 1721 2798 +f 2799 840 1887 817 +f 1886 79 2827 816 +f 2800 1729 1960 818 +f 2801 820 3610 819 +f 1869 66 2801 819 +f 1957 848 2831 822 +f 2802 852 1956 821 +f 2803 824 1862 823 +f 1955 851 2804 825 +f 2804 1706 3617 825 +f 2787 806 3608 853 +f 1950 107 2809 827 +f 2806 1673 1966 826 +f 1857 58 2807 1696 +f 1949 108 2840 1724 +f 2808 1726 2809 107 +f 1856 828 2810 829 +f 1947 830 2813 831 +f 2811 832 1948 106 +f 1855 857 2812 1665 +f 1852 55 2814 1664 +f 1953 110 2815 1703 +f 2817 835 2795 834 +f 2818 837 2817 834 +f 2820 812 2821 1722 +f 1717 2829 92 2823 +f 841 2825 1718 2823 +f 842 3605 845 2824 +f 74 1884 843 2824 +f 2825 95 2826 844 +f 845 3605 1671 2828 +f 79 1885 74 2828 +f 2829 738 2719 92 +f 2830 842 2824 843 +f 2720 1669 2830 843 +f 2832 849 2831 848 +f 2833 1679 2834 850 +f 2834 820 2801 850 +f 2801 66 1864 850 +f 2804 851 1956 852 +f 2805 853 3609 854 +f 1860 61 2805 854 +f 2835 1674 2836 114 +f 1866 64 2837 1697 +f 2838 1698 2837 64 +f 2839 855 2840 108 +f 1865 57 2841 856 +f 2842 858 2841 57 +f 2843 860 2844 859 +f 863 3640 1744 2843 +f 860 1853 54 2844 +f 101 1938 861 2845 +f 1548 2846 862 2845 +f 2847 863 1834 45 +f 1932 101 2848 1749 +f 1900 81 1908 865 +f 123 1908 81 2849 +f 1899 867 1898 213 +f 2111 866 2849 81 +f 866 2138 122 2849 +f 2850 124 1976 117 +f 2851 1022 2850 117 +f 2852 1021 2851 117 +f 2853 868 2852 117 +f 2854 1108 2853 117 +f 2855 1059 2854 117 +f 2856 869 2855 117 +f 2857 1054 2856 117 +f 2858 870 2857 117 +f 2859 871 2858 117 +f 2860 1068 2859 117 +f 2861 872 2860 117 +f 2862 1063 2861 117 +f 2863 874 2864 873 +f 2865 1075 2866 874 +f 2866 875 2867 874 +f 2867 1039 2868 874 +f 2868 876 2869 874 +f 2869 877 2870 874 +f 2870 878 2871 874 +f 2871 1031 2872 874 +f 2872 879 2873 874 +f 2873 881 2874 874 +f 2874 880 2875 874 +f 2875 882 2876 874 +f 2876 1065 2864 874 +f 2850 1022 3183 1117 +f 2877 883 2878 124 +f 2878 884 2879 124 +f 2879 885 2880 124 +f 2880 1116 2881 124 +f 2881 886 2882 124 +f 2882 1114 2883 124 +f 2883 1045 2884 124 +f 2884 887 2885 124 +f 2885 1112 2886 124 +f 2886 888 2887 124 +f 2887 889 2893 124 +f 2888 1026 2889 891 +f 2889 894 2894 891 +f 2890 891 2891 890 +f 2892 1023 2891 891 +f 2894 893 2895 891 +f 2895 895 2896 891 +f 2896 1093 2897 891 +f 2897 896 2898 891 +f 2898 897 2899 891 +f 2899 898 2900 891 +f 2900 899 2901 891 +f 2901 1052 2902 891 +f 2902 892 2892 891 +f 2903 900 2904 904 +f 2890 874 2903 904 +f 905 2979 952 2904 +f 121 1907 84 2905 +f 902 2987 901 2905 +f 2906 902 2905 84 +f 2893 891 2908 903 +f 1977 124 2893 903 +f 904 2904 952 2907 +f 2907 946 2908 891 +f 2909 905 2904 900 +f 2910 953 2903 874 +f 2911 906 1975 121 +f 2863 117 1975 906 +f 2912 940 2913 907 +f 2914 1161 2913 940 +f 2915 1160 2914 940 +f 2916 1210 2915 940 +f 2917 908 2916 940 +f 2918 909 2917 940 +f 2919 910 2918 940 +f 2920 911 2919 940 +f 2921 1171 2920 940 +f 2922 912 2921 940 +f 2923 1132 2922 940 +f 2924 1213 2923 940 +f 2925 913 2924 940 +f 2926 933 2927 914 +f 2928 916 2929 933 +f 2929 917 2930 933 +f 2930 1139 2931 933 +f 2931 918 2932 933 +f 2932 1177 2933 933 +f 2933 919 2934 933 +f 2934 920 2935 933 +f 2935 1142 2936 933 +f 2936 921 2937 933 +f 2937 922 2938 933 +f 2938 915 2927 933 +f 2939 923 2940 927 +f 2940 924 2941 927 +f 2941 1192 2942 927 +f 2942 925 2943 927 +f 2943 926 2944 927 +f 2944 928 2945 927 +f 2945 1218 2946 927 +f 2946 1190 2947 927 +f 2947 1148 2948 927 +f 2948 1187 2949 927 +f 2949 929 2950 927 +f 2950 930 2951 927 +f 2951 931 2959 927 +f 2952 1201 2953 80 +f 2953 1200 2960 80 +f 1896 80 2954 932 +f 2955 1120 2954 80 +f 2956 1207 2955 80 +f 2957 934 2956 80 +f 2958 1130 2957 80 +f 2960 1197 2961 80 +f 2961 1198 2962 80 +f 2962 1156 2963 80 +f 2963 935 2964 80 +f 2964 1204 2958 80 +f 942 2970 944 2965 +f 936 3056 1000 2965 +f 937 3047 993 2966 +f 2967 937 2966 938 +f 2926 940 2969 939 +f 1895 933 2926 939 +f 942 2965 1000 2968 +f 2968 941 2969 940 +f 2912 927 2970 942 +f 2971 936 2965 944 +f 2972 943 2970 927 +f 2973 995 1897 945 +f 2959 80 1897 995 +f 2908 946 2974 947 +f 2975 958 2910 906 +f 2976 950 3028 981 +f 905 2909 958 2977 +f 2977 957 2983 950 +f 2976 949 2978 951 +f 2979 905 2977 950 +f 2979 951 2980 952 +f 2980 954 2981 952 +f 948 2974 946 2981 +f 2909 953 2910 958 +f 2982 962 2988 954 +f 2980 951 2978 955 +f 956 2985 960 2983 +f 956 2983 957 2984 +f 2975 959 2985 956 +f 2906 903 2908 947 +f 2911 901 2985 959 +f 2985 901 2987 960 +f 961 3015 974 2986 +f 2987 955 2986 960 +f 2982 955 2987 902 +f 2988 962 2982 902 +f 2989 963 2990 961 +f 2988 947 2974 954 +f 2974 948 2981 954 +f 2991 965 2990 963 +f 2992 964 2991 963 +f 2993 1092 2992 963 +f 2994 966 2993 963 +f 2995 968 2994 963 +f 2996 967 2995 963 +f 2997 1086 2996 963 +f 2998 1089 2997 963 +f 2999 969 2998 963 +f 3000 970 2999 963 +f 3001 1081 3000 963 +f 3002 1083 3001 963 +f 1098 3002 963 3003 +f 3003 971 3004 1099 +f 3005 1047 3004 971 +f 3006 1095 3005 971 +f 3007 1096 3006 971 +f 3008 1050 3007 971 +f 3009 972 3008 971 +f 3010 1102 3009 971 +f 3011 1100 3010 971 +f 3012 973 3011 971 +f 3013 1106 3012 971 +f 3014 1104 3013 971 +f 3015 975 3016 974 +f 3016 1053 3017 974 +f 3017 976 3018 974 +f 3018 1061 3019 974 +f 3019 1060 3020 974 +f 3020 977 3021 974 +f 3021 1056 3022 974 +f 3022 978 3023 974 +f 3023 979 3024 974 +f 3024 1037 3025 974 +f 3025 980 3026 974 +f 3026 1067 3027 974 +f 3027 1066 3028 974 +f 3029 1080 3030 981 +f 3030 1043 3031 981 +f 3031 982 3032 981 +f 3032 983 3033 981 +f 3033 1076 3034 981 +f 3034 985 3035 981 +f 3035 984 3036 981 +f 3036 1040 3037 981 +f 3037 1041 3038 981 +f 3038 1074 3039 981 +f 3039 986 3040 981 +f 3040 949 2976 981 +f 2989 955 2978 971 +f 950 2983 960 3041 +f 2972 995 3042 987 +f 3043 990 2969 941 +f 3044 988 3097 1018 +f 937 2967 990 3045 +f 3045 989 3051 988 +f 3044 1012 3046 991 +f 3047 937 3045 988 +f 3047 991 3048 993 +f 3048 994 3049 993 +f 2973 993 3049 992 +f 2967 939 2969 990 +f 3050 996 3057 994 +f 3048 991 3046 1020 +f 998 3054 997 3051 +f 998 3051 989 3052 +f 3043 999 3053 998 +f 2971 943 2972 987 +f 999 3043 941 3053 +f 1000 3054 998 3053 +f 1184 3084 1015 3055 +f 3054 1000 3056 1020 +f 3055 997 3054 1020 +f 3050 1020 3056 936 +f 3057 996 3050 936 +f 3058 1004 3059 1184 +f 3057 987 3042 994 +f 3042 992 3049 994 +f 3060 1185 3059 1004 +f 3061 1224 3060 1004 +f 3062 1193 3061 1004 +f 3063 1001 3062 1004 +f 3064 1002 3063 1004 +f 3065 1220 3064 1004 +f 3066 1003 3065 1004 +f 3067 1151 3066 1004 +f 3068 1188 3067 1004 +f 3069 1189 3068 1004 +f 3070 1005 3069 1004 +f 3071 1121 3070 1004 +f 1123 3071 1004 3072 +f 3072 1006 3073 1203 +f 3074 1153 3073 1006 +f 3075 1155 3074 1006 +f 3076 1007 3075 1006 +f 3077 1125 3076 1006 +f 3078 1008 3077 1006 +f 3079 1010 3078 1006 +f 3080 1009 3079 1006 +f 3081 1131 3080 1006 +f 3082 1011 3081 1006 +f 3083 1208 3082 1006 +f 3084 1162 3085 1015 +f 3085 1013 3086 1015 +f 3086 1165 3087 1015 +f 3087 1169 3088 1015 +f 3088 1211 3089 1015 +f 3089 1167 3090 1015 +f 3090 1166 3091 1015 +f 3091 1172 3092 1015 +f 3092 1137 3093 1015 +f 3093 1136 3094 1015 +f 3094 1216 3095 1015 +f 3095 1014 3096 1015 +f 3096 1170 3097 1015 +f 3098 1147 3099 1018 +f 3099 1016 3100 1018 +f 3100 1129 3101 1018 +f 3101 1144 3102 1018 +f 3102 1182 3103 1018 +f 3103 1178 3104 1018 +f 3104 1176 3105 1018 +f 3105 1141 3106 1018 +f 3106 1017 3107 1018 +f 3107 1173 3108 1018 +f 3108 1019 3109 1018 +f 3109 1012 3044 1018 +f 988 3051 997 3110 +f 3058 1020 3046 1006 +f 3015 961 3111 1387 +f 3112 1290 2851 1021 +f 3113 1436 3014 949 +f 2891 1023 3114 1024 +f 2888 889 3170 1025 +f 3115 1027 2889 1026 +f 3116 1082 3001 1083 +f 3002 1098 3189 1447 +f 2898 896 3139 1028 +f 3117 897 2898 1028 +f 3118 1049 3007 1050 +f 3008 972 3009 1030 +f 2871 878 2870 1298 +f 3119 1275 2872 1031 +f 3120 985 3034 1077 +f 3034 1076 3167 1077 +f 2874 881 3131 1042 +f 3121 1271 2875 880 +f 3122 1032 3032 982 +f 3031 1043 3134 1377 +f 2901 899 3123 1033 +f 3124 1034 3011 973 +f 2860 872 3151 1035 +f 3125 1036 2859 1068 +f 3126 1038 3025 1037 +f 3024 979 3156 1423 +f 2858 871 3155 1232 +f 3127 1269 2857 870 +f 3128 1069 3023 978 +f 3022 1056 3148 1431 +f 2868 1039 3129 1250 +f 3130 1419 3037 1040 +f 2873 879 3166 1296 +f 3132 1078 3033 983 +f 3133 1079 2876 882 +f 3030 1080 3169 1044 +f 2884 1045 3174 1259 +f 3135 1288 2885 887 +f 3136 1088 2997 1089 +f 2998 969 3173 1084 +f 2895 893 3186 1251 +f 3137 1046 2896 895 +f 3138 1097 3004 1047 +f 3005 1095 3185 1407 +f 2897 1093 3184 1094 +f 3140 1048 3006 1096 +f 2902 1052 3141 1051 +f 3142 1411 3012 1106 +f 3143 1231 2852 868 +f 3016 975 3144 1444 +f 3145 1107 2854 1059 +f 3018 976 3146 1430 +f 3147 1054 2857 1269 +f 3021 977 3020 1110 +f 3149 1058 2855 869 +f 3019 1061 3150 1445 +f 2861 1063 3198 1062 +f 3152 1064 3026 980 +f 2864 1065 3168 1244 +f 3153 1243 2862 873 +f 3154 1402 3029 1066 +f 3027 1067 3199 1111 +f 2866 1075 3157 1073 +f 3158 1071 3039 1074 +f 2869 876 3159 1277 +f 3160 1072 3036 984 +f 2870 877 3161 1298 +f 3120 1420 3385 1400 +f 3035 985 3120 1400 +f 2867 875 3162 1279 +f 3163 1397 3038 1041 +f 3164 1378 3040 986 +f 2865 890 3165 1070 +f 2887 888 3200 1283 +f 3171 1439 3000 1081 +f 3172 1286 2886 1112 +f 2999 970 3201 1113 +f 2883 1114 3202 1085 +f 3175 1087 2996 1086 +f 3176 1090 2881 1116 +f 2995 967 3177 1091 +f 3177 1415 3399 1091 +f 3178 1227 2879 884 +f 2993 966 3179 1389 +f 3180 1228 2878 883 +f 2992 1092 3181 1412 +f 2990 965 3182 1393 +f 1022 2851 1290 3183 +f 1292 3205 1117 3183 +f 2894 894 3188 1280 +f 3187 1099 3004 1097 +f 3187 1424 3189 1098 +f 2900 898 3190 1281 +f 3191 1425 3010 1100 +f 2899 897 3117 1101 +f 3117 1029 3313 1101 +f 3192 1030 3009 1102 +f 2892 892 3193 1103 +f 3194 1105 3013 1104 +f 3195 1294 2853 1108 +f 3017 1053 3196 1109 +f 3147 1055 3317 1057 +f 2856 1054 3147 1057 +f 3020 1060 3197 1110 +f 2882 886 2881 1090 +f 3177 967 2996 1087 +f 3203 1115 2880 885 +f 2994 968 3204 1417 +f 3205 1230 2877 1117 +f 2991 964 3206 1394 +f 3084 1184 3207 1529 +f 3208 1119 2913 1161 +f 3209 1451 3083 1012 +f 2954 1120 3210 1323 +f 2951 930 3268 1186 +f 3211 1371 2952 931 +f 3212 1526 3070 1121 +f 3071 1123 3285 1122 +f 2963 1156 3235 1124 +f 3213 935 2963 1124 +f 3214 1157 3076 1125 +f 3077 1008 3078 1206 +f 2934 919 2933 1370 +f 3215 1181 2935 920 +f 3216 1178 3103 1126 +f 3103 1182 3263 1126 +f 2937 921 3227 1127 +f 3217 1145 2938 922 +f 3218 1457 3101 1129 +f 3100 1016 3230 1128 +f 2957 1130 3219 1351 +f 3220 1468 3080 1131 +f 2922 1132 3247 1133 +f 3221 1134 2921 912 +f 3222 1504 3094 1136 +f 3093 1137 3252 1135 +f 2920 1171 3251 1138 +f 3223 1339 2919 911 +f 3224 1481 3092 1172 +f 3091 1166 3244 1517 +f 2931 1139 3225 1175 +f 3226 1498 3106 1141 +f 2936 1142 3262 1143 +f 3228 1479 3102 1144 +f 3229 1146 2927 915 +f 3099 1147 3265 1501 +f 2947 1190 3272 1314 +f 3231 1330 2948 1148 +f 3232 1149 3066 1151 +f 3067 1188 3271 1150 +f 2960 1200 3282 1349 +f 3233 1152 2961 1197 +f 3234 1507 3073 1153 +f 3074 1155 3281 1154 +f 2962 1198 3280 1313 +f 3236 1199 3075 1007 +f 2956 934 3237 1158 +f 3238 1485 3081 1011 +f 3239 1118 2914 1160 +f 3085 1162 3240 1163 +f 3241 1209 2916 908 +f 3087 1165 3242 1164 +f 3243 910 2919 1339 +f 3090 1167 3089 1503 +f 3245 1336 2917 909 +f 3088 1169 3246 1168 +f 2923 1213 3294 1214 +f 3248 1215 3095 1216 +f 2925 914 3264 1341 +f 3249 1212 2924 913 +f 3250 1183 3098 1170 +f 3096 1014 3295 1466 +f 2929 916 3253 1179 +f 3254 1473 3108 1173 +f 2932 918 3255 1174 +f 3256 1476 3105 1176 +f 2933 1177 3257 1370 +f 3216 1478 3456 1499 +f 3104 1178 3216 1499 +f 2930 917 3258 1140 +f 3259 1180 3107 1017 +f 3260 1450 3109 1019 +f 2928 932 3261 1324 +f 3059 1185 3266 1470 +f 3267 1366 2939 907 +f 2950 929 3296 1362 +f 3269 1217 3069 1005 +f 3270 1364 2949 1187 +f 3068 1189 3297 1523 +f 2946 1218 3298 1219 +f 3273 1191 3065 1003 +f 3274 1331 2944 926 +f 3064 1220 3275 1493 +f 3275 1221 3470 1493 +f 3276 1333 2942 1192 +f 3062 1001 3277 1196 +f 3278 1194 2941 924 +f 3061 1193 3279 1195 +f 2953 1201 3284 1372 +f 3283 1203 3073 1507 +f 3283 1202 3285 1123 +f 2958 1204 3286 1352 +f 3287 1483 3079 1009 +f 2964 935 3213 1205 +f 3213 1356 3329 1205 +f 3288 1206 3078 1010 +f 2955 1207 3289 1327 +f 3290 1159 3082 1208 +f 3291 1308 2915 1210 +f 3086 1013 3292 1515 +f 3243 1316 3347 1338 +f 2918 910 3243 1338 +f 3089 1211 3293 1503 +f 2945 928 2944 1331 +f 3275 1220 3065 1191 +f 3299 1222 2943 925 +f 3063 1002 3300 1492 +f 3301 1223 2940 923 +f 3060 1224 3302 1471 +f 2049 1291 2050 1290 +f 3112 1231 2137 1225 +f 1257 2064 173 3303 +f 2036 1297 2165 1296 +f 2076 179 3184 1046 +f 2058 170 3203 1227 +f 2055 1229 2056 1228 +f 3180 1230 2053 1237 +f 2054 169 2055 1228 +f 2140 221 3143 1294 +f 2139 220 2137 1231 +f 3155 1036 2155 1268 +f 2154 1233 2153 1232 +f 3125 1035 2157 1234 +f 2156 1235 2155 1036 +f 1244 3168 1272 3304 +f 3139 1094 2079 191 +f 2093 190 3323 1028 +f 1029 2091 189 3305 +f 2082 182 2081 1103 +f 3306 1259 3174 1300 +f 3205 1292 2052 168 +f 2143 222 2142 1107 +f 2142 1239 2141 1107 +f 1055 3147 1269 3307 +f 2150 1240 2149 1055 +f 2152 224 3127 1232 +f 2153 225 2152 1232 +f 1079 3133 228 3308 +f 3153 1244 3304 1242 +f 1242 2161 1245 3309 +f 2160 227 3198 1243 +f 1070 3165 1246 3310 +f 2041 161 2040 1298 +f 3161 1277 2042 1276 +f 3311 1279 3162 1247 +f 3162 1073 3321 1247 +f 3312 1249 2043 1248 +f 3312 1250 3129 163 +f 3129 1279 3320 163 +f 3186 1280 2073 1252 +f 2074 177 3137 1251 +f 2071 176 3188 1027 +f 3141 1033 2084 192 +f 3190 1101 3313 186 +f 3313 187 2089 186 +f 3314 1024 3114 1255 +f 3114 1103 2081 1255 +f 1259 3306 1256 3315 +f 3135 1259 3315 1257 +f 1258 2061 1301 3316 +f 1236 2063 1256 3306 +f 2069 175 3115 1025 +f 3176 1115 2059 1260 +f 2060 171 3316 1090 +f 3178 1228 2056 1261 +f 2057 1262 2058 1227 +f 2144 1238 3145 1058 +f 2147 1263 3317 1055 +f 1263 2146 1057 3317 +f 2145 1266 2144 1058 +f 2148 1264 2147 1055 +f 2149 1267 2148 1055 +f 2151 223 3307 1269 +f 3121 1042 3318 229 +f 1271 3121 229 3319 +f 3133 1271 3319 228 +f 3308 1241 2162 1272 +f 3168 1079 3308 1272 +f 3151 1062 2158 1273 +f 2159 226 2158 1062 +f 3310 1274 2046 165 +f 2039 160 2038 1298 +f 2037 1226 3166 1275 +f 3159 1250 3312 1248 +f 1279 3311 1278 3320 +f 3157 1070 3310 165 +f 2109 200 2079 1094 +f 2072 1299 2073 1280 +f 2086 183 2085 1033 +f 2085 1254 2084 1033 +f 3123 1281 3322 184 +f 3322 197 2087 184 +f 1281 3190 186 3322 +f 1029 3117 1028 3323 +f 2092 198 2091 1029 +f 3193 1051 3324 1282 +f 1051 3141 192 3324 +f 3165 1024 3314 1246 +f 3314 181 2048 1246 +f 2067 1284 2068 1283 +f 2068 174 3170 1283 +f 2066 1285 3200 1286 +f 2065 1287 3172 1288 +f 2070 1253 2071 1027 +f 2051 1289 2052 1292 +f 2050 167 3325 1290 +f 167 2051 1292 3325 +f 2141 1293 3195 1107 +f 2146 1265 3149 1057 +f 2165 1295 3328 1296 +f 2040 159 2039 1298 +f 2038 158 3119 1298 +f 2075 178 2076 1046 +f 1085 3202 172 3326 +f 3174 1085 3326 1300 +f 1090 3316 1301 3327 +f 3202 1090 3327 172 +f 1042 3131 1302 3318 +f 3131 1296 3328 1302 +f 2078 202 2109 1094 +f 1355 3354 1205 3329 +f 1356 3339 214 3329 +f 3208 1118 3331 235 +f 1309 2170 235 3331 +f 1329 2077 1304 3332 +f 1143 3262 1347 3333 +f 2134 210 3280 1152 +f 1306 2088 185 3334 +f 2101 1334 2102 1194 +f 3278 1223 2099 195 +f 2100 1307 2101 1194 +f 1308 3291 1367 3335 +f 3239 1308 3335 236 +f 3331 1118 3239 236 +f 3251 1134 2183 1310 +f 2182 244 2181 1138 +f 3221 1133 2185 1343 +f 2184 1311 2183 1134 +f 1341 3264 1340 3336 +f 3235 1313 3338 211 +f 3337 1124 3235 211 +f 3339 215 2122 214 +f 2127 1328 2125 1327 +f 2107 201 2108 1314 +f 3301 1366 2098 1335 +f 3340 1209 3241 238 +f 237 2172 1315 3340 +f 1316 3243 1339 3341 +f 2178 1317 2177 1316 +f 2180 243 3223 1138 +f 2181 245 2180 1138 +f 1146 3229 248 3342 +f 2030 155 3253 1324 +f 152 2024 1319 3343 +f 3255 1175 3344 152 +f 2025 1321 3344 1175 +f 3225 1140 2026 1320 +f 2027 153 2026 1140 +f 2117 209 3284 1371 +f 2130 1322 2124 1351 +f 3261 1323 2031 1325 +f 2125 1326 3210 1327 +f 2094 1329 3231 1314 +f 2104 199 2105 1331 +f 2108 203 2094 1314 +f 2115 207 3211 1186 +f 180 2080 1332 3345 +f 3276 1194 2102 196 +f 2103 1306 3334 1333 +f 3346 1336 3245 1337 +f 2175 240 3347 1316 +f 240 2174 1338 3347 +f 2174 1337 3245 1338 +f 2176 239 2175 1316 +f 2177 241 2176 1316 +f 2179 242 3341 1339 +f 3348 250 2190 249 +f 3217 1127 3348 249 +f 1145 3217 249 3349 +f 3229 1145 3349 248 +f 2193 1340 3264 1146 +f 3247 1214 2186 1342 +f 2188 1344 3294 1212 +f 3249 1341 3336 1312 +f 1312 2192 247 3350 +f 2187 246 2186 1214 +f 2033 157 2030 1324 +f 1370 3353 1346 3351 +f 1181 3215 150 3352 +f 3262 1181 3352 1347 +f 3257 1174 3343 1319 +f 2028 1348 2027 1140 +f 3258 1179 2029 154 +f 3233 1349 2135 219 +f 2119 1305 2134 1152 +f 3282 1372 2118 218 +f 2124 1350 2129 1351 +f 3219 1352 2123 1353 +f 3286 1205 3354 1354 +f 2131 212 2123 1352 +f 1356 2133 1357 3339 +f 3337 216 2133 1356 +f 3289 1158 2128 1358 +f 3237 1351 2129 1359 +f 2032 1360 2031 1323 +f 2105 1361 3298 1331 +f 2113 205 2114 1362 +f 2114 206 3268 1362 +f 2112 204 3296 1364 +f 2110 1363 3270 1330 +f 2116 208 2117 1371 +f 3274 1222 3355 1365 +f 3345 1331 3274 1365 +f 3299 1333 3334 185 +f 185 2089 1365 3355 +f 2097 194 2098 1366 +f 2096 193 3267 1119 +f 1209 3340 1315 3356 +f 3291 1209 3356 1367 +f 3346 1368 2173 238 +f 3241 1336 3346 238 +f 1370 3351 151 3357 +f 3215 1370 3357 150 +f 1313 3358 1375 3338 +f 2136 1373 2118 1372 +f 2106 1374 3272 1219 +f 1127 3227 253 3348 +f 2194 253 3227 1143 +f 3111 1393 3359 1376 +f 3359 1604 3543 1376 +f 3360 1556 3519 1565 +f 3361 1553 3122 1377 +f 3134 1044 3404 1552 +f 3362 1605 3552 1603 +f 3363 1399 3563 1379 +f 3113 1378 3363 1379 +f 3364 1589 3545 1592 +f 3365 1380 3522 1381 +f 3366 1382 3544 1588 +f 3367 1383 3572 1626 +f 3368 1384 3554 1606 +f 1385 3167 1078 3369 +f 1390 3132 1032 3370 +f 1600 3409 1423 3371 +f 3366 1387 3111 1376 +f 3372 1621 3561 1388 +f 3362 1394 3206 1413 +f 3181 1389 3373 1607 +f 3374 1426 3559 1610 +f 3375 1419 3130 1557 +f 3375 1561 3524 1567 +f 3376 1032 3122 1553 +f 3377 1554 3369 1078 +f 3376 1391 3516 1386 +f 3378 1402 3154 1549 +f 3154 1111 3410 1549 +f 1446 3189 1424 3379 +f 3124 1411 3372 1388 +f 3380 1034 3124 1388 +f 3359 1393 3182 1603 +f 3182 1394 3362 1603 +f 3381 1417 3204 1414 +f 3382 1084 3397 1396 +f 3365 1071 3158 1398 +f 3383 1398 3158 1397 +f 3384 1563 3520 1557 +f 3130 1072 3384 1557 +f 3363 1378 3164 1381 +f 3164 1071 3365 1381 +f 3384 1072 3160 1568 +f 3385 1564 3527 1568 +f 3160 1400 3385 1568 +f 3360 1420 3403 1401 +f 3386 1077 3167 1385 +f 3169 1402 3378 1550 +f 3378 1403 3515 1550 +f 3387 1404 3197 1445 +f 3150 1430 3406 1594 +f 3388 1064 3152 1601 +f 3156 1069 3389 1405 +f 3389 1598 3549 1405 +f 1406 3420 1407 3390 +f 1632 3390 1407 3391 +f 3118 1030 3392 1408 +f 3380 1622 3568 1623 +f 3192 1425 3416 1409 +f 3393 1438 3367 1030 +f 3372 1411 3394 1620 +f 3142 1105 3419 1410 +f 3206 1412 3395 1413 +f 1612 3422 1439 3396 +f 3173 1113 3421 1611 +f 3398 1609 3557 1608 +f 3204 1091 3399 1414 +f 3399 1416 3556 1414 +f 3400 1416 3399 1415 +f 3179 1417 3381 1418 +f 3381 1395 3555 1418 +f 3368 1389 3179 1418 +f 3401 1558 3163 1419 +f 3402 1564 3385 1420 +f 3386 1421 3403 1420 +f 1551 3404 1044 3405 +f 3406 1430 3146 1592 +f 3146 1109 3364 1592 +f 3407 1433 3547 1596 +f 3388 1602 3551 1422 +f 3152 1038 3408 1601 +f 3199 1064 3388 1422 +f 3411 1616 3562 1618 +f 3411 1436 3113 1379 +f 3412 1635 3577 1392 +f 3412 1424 3187 1097 +f 3413 1635 3412 1097 +f 3414 1048 3140 1627 +f 3140 1049 3415 1627 +f 3191 1034 3380 1623 +f 3374 1088 3136 1428 +f 3136 1084 3382 1428 +f 3382 1427 3560 1428 +f 3398 1415 3425 1429 +f 3406 1591 3546 1594 +f 3364 1109 3427 1590 +f 3148 1110 3417 1432 +f 3407 1110 3429 1595 +f 3389 1069 3128 1597 +f 3128 1431 3418 1597 +f 3126 1423 3409 1434 +f 3409 1435 3550 1434 +f 3194 1436 3411 1618 +f 3138 1407 3420 1437 +f 3185 1048 3414 1630 +f 3414 1629 3575 1630 +f 3201 1439 3422 1440 +f 3171 1082 3423 1441 +f 3424 1613 3423 1082 +f 3116 1447 3430 1442 +f 1443 3558 1429 3425 +f 1415 3177 1087 3425 +f 3426 1443 3425 1087 +f 3175 1088 3374 1610 +f 3196 1444 3428 1587 +f 3144 1387 3366 1588 +f 3207 1470 3431 1566 +f 3431 1559 3521 1566 +f 3432 1580 3535 1448 +f 3433 1584 3218 1128 +f 3230 1501 3475 1575 +f 3434 1617 3562 1615 +f 3435 1449 3580 1639 +f 3209 1450 3435 1639 +f 3436 1562 3525 1452 +f 3437 1472 3538 1453 +f 3438 1560 3523 1454 +f 3439 1645 3587 1644 +f 3440 1455 3561 1460 +f 3441 1581 3541 1582 +f 3442 1583 3534 1456 +f 3443 1135 3252 1458 +f 3438 1529 3207 1566 +f 3444 1459 3579 1638 +f 3434 1471 3302 1619 +f 3279 1196 3445 1488 +f 3446 1633 3574 1628 +f 3447 1498 3226 1461 +f 3447 1576 3537 1462 +f 3448 1463 3442 1457 +f 3441 1479 3228 1456 +f 3228 1457 3442 1456 +f 3449 1464 3458 1183 +f 3250 1466 3479 1465 +f 1467 3285 1202 3450 +f 3220 1485 3444 1638 +f 3451 1468 3220 1638 +f 3431 1470 3266 1615 +f 3266 1471 3434 1615 +f 3452 1492 3300 1624 +f 1634 3486 1150 3453 +f 3437 1473 3254 1577 +f 3454 1577 3254 1180 +f 3455 1474 3540 1461 +f 3226 1476 3455 1461 +f 3435 1450 3260 1453 +f 3260 1473 3437 1453 +f 3455 1476 3256 1475 +f 3456 1579 3536 1475 +f 3256 1499 3456 1475 +f 3432 1478 3474 1477 +f 3457 1126 3263 1582 +f 3263 1479 3441 1582 +f 3265 1183 3458 1586 +f 3459 1530 3293 1168 +f 3246 1164 3476 1513 +f 3460 1215 3248 1574 +f 3252 1481 3461 1458 +f 3461 1571 3529 1458 +f 3462 1643 3585 1642 +f 3462 1154 3281 1521 +f 3214 1206 3463 1482 +f 3451 1469 3583 1646 +f 3288 1483 3485 1511 +f 3464 1484 3439 1206 +f 3444 1485 3465 1486 +f 3238 1159 3490 1487 +f 3302 1195 3466 1619 +f 1636 3493 1217 3467 +f 3468 1490 3453 1150 +f 3271 1523 3492 1524 +f 3469 1512 3571 1491 +f 3300 1493 3470 1624 +f 3470 1494 3570 1624 +f 3471 1494 3470 1221 +f 3277 1492 3452 1495 +f 3452 1496 3568 1495 +f 3440 1196 3277 1495 +f 3472 1497 3259 1498 +f 3473 1579 3456 1478 +f 3457 1500 3474 1478 +f 3475 1585 3533 1575 +f 3475 1501 3265 1586 +f 3476 1164 3242 1452 +f 3242 1515 3436 1452 +f 3477 1555 3528 1570 +f 3460 1480 3532 1505 +f 3248 1504 3478 1574 +f 3443 1572 3530 1520 +f 3295 1215 3460 1505 +f 3480 1506 3581 1640 +f 3480 1451 3209 1639 +f 3481 1508 3584 1641 +f 3481 1202 3283 1507 +f 3482 1508 3481 1507 +f 3483 1199 3236 1510 +f 3236 1157 3484 1510 +f 3287 1468 3451 1646 +f 3446 1149 3232 1631 +f 3232 1150 3486 1631 +f 3469 1221 3496 1625 +f 3476 1502 3526 1513 +f 3436 1515 3498 1514 +f 3244 1503 3487 1518 +f 3477 1503 3500 1569 +f 3461 1481 3224 1516 +f 3224 1517 3488 1516 +f 3222 1135 3489 1519 +f 3290 1451 3480 1640 +f 3234 1154 3491 1509 +f 3281 1199 3483 1521 +f 3483 1522 3586 1521 +f 3297 1217 3493 1525 +f 3269 1526 3494 1489 +f 3495 1637 3494 1526 +f 3212 1122 3501 1527 +f 1614 3573 1625 3496 +f 1221 3275 1191 3496 +f 3497 1614 3496 1191 +f 3273 1149 3446 1628 +f 3292 1163 3499 1528 +f 3240 1529 3438 1454 +f 2473 475 1812 26 +f 403 2356 1531 3502 +f 2415 403 3502 391 +f 1814 31 2434 460 +f 2354 402 2357 1533 +f 2199 257 3503 146 +f 2007 231 2198 1534 +f 2200 256 3504 1535 +f 2337 388 2336 1540 +f 1536 2337 1540 3505 +f 3505 166 2047 1537 +f 2047 162 2203 1537 +f 2204 1538 3506 142 +f 1538 2207 252 3506 +f 3507 230 2166 1539 +f 3508 1540 3509 454 +f 3509 1542 2427 454 +f 2166 166 3508 1539 +f 3509 1540 3510 386 +f 1540 2335 1541 3510 +f 2353 401 2346 1542 +f 2206 1543 3511 234 +f 2191 252 2207 259 +f 3507 1544 2198 231 +f 2336 387 2335 1540 +f 3511 1543 2203 162 +f 3512 1546 3513 860 +f 3513 56 1853 860 +f 1898 1547 2126 213 +f 1978 122 2138 1548 +f 3514 1553 3361 1599 +f 3410 1422 3551 1599 +f 1403 3378 1549 3515 +f 3515 1599 3405 1550 +f 3404 1551 3405 1599 +f 3361 1552 3404 1599 +f 3514 1573 3516 1391 +f 3370 1386 3516 1573 +f 3377 1390 3370 1573 +f 3369 1554 3377 1573 +f 1421 3386 1385 3517 +f 3403 1421 3517 1573 +f 1556 3360 1401 3518 +f 3518 1573 3528 1555 +f 3520 1452 3525 1557 +f 3383 1558 3499 1454 +f 1559 3563 1399 3521 +f 3522 1566 3521 1381 +f 1560 3522 1380 3523 +f 3401 1567 3498 1528 +f 3524 1514 3498 1567 +f 1562 3524 1561 3525 +f 3526 1502 3520 1563 +f 3519 1555 3477 1569 +f 3523 1398 3383 1454 +f 3526 1568 3527 1513 +f 3459 1513 3527 1564 +f 3500 1530 3459 1564 +f 3402 1565 3519 1569 +f 3487 1570 3528 1573 +f 3488 1518 3487 1573 +f 1571 3461 1516 3529 +f 3529 1573 3530 1458 +f 1572 3443 1458 3530 +f 3530 1573 3489 1520 +f 3478 1519 3489 1573 +f 1480 3460 1574 3531 +f 2426 1578 3532 1480 +f 1578 3479 1505 3532 +f 3533 1578 3433 1575 +f 3474 1500 3541 1578 +f 3535 1578 3473 1448 +f 3537 1578 3472 1462 +f 3538 1578 3539 1453 +f 3539 1449 3435 1453 +f 3538 1472 3437 1577 +f 3454 1497 3472 1578 +f 3537 1576 3447 1461 +f 1578 3537 1461 3540 +f 1578 3540 1474 3536 +f 3535 1580 3432 1477 +f 1578 3541 1581 3534 +f 1578 3534 1583 3542 +f 3448 1584 3433 1578 +f 3533 1585 3475 1586 +f 3458 1464 3449 1578 +f 3449 1465 3479 1578 +f 1909 1593 3543 1604 +f 3543 1593 3544 1382 +f 3428 1588 3544 1593 +f 3427 1587 3428 1593 +f 1589 3364 1590 3545 +f 3545 1593 3546 1592 +f 1591 3406 1592 3546 +f 3546 1593 3387 1594 +f 3429 1404 3387 1593 +f 1433 3407 1595 3547 +f 3547 1593 3417 1596 +f 3418 1432 3417 1593 +f 1598 3389 1597 3548 +f 1599 3371 1405 3549 +f 3550 1435 3409 1600 +f 1599 3408 1434 3550 +f 3551 1602 3388 1601 +f 865 1909 1604 3552 +f 865 3552 1605 3553 +f 3395 1607 3373 865 +f 3373 1606 3554 865 +f 3554 1384 3368 1418 +f 865 3554 1418 3555 +f 3555 1395 3381 1414 +f 865 3555 1414 3556 +f 3400 1608 3557 865 +f 3557 1609 3398 1429 +f 865 3557 1429 3558 +f 3426 1610 3559 865 +f 3559 1426 3374 1428 +f 865 3559 1428 3560 +f 3560 1427 3382 1396 +f 3397 1611 3421 865 +f 3421 1440 3422 865 +f 3422 1612 3396 865 +f 3396 1441 3423 865 +f 3423 1613 3424 865 +f 3424 1442 3430 865 +f 3430 1446 3379 865 +f 3379 1392 3578 865 +f 3415 1408 3573 1614 +f 3561 1455 3567 1388 +f 3562 1617 3565 1618 +f 3563 1559 3431 1615 +f 1615 3562 1616 3564 +f 3419 1618 3565 1619 +f 3466 1488 3394 1410 +f 3445 1460 3566 1620 +f 1460 3561 1621 3566 +f 1495 3568 1622 3567 +f 1496 3569 1623 3568 +f 3416 1623 3569 1624 +f 1409 3416 1624 3570 +f 3393 1409 3570 1494 +f 3471 1491 3571 1438 +f 3571 1383 3367 1438 +f 3572 1512 3469 1625 +f 3392 1626 3572 1625 +f 1408 3392 1625 3573 +f 3497 1628 3574 1627 +f 3574 1629 3414 1627 +f 1633 3391 1630 3575 +f 1632 3391 1633 3576 +f 3486 1634 3390 1632 +f 3420 1406 3390 1634 +f 3453 1490 3468 1437 +f 3468 1524 3413 1437 +f 1524 3492 1392 3577 +f 3492 1525 3493 1392 +f 3493 1636 3578 1392 +f 3467 1489 3494 864 +f 3494 1637 3495 864 +f 3495 1527 3501 864 +f 3484 1482 3463 864 +f 864 3581 1506 3580 +f 864 3490 1640 3581 +f 3465 1487 3490 864 +f 864 3579 1459 3582 +f 864 3583 1469 3579 +f 864 3485 1646 3583 +f 3578 1636 3467 864 +f 3501 1467 3450 864 +f 3450 1641 3584 864 +f 3482 1509 3491 864 +f 3491 1642 3585 864 +f 3585 1643 3462 1521 +f 864 3585 1521 3586 +f 3586 1522 3483 1510 +f 3463 1644 3587 864 +f 3587 1645 3439 1484 +f 3464 1511 3485 864 +f 1780 3594 1647 3588 +f 1648 3591 1654 3588 +f 3589 1650 3593 1649 +f 3590 1780 3591 1651 +f 3592 1653 3589 1651 +f 1653 3592 1781 3593 +f 3594 1781 3592 1652 +f 3595 1648 3588 1647 +f 3595 1652 3592 1651 +f 1656 3601 1662 3596 +f 1660 3600 1655 3596 +f 1657 3598 1661 3597 +f 1656 3596 1655 3597 +f 1657 3600 1658 3598 +f 1659 3601 1661 3598 +f 1660 3596 1662 3599 +f 1659 3598 1658 3599 +f 1660 3599 1658 3600 +f 1657 3597 1655 3600 +f 1656 3597 1661 3601 +f 1659 3599 1662 3601 +f 1988 1713 2008 1663 +f 2009 144 1989 135 +f 2780 1664 3602 1666 +f 2814 1665 2812 148 +f 2812 856 2841 148 +f 2841 858 2019 148 +f 1667 1787 1668 3603 +f 1669 2720 739 3603 +f 1669 3603 1668 3604 +f 2830 1669 3604 1670 +f 1670 1904 1671 3605 +f 2002 1672 2809 1726 +f 2806 827 2809 1672 +f 2835 1673 2806 1672 +f 2821 1675 3606 1676 +f 1675 2794 1710 3606 +f 1797 1723 3627 1722 +f 1980 128 3607 1684 +f 1677 2790 1684 3607 +f 2791 1677 3607 128 +f 2816 833 2791 128 +f 806 2787 1678 3608 +f 3608 128 3609 853 +f 3609 1694 2803 854 +f 2833 824 2803 1694 +f 2015 1682 2834 1679 +f 2838 1680 2726 1682 +f 2726 1681 2748 1682 +f 2748 819 3610 1682 +f 2017 1683 2837 1698 +f 1700 2797 1699 3611 +f 1995 1684 2796 1700 +f 2819 1686 2796 1684 +f 2793 1687 2819 1684 +f 2746 765 2793 1684 +f 2790 764 2746 1684 +f 2776 796 2767 1688 +f 2767 786 2018 1688 +f 2781 798 2780 1666 +f 2751 1770 3655 1690 +f 2755 776 2751 1690 +f 1785 1667 3603 739 +f 739 2721 775 3612 +f 1692 2014 1693 3614 +f 2014 1694 1981 1693 +f 2012 1695 3613 137 +f 2842 829 2810 1683 +f 2810 1696 2807 1683 +f 2807 1697 2837 1683 +f 1904 15 3615 1671 +f 2827 1671 3615 1699 +f 2799 816 2827 1699 +f 2822 840 2799 1699 +f 2797 839 2822 1699 +f 2011 147 1990 1701 +f 2010 145 2011 1701 +f 1994 1730 3616 130 +f 2789 1703 2815 130 +f 2815 1704 2786 130 +f 2786 825 3617 130 +f 2005 1705 2831 849 +f 2802 822 2831 1705 +f 1705 3617 1706 3618 +f 2773 1767 3652 233 +f 2766 1707 2773 233 +f 785 2766 233 3619 +f 2778 785 3619 143 +f 1708 3620 774 3621 +f 1783 1708 3621 737 +f 2003 1727 2836 1674 +f 2725 1709 2836 1727 +f 1798 1676 3606 1710 +f 14 1798 1710 3622 +f 2817 837 3629 14 +f 1984 130 3617 1705 +f 2006 1712 3623 1711 +f 1987 131 1985 1711 +f 1713 1987 1711 3623 +f 2779 1714 2778 143 +f 2000 1715 2813 797 +f 2811 831 2813 1715 +f 2839 832 2811 1715 +f 3 1783 737 3624 +f 3624 738 3626 1716 +f 2829 1717 3625 1719 +f 1718 2825 844 3625 +f 2826 814 3627 1723 +f 1906 1719 3625 844 +f 1721 2820 1722 3627 +f 2001 1725 2840 855 +f 2808 1724 2840 1725 +f 2004 1728 2724 744 +f 2747 745 2724 1728 +f 2800 767 2747 1728 +f 2832 1729 2800 1728 +f 1733 1799 14 3629 +f 2818 1731 3630 1730 +f 811 2745 809 3630 +f 2788 1732 3616 1730 +f 1993 1733 3629 837 +f 2763 782 2847 13 +f 2761 1734 3639 22 +f 2736 755 2761 22 +f 1735 2765 754 3631 +f 3631 22 3633 1736 +f 747 2728 1736 3633 +f 3633 22 3634 1737 +f 3635 1771 3632 16 +f 3637 1734 2763 13 +f 12 1809 24 3637 +f 3638 1734 3637 24 +f 2723 743 2785 863 +f 2785 1740 2744 863 +f 2744 1741 2732 863 +f 2732 1742 2742 863 +f 2742 803 2783 863 +f 2783 1743 2730 863 +f 2730 1745 3640 863 +f 1745 2771 1744 3640 +f 2771 792 3641 1744 +f 2740 1746 3642 1744 +f 2775 1768 3653 1769 +f 1769 2035 1744 3642 +f 2848 862 2762 1748 +f 2760 1747 2735 1749 +f 2735 783 2764 1749 +f 3643 1750 1794 1749 +f 757 2737 23 3643 +f 2846 232 2770 1753 +f 862 2784 740 3644 +f 2743 1751 2784 862 +f 2731 762 2743 862 +f 2741 758 2731 862 +f 2782 1752 2741 862 +f 2768 1755 2770 232 +f 2739 787 2768 232 +f 2169 1756 2772 1757 +f 2764 757 3643 1749 +f 2737 1758 3646 23 +f 1758 2727 1759 3646 +f 2727 1760 3647 1759 +f 1760 2756 1761 3647 +f 2756 1762 3648 1761 +f 1762 2758 1763 3648 +f 2758 1766 3649 1763 +f 3649 1766 2733 1765 +f 1764 3649 1765 3650 +f 769 3651 9 3650 +f 769 3654 2 3651 +f 1767 2773 793 3652 +f 1756 1999 233 3652 +f 1688 2018 1769 3653 +f 1708 1783 2 3654 +f 3620 1708 3654 769 +f 1770 2751 1771 3655 +f 3656 1772 2430 1773 +f 2430 1774 1874 1773 +f 1777 2429 1776 3657 +f 1854 56 3513 1775 +f 3513 1546 3660 1775 +f 1775 3658 456 3657 +f 3656 59 1864 1776 +f 2429 1772 3656 1776 +f 3658 141 1998 1779 +f 2472 456 3658 1779 +f 1547 1901 1778 3659 +f 1779 2034 156 3659 +f 141 3658 1775 3660 +f 2469 492 2497 493 +f 3590 1649 3593 1781 +f 1782 1 3651 2 +f 1782 83 1921 90 +f 1784 1716 1905 83 +f 1785 739 3612 1690 +f 1786 4 1787 5 +f 1786 6 1793 73 +f 1788 23 3646 1759 +f 1789 20 3638 24 +f 1790 99 1806 1763 +f 1790 1764 1802 7 +f 1791 22 1807 8 +f 1791 17 1803 1739 +f 1792 89 1802 9 +f 1793 16 1803 70 +f 1795 12 3637 13 +f 1795 45 1837 47 +f 1796 1723 1797 94 +f 1797 1722 2821 1676 +f 1800 82 1884 76 +f 1800 1699 3615 15 +f 1804 1761 1806 19 +f 1805 21 1807 1738 +f 1808 97 1930 10 +f 1808 1750 3643 23 +f 1810 26 1812 612 +f 40 1825 37 1811 +f 1811 604 2526 612 +f 475 2424 40 1812 +f 1816 590 2603 30 +f 1817 606 2611 33 +f 1817 32 2496 31 +f 1818 33 2534 607 +f 1819 36 1820 32 +f 1824 43 1829 527 +f 1826 40 1827 39 +f 1826 587 2532 38 +f 1827 43 1828 614 +f 1828 585 2527 603 +f 1538 2204 32 1829 +f 1832 529 2598 602 +f 1834 44 1835 45 +f 863 2843 859 1834 +f 755 2736 753 1835 +f 1837 784 2765 46 +f 859 2844 54 1838 +f 50 2769 791 1838 +f 782 2763 781 1839 +f 1734 2761 44 1840 +f 743 2723 48 1841 +f 1740 2785 805 1842 +f 1741 2744 49 1843 +f 1742 2732 760 1844 +f 803 2742 802 1845 +f 1743 2783 801 1846 +f 1848 1746 2740 51 +f 1768 2775 52 1849 +f 1850 786 2767 795 +f 798 2781 799 1851 +f 1852 54 1853 55 +f 1664 2780 53 1852 +f 1854 1775 3657 1776 +f 1665 2814 55 1855 +f 829 2842 57 1856 +f 1696 2810 828 1857 +f 1858 61 1860 59 +f 1858 255 1870 60 +f 854 2803 823 1860 +f 1691 1982 62 1861 +f 824 2833 850 1862 +f 856 2812 857 1865 +f 1697 2807 58 1866 +f 1680 2838 64 1867 +f 1681 2726 768 1868 +f 819 2748 65 1869 +f 1873 59 3656 1773 +f 1874 1774 2469 493 +f 1875 68 1876 25 +f 1876 777 1877 69 +f 1877 780 1878 21 +f 1878 751 1879 8 +f 1879 750 1880 17 +f 1880 72 1881 70 +f 1882 71 2721 847 +f 1883 847 2720 843 +f 82 1903 4 1883 +f 1886 816 2799 817 +f 1887 840 2822 838 +f 1888 839 2797 813 +f 1889 1686 2819 77 +f 1890 1687 2793 78 +f 1891 765 2746 763 +f 1892 764 2790 810 +f 1893 833 2816 807 +f 939 2967 938 1895 +f 1896 1547 1898 80 +f 932 2928 933 1896 +f 1897 81 1900 945 +f 1899 213 2111 81 +f 865 3578 864 1900 +f 938 1902 1578 1901 +f 1903 1670 3604 1668 +f 1905 1719 1906 93 +f 1906 844 3628 1720 +f 1748 2762 98 1911 +f 1747 2760 85 1912 +f 783 2735 752 1913 +f 757 2764 86 1914 +f 1758 2737 756 1916 +f 1917 99 1918 88 +f 1917 1762 2756 87 +f 1922 93 1923 92 +f 1923 94 1924 841 +f 1924 138 1925 95 +f 1925 129 1926 815 +f 1926 134 1927 812 +f 1927 133 1928 836 +f 1928 139 1929 834 +f 742 1933 101 1932 +f 740 2784 804 1933 +f 1751 2743 761 1934 +f 762 2731 748 1935 +f 758 2741 759 1936 +f 1752 2782 800 1937 +f 1940 1753 2770 790 +f 1941 1755 2768 102 +f 1942 787 2739 103 +f 1943 1757 2772 794 +f 1944 1707 2766 105 +f 1714 2779 104 1945 +f 1946 797 2813 830 +f 1947 831 2811 106 +f 1948 832 2839 108 +f 1949 1724 2808 107 +f 1950 115 1968 107 +f 1950 827 2806 826 +f 1951 109 2745 96 +f 1703 2789 808 1953 +f 1704 2815 110 1954 +f 825 2786 111 1955 +f 126 1958 848 1957 +f 1957 822 2802 821 +f 1959 451 2420 629 +f 1960 1729 2832 848 +f 629 1962 766 1961 +f 1961 767 2800 818 +f 1962 627 2421 113 +f 1963 745 2747 766 +f 113 1965 114 1964 +f 1964 1709 2725 746 +f 1965 452 2422 630 +f 630 1967 826 1966 +f 1966 1673 2835 114 +f 1967 438 2392 115 +f 1969 437 2618 116 +f 1970 441 2619 633 +f 1971 632 2620 118 +f 1972 442 2617 631 +f 1973 121 1975 631 +f 1973 634 2621 119 +f 1974 450 2417 120 +f 1976 123 2849 122 +f 903 2906 84 1977 +f 1548 2845 861 1978 +f 1684 1996 127 1980 +f 1694 3609 128 1981 +f 1982 136 3614 1693 +f 1983 130 1984 129 +f 1984 1705 2006 1711 +f 1988 1663 2009 135 +f 1989 144 2010 1701 +f 1990 136 1991 1701 +f 147 2012 137 1990 +f 1993 837 2818 1730 +f 75 1996 1684 1995 +f 1700 3611 1685 1995 +f 1998 149 2016 142 +f 797 2779 143 2000 +f 2001 1548 2168 1725 +f 855 2839 1715 2001 +f 1726 2808 1725 2002 +f 1674 2835 1672 2003 +f 744 2725 1727 2004 +f 849 2832 1728 2005 +f 2006 231 2007 1712 +f 2007 1534 2199 146 +f 2008 1713 3623 1712 +f 146 2013 1695 2012 +f 2015 1535 2016 1682 +f 2015 1679 2833 1694 +f 2017 1698 2838 1682 +f 2018 786 2781 1666 +f 149 2020 148 2019 +f 2019 858 2842 1683 +f 2020 860 2035 1666 +f 150 3357 151 2022 +f 1345 3351 1346 2023 +f 2024 1321 2025 156 +f 2025 1175 3225 1320 +f 2028 1140 3258 154 +f 2029 1179 3253 155 +f 2032 1323 3210 1326 +f 1324 3261 1325 2033 +f 2034 142 3506 252 +f 1296 3166 1226 2036 +f 1275 3119 158 2037 +f 2040 162 2047 159 +f 2041 1298 3161 1276 +f 2042 1277 3159 1248 +f 163 3320 1278 2044 +f 164 3311 1247 2045 +f 2048 1255 2081 162 +f 2049 1290 3112 1225 +f 2053 1230 3205 168 +f 2054 1228 3180 1237 +f 2057 1227 3178 1261 +f 2059 1115 3203 170 +f 2060 866 2061 171 +f 2060 1090 3176 1260 +f 1300 3326 172 2062 +f 1288 3303 173 2065 +f 1286 3172 1287 2066 +f 2067 1283 3200 1285 +f 2069 1025 3170 174 +f 2070 1027 3115 175 +f 2072 1280 3188 176 +f 2074 1251 3186 1252 +f 2075 1046 3137 177 +f 2076 866 2077 179 +f 1329 2094 202 2078 +f 2078 1094 3184 179 +f 188 3305 189 2080 +f 1103 3193 1282 2082 +f 2083 1282 3324 192 +f 1545 3511 162 2083 +f 1033 3123 184 2086 +f 1306 2103 1545 2087 +f 197 3322 186 2088 +f 2092 199 2104 198 +f 2092 1029 3323 190 +f 1374 2106 190 2093 +f 2093 1028 3139 191 +f 1119 3330 1303 2096 +f 2097 1366 3267 193 +f 2099 1223 3301 1335 +f 2100 1194 3278 195 +f 2103 1333 3276 196 +f 2104 1331 3345 1332 +f 1219 3298 1361 2106 +f 2107 1314 3272 1374 +f 2108 200 2109 203 +f 2110 866 2111 1363 +f 1330 3332 1304 2110 +f 1364 3270 1363 2112 +f 2113 1362 3296 204 +f 2115 1186 3268 206 +f 2116 1371 3211 207 +f 2119 1152 3233 219 +f 2120 210 2134 213 +f 2120 217 3338 1375 +f 2121 216 3337 211 +f 2122 1357 2133 213 +f 2125 213 2126 1326 +f 1547 3659 156 2126 +f 1327 3289 1358 2127 +f 1158 3237 1359 2128 +f 1351 3219 1353 2130 +f 1352 3286 1354 2131 +f 2132 1354 3354 1355 +f 2135 1349 3282 218 +f 2136 1372 3284 209 +f 220 2139 1548 2138 +f 2139 1231 3143 221 +f 1294 3195 1293 2140 +f 2143 1107 3145 1238 +f 2145 1058 3149 1265 +f 2147 230 2167 1263 +f 2150 1055 3307 223 +f 2151 166 2166 223 +f 2151 1269 3127 224 +f 2154 1232 3155 1268 +f 2156 1036 3125 1234 +f 2157 1035 3151 1273 +f 2159 1062 3198 227 +f 2160 1243 3309 1245 +f 228 3319 229 2163 +f 1270 3318 1302 2164 +f 2164 1295 2165 166 +f 1757 2739 232 2169 +f 2171 236 3335 1367 +f 2173 1337 2174 234 +f 2178 1316 3341 242 +f 2179 1339 3223 243 +f 2182 1138 3251 1310 +f 2183 252 2191 1310 +f 2184 1134 3221 1343 +f 2185 1133 3247 1342 +f 2187 1214 3294 1344 +f 2188 1212 3350 247 +f 2189 1318 3342 248 +f 2190 253 2194 252 +f 259 2206 234 2191 +f 2192 1340 2193 252 +f 1146 3342 1318 2193 +f 1143 3333 251 2194 +f 1369 3333 1347 2195 +f 2196 257 2199 254 +f 2200 255 2201 256 +f 2200 1535 3503 257 +f 2202 1537 2203 453 +f 142 3504 258 2204 +f 2205 1543 2206 43 +f 1578 2426 453 2205 +f 308 2264 260 2208 +f 267 2228 262 2210 +f 2212 400 2350 263 +f 2212 279 2240 338 +f 2215 264 2270 353 +f 2217 265 2351 275 +f 2217 324 2243 289 +f 2218 268 2347 398 +f 2218 282 2231 269 +f 2220 270 2352 271 +f 2220 340 2245 288 +f 2221 325 2282 326 +f 2222 369 2315 375 +f 2223 345 2316 330 +f 2224 330 2322 346 +f 2229 399 2348 266 +f 2229 283 2234 335 +f 2230 339 2292 274 +f 685 2625 276 2231 +f 2232 671 2235 339 +f 284 2675 685 2233 +f 285 2652 284 2234 +f 2235 650 2238 277 +f 646 2633 285 2236 +f 684 2673 646 2237 +f 2238 278 2241 273 +f 337 2291 338 2239 +f 286 2651 684 2239 +f 645 2631 286 2240 +f 2241 281 2256 280 +f 291 2671 681 2246 +f 2247 293 2248 727 +f 2248 294 2249 724 +f 2249 341 2250 725 +f 2250 296 2251 295 +f 2251 357 2252 718 +f 2252 325 2253 700 +f 2253 356 2254 297 +f 2254 299 2255 298 +f 2255 319 2277 300 +f 2256 677 2257 331 +f 2257 302 2258 344 +f 2258 643 2259 301 +f 2259 304 2260 303 +f 2260 305 2261 332 +f 2261 667 2262 306 +f 2262 307 2263 261 +f 334 2289 350 2266 +f 312 2300 311 2268 +f 313 2701 711 2271 +f 354 2297 353 2271 +f 711 2687 314 2272 +f 314 2683 315 2273 +f 315 2693 666 2274 +f 323 2285 360 2274 +f 666 2654 316 2275 +f 316 2627 713 2276 +f 2277 321 2278 732 +f 2278 320 2279 735 +f 2279 322 2280 716 +f 394 2344 395 2281 +f 2285 329 2327 328 +f 361 2308 362 2286 +f 2288 373 2299 301 +f 2297 382 2331 342 +f 2298 384 2307 343 +f 390 2339 358 2305 +f 2310 411 2319 363 +f 2310 272 2323 417 +f 2311 374 2383 364 +f 2312 406 2313 365 +f 2312 347 2318 433 +f 2313 432 2320 366 +f 2315 429 2380 368 +f 2317 371 2412 370 +f 2318 372 2319 412 +f 2321 430 2358 404 +f 2322 376 2381 367 +f 2323 333 2324 418 +f 2324 378 2326 377 +f 2325 423 2329 349 +f 2326 379 2333 420 +f 2329 421 2333 348 +f 2334 386 3510 1541 +f 2336 327 2338 387 +f 444 2415 391 2343 +f 2353 1542 3509 386 +f 1533 2427 455 2354 +f 404 2379 428 2357 +f 2365 406 2366 409 +f 2370 418 2371 415 +f 2371 377 2372 419 +f 2372 420 2373 706 +f 2373 421 2374 649 +f 2374 423 2375 422 +f 2375 383 2376 424 +f 2376 426 2377 427 +f 2377 385 2409 425 +f 439 2428 1533 2378 +f 2378 661 2651 435 +f 2379 663 2387 1533 +f 2383 432 2384 430 +f 2391 436 2631 440 +f 2391 437 2392 439 +f 2394 287 2669 659 +f 2397 696 2671 728 +f 2398 727 2710 443 +f 2400 725 2691 702 +f 2401 295 2706 719 +f 2402 444 2414 719 +f 731 2712 447 2406 +f 2409 380 2410 446 +f 2410 381 2411 448 +f 2412 640 2627 449 +f 1573 3514 1599 2418 +f 2418 1593 2622 635 +f 2419 112 2616 254 +f 1578 2425 467 2423 +f 2423 492 2469 1778 +f 1480 3531 1573 2426 +f 2428 1539 3508 454 +f 2430 456 2472 1774 +f 2431 457 2444 1532 +f 2431 32 2497 492 +f 2433 652 2642 459 +f 463 2495 460 2434 +f 2435 461 2653 686 +f 2438 676 2439 501 +f 2439 653 2440 512 +f 2440 679 2474 511 +f 2445 564 2648 457 +f 2446 656 2646 657 +f 2447 565 2670 466 +f 2448 660 2650 680 +f 2449 682 2672 683 +f 2451 730 2711 729 +f 2452 571 2709 726 +f 467 2454 468 2453 +f 2453 572 2692 703 +f 2455 717 2457 505 +f 2455 504 2470 477 +f 2458 609 2517 470 +f 475 2473 505 2459 +f 2460 519 2515 514 +f 520 2519 521 2464 +f 522 2520 473 2466 +f 524 2521 523 2468 +f 517 2508 476 2470 +f 2472 1779 3659 1778 +f 26 2494 478 2473 +f 2474 644 2475 479 +f 2475 669 2476 507 +f 2476 670 2477 508 +f 2477 480 2478 497 +f 2478 481 2479 498 +f 2479 482 2480 483 +f 491 2500 515 2484 +f 2485 554 2698 708 +f 2486 709 2636 487 +f 2487 555 2680 689 +f 715 2586 477 2493 +f 2498 495 2510 494 +f 2498 513 2514 525 +f 2499 621 2610 605 +f 2501 617 2502 497 +f 2501 498 2509 620 +f 2502 622 2511 508 +f 2503 499 2608 500 +f 2506 597 2514 503 +f 2509 483 2510 506 +f 599 2524 528 2516 +f 532 2535 496 2517 +f 557 2528 519 2519 +f 556 2596 522 2521 +f 2525 593 2592 591 +f 714 2586 583 2526 +f 2527 584 2577 577 +f 698 2559 600 2528 +f 2529 570 2573 530 +f 2530 573 2576 584 +f 2531 613 2579 580 +f 2535 598 2595 516 +f 615 2615 624 2537 +f 542 2642 533 2537 +f 2538 673 2541 534 +f 543 2640 542 2539 +f 535 2608 536 2540 +f 675 2666 543 2540 +f 2541 688 2544 619 +f 537 2644 675 2542 +f 538 2668 537 2543 +f 2544 665 2547 540 +f 618 2610 539 2545 +f 546 2630 538 2545 +f 539 2602 545 2546 +f 2547 559 2562 558 +f 623 2589 548 2549 +f 626 2613 625 2551 +f 2553 552 2554 551 +f 2554 599 2555 553 +f 2555 598 2556 554 +f 2556 532 2557 709 +f 2557 610 2558 555 +f 689 2680 690 2559 +f 2560 556 2561 699 +f 2561 575 2583 694 +f 2562 648 2563 560 +f 2563 662 2564 592 +f 2564 563 2565 591 +f 2565 562 2566 561 +f 2566 564 2567 594 +f 2567 656 2568 596 +f 2568 565 2569 595 +f 568 2597 569 2571 +f 723 2705 574 2576 +f 574 2690 693 2577 +f 693 2682 579 2578 +f 613 2587 577 2578 +f 579 2715 733 2579 +f 581 2717 582 2581 +f 604 2600 588 2582 +f 2583 578 2584 704 +f 2584 611 2585 576 +f 589 2612 502 2588 +f 2590 616 2609 534 +f 2606 27 2607 611 +f 2623 416 2677 636 +f 2623 637 2658 414 +f 2624 639 2678 485 +f 2624 484 2659 638 +f 2635 422 2679 318 +f 2635 317 2697 649 +f 2638 547 2657 481 +f 2638 480 2662 544 +f 2645 440 2647 654 +f 2649 659 2669 290 +f 2656 668 2658 307 +f 2657 549 2659 482 +f 2660 541 2662 670 +f 2677 415 2699 309 +f 2678 551 2700 486 +f 2686 729 2711 567 +f 2695 706 2697 705 +f 2695 310 2699 419 +f 2696 707 2700 553 +f 2708 443 2710 724 +f 2713 469 2717 581 +f 2718 846 2719 737 +f 2718 772 2754 773 +f 2723 863 2847 782 +f 2734 771 2753 750 +f 2734 751 2759 779 +f 2736 22 3631 754 +f 2738 1735 3631 1736 +f 1744 3641 788 2740 +f 2750 91 2754 774 +f 2750 770 3620 769 +f 2755 1690 3612 775 +f 2757 1737 2759 780 +f 1749 2848 1748 2760 +f 862 3644 741 2762 +f 788 3641 792 2769 +f 1756 3652 793 2772 +f 2775 1769 3642 1746 +f 2776 1768 2777 796 +f 1688 3653 1689 2776 +f 2782 862 3645 1754 +f 2787 853 2805 60 +f 2788 1730 3630 809 +f 130 3616 1732 2789 +f 2792 811 3630 1731 +f 2798 1721 3627 814 +f 1705 3618 852 2802 +f 852 3618 1706 2804 +f 2814 148 3602 1664 +f 2816 128 3608 1678 +f 2817 14 3622 835 +f 2823 1718 3625 1717 +f 2824 845 2828 74 +f 2826 1723 3628 844 +f 2827 79 2828 1671 +f 2829 1719 3626 738 +f 1670 3605 842 2830 +f 2834 1682 3610 820 +f 2845 862 2848 101 +f 2846 1753 3645 862 +f 2850 1117 2877 124 +f 1231 3112 1021 2852 +f 1294 3143 868 2853 +f 1107 3195 1108 2854 +f 1058 3145 1059 2855 +f 1057 3149 869 2856 +f 2858 1232 3127 870 +f 2859 1036 3155 871 +f 2860 1035 3125 1068 +f 2861 1062 3151 872 +f 2862 117 2863 873 +f 2862 1243 3198 1063 +f 906 2910 874 2863 +f 2864 1244 3153 873 +f 874 2890 890 2865 +f 1070 3157 1075 2865 +f 1073 3162 875 2866 +f 1279 3129 1039 2867 +f 1250 3159 876 2868 +f 1277 3161 877 2869 +f 2871 1298 3119 1031 +f 2872 1275 3166 879 +f 2873 1296 3131 881 +f 2874 1042 3121 880 +f 2875 1271 3133 882 +f 2876 1079 3168 1065 +f 1230 3180 883 2877 +f 1228 3178 884 2878 +f 1227 3203 885 2879 +f 1115 3176 1116 2880 +f 2882 1090 3202 1114 +f 2883 1085 3174 1045 +f 2884 1259 3135 887 +f 2885 1288 3172 1112 +f 2886 1286 3200 888 +f 2887 1283 3170 889 +f 2888 891 2893 889 +f 2888 1025 3115 1026 +f 2889 1027 3188 894 +f 904 2907 891 2890 +f 1024 3165 890 2891 +f 1103 3114 1023 2892 +f 2894 1280 3186 893 +f 2895 1251 3137 895 +f 2896 1046 3184 1093 +f 2897 1094 3139 896 +f 1101 3190 898 2899 +f 1281 3123 899 2900 +f 1033 3141 1052 2901 +f 1051 3193 892 2902 +f 953 2909 900 2903 +f 2905 901 2911 121 +f 947 2988 902 2906 +f 2907 952 2981 946 +f 959 2975 906 2911 +f 907 2939 927 2912 +f 942 2968 940 2912 +f 1119 3267 907 2913 +f 1118 3208 1161 2914 +f 1308 3239 1160 2915 +f 1209 3291 1210 2916 +f 1336 3241 908 2917 +f 1338 3245 909 2918 +f 2920 1138 3223 911 +f 2921 1134 3251 1171 +f 2922 1133 3221 912 +f 2923 1214 3247 1132 +f 2924 1212 3294 1213 +f 2925 940 2926 914 +f 2925 1341 3249 913 +f 2927 1146 3264 914 +f 1324 3253 916 2928 +f 1179 3258 917 2929 +f 1140 3225 1139 2930 +f 1175 3255 918 2931 +f 1174 3257 1177 2932 +f 2934 1370 3215 920 +f 2935 1181 3262 1142 +f 2936 1143 3227 921 +f 2937 1127 3217 922 +f 2938 1145 3229 915 +f 1366 3301 923 2939 +f 1223 3278 924 2940 +f 1194 3276 1192 2941 +f 1333 3299 925 2942 +f 1222 3274 926 2943 +f 2945 1331 3298 1218 +f 2946 1219 3272 1190 +f 2947 1314 3231 1148 +f 2948 1330 3270 1187 +f 2949 1364 3296 929 +f 2950 1362 3268 930 +f 2951 1186 3211 931 +f 2952 80 2959 931 +f 2952 1371 3284 1201 +f 2953 1372 3282 1200 +f 1323 3261 932 2954 +f 1327 3210 1120 2955 +f 1158 3289 1207 2956 +f 1351 3237 934 2957 +f 1352 3219 1130 2958 +f 995 2972 927 2959 +f 2960 1349 3233 1197 +f 2961 1152 3280 1198 +f 2962 1313 3235 1156 +f 1205 3286 1204 2964 +f 2966 993 2973 945 +f 2968 1000 3053 941 +f 943 2971 944 2970 +f 987 3057 936 2971 +f 992 3042 995 2973 +f 2975 956 2984 958 +f 2976 951 2979 950 +f 2977 958 2984 957 +f 949 3014 971 2978 +f 2980 955 2982 954 +f 2986 974 3041 960 +f 955 2989 961 2986 +f 971 3003 963 2989 +f 1393 3111 961 2990 +f 1394 3182 965 2991 +f 1412 3206 964 2992 +f 1389 3181 1092 2993 +f 1417 3179 966 2994 +f 1091 3204 968 2995 +f 2997 1088 3175 1086 +f 2998 1084 3136 1089 +f 2999 1113 3173 969 +f 3000 1439 3201 970 +f 3001 1082 3171 1081 +f 3002 1447 3116 1083 +f 1099 3187 1098 3003 +f 3005 1407 3138 1047 +f 3006 1048 3185 1095 +f 3007 1049 3140 1096 +f 3008 1030 3118 1050 +f 1425 3192 1102 3010 +f 1034 3191 1100 3011 +f 1411 3124 973 3012 +f 1105 3142 1106 3013 +f 1436 3194 1104 3014 +f 1387 3144 975 3015 +f 1444 3196 1053 3016 +f 1109 3146 976 3017 +f 1430 3150 1061 3018 +f 1445 3197 1060 3019 +f 3021 1110 3148 1056 +f 3022 1431 3128 978 +f 3023 1069 3156 979 +f 3024 1423 3126 1037 +f 3025 1038 3152 980 +f 3026 1064 3199 1067 +f 3027 1111 3154 1066 +f 3028 1066 3029 981 +f 3028 950 3041 974 +f 3029 1402 3169 1080 +f 3030 1044 3134 1043 +f 3031 1377 3122 982 +f 3032 1032 3132 983 +f 3033 1078 3167 1076 +f 1400 3160 984 3035 +f 1072 3130 1040 3036 +f 1419 3163 1041 3037 +f 1397 3158 1074 3038 +f 1071 3164 986 3039 +f 1378 3113 949 3040 +f 3043 998 3052 990 +f 3044 991 3047 988 +f 3045 990 3052 989 +f 1012 3083 1006 3046 +f 3048 1020 3050 994 +f 3055 1015 3110 997 +f 1020 3058 1184 3055 +f 1006 3072 1004 3058 +f 1470 3207 1184 3059 +f 1471 3266 1185 3060 +f 1195 3302 1224 3061 +f 1196 3279 1193 3062 +f 1492 3277 1001 3063 +f 1493 3300 1002 3064 +f 3066 1149 3273 1003 +f 3067 1150 3232 1151 +f 3068 1523 3271 1188 +f 3069 1217 3297 1189 +f 3070 1526 3269 1005 +f 3071 1122 3212 1121 +f 1203 3283 1123 3072 +f 3074 1154 3234 1153 +f 3075 1199 3281 1155 +f 3076 1157 3236 1007 +f 3077 1206 3214 1125 +f 1483 3288 1010 3079 +f 1468 3287 1009 3080 +f 1485 3220 1131 3081 +f 1159 3238 1011 3082 +f 1451 3290 1208 3083 +f 1529 3240 1162 3084 +f 1163 3292 1013 3085 +f 1515 3242 1165 3086 +f 1164 3246 1169 3087 +f 1168 3293 1211 3088 +f 3090 1503 3244 1166 +f 3091 1517 3224 1172 +f 3092 1481 3252 1137 +f 3093 1135 3222 1136 +f 3094 1504 3248 1216 +f 3095 1215 3295 1014 +f 3096 1466 3250 1170 +f 3097 1170 3098 1018 +f 3097 988 3110 1015 +f 3098 1183 3265 1147 +f 3099 1501 3230 1016 +f 3100 1128 3218 1129 +f 3101 1457 3228 1144 +f 3102 1479 3263 1182 +f 1499 3256 1176 3104 +f 1476 3226 1141 3105 +f 1498 3259 1017 3106 +f 1180 3254 1173 3107 +f 1473 3260 1019 3108 +f 1450 3209 1012 3109 +f 3116 1442 3424 1082 +f 1408 3415 1049 3118 +f 1077 3386 1420 3120 +f 1434 3408 1038 3126 +f 3132 1390 3377 1078 +f 3134 1552 3361 1377 +f 1257 3303 1288 3135 +f 1437 3413 1097 3138 +f 3142 1410 3394 1411 +f 3144 1588 3428 1444 +f 1432 3418 1431 3148 +f 3150 1594 3387 1445 +f 3153 1242 3309 1243 +f 1405 3371 1423 3156 +f 3157 165 3321 1073 +f 1558 3383 1397 3163 +f 1550 3405 1044 3169 +f 1441 3396 1439 3171 +f 3173 1611 3397 1084 +f 3175 1610 3426 1087 +f 1607 3395 1412 3181 +f 3183 1290 3325 1292 +f 1630 3391 1407 3185 +f 3189 1446 3430 1447 +f 3191 1623 3416 1425 +f 3192 1409 3393 1030 +f 3194 1618 3419 1105 +f 3196 1587 3427 1109 +f 3197 1404 3429 1110 +f 3199 1422 3410 1111 +f 3201 1440 3421 1113 +f 3208 235 3330 1119 +f 3212 1527 3495 1526 +f 1124 3337 1356 3213 +f 1482 3484 1157 3214 +f 1126 3457 1478 3216 +f 3218 1584 3448 1457 +f 1519 3478 1504 3222 +f 3230 1575 3433 1128 +f 1329 3332 1330 3231 +f 1509 3482 1507 3234 +f 3238 1487 3465 1485 +f 3240 1454 3499 1163 +f 1518 3488 1517 3244 +f 3246 1513 3459 1168 +f 3249 1312 3350 1212 +f 3250 1465 3449 1183 +f 3255 152 3343 1174 +f 3257 1319 3353 1370 +f 1497 3454 1180 3259 +f 1489 3467 1217 3269 +f 3271 1524 3468 1150 +f 3273 1628 3497 1191 +f 1488 3466 1195 3279 +f 3280 210 3358 1313 +f 3285 1467 3501 1122 +f 3287 1646 3485 1483 +f 3288 1511 3464 1206 +f 3290 1640 3490 1159 +f 3292 1528 3498 1515 +f 3293 1530 3500 1503 +f 3295 1505 3479 1466 +f 3297 1525 3492 1523 +f 3299 185 3355 1222 +f 3305 187 3313 1029 +f 1603 3552 1604 3359 +f 1565 3402 1420 3360 +f 3362 1413 3553 1605 +f 1381 3521 1399 3363 +f 3365 1398 3523 1380 +f 3366 1376 3543 1382 +f 1626 3392 1030 3367 +f 1606 3373 1389 3368 +f 3369 1573 3517 1385 +f 3370 1032 3376 1386 +f 1599 3550 1600 3371 +f 3372 1620 3566 1621 +f 3375 1557 3525 1561 +f 1567 3401 1419 3375 +f 1553 3514 1391 3376 +f 3379 1424 3412 1392 +f 1388 3567 1622 3380 +f 3384 1568 3526 1563 +f 1494 3471 1438 3393 +f 1488 3445 1620 3394 +f 865 3553 1413 3395 +f 865 3560 1396 3397 +f 1608 3400 1415 3398 +f 865 3556 1416 3400 +f 3401 1528 3499 1558 +f 1569 3500 1564 3402 +f 3403 1573 3518 1401 +f 1596 3417 1110 3407 +f 1599 3551 1601 3408 +f 3410 1599 3515 1549 +f 3411 1379 3564 1616 +f 1524 3577 1635 3413 +f 3415 1614 3497 1627 +f 3418 1593 3548 1597 +f 1619 3466 1410 3419 +f 1634 3453 1437 3420 +f 865 3558 1443 3426 +f 3427 1593 3545 1590 +f 3429 1593 3547 1595 +f 1448 3473 1478 3432 +f 3434 1619 3565 1617 +f 3436 1514 3524 1562 +f 3438 1566 3522 1560 +f 1644 3463 1206 3439 +f 1460 3445 1196 3440 +f 3440 1495 3567 1455 +f 1456 3534 1581 3441 +f 3442 1463 3542 1583 +f 1520 3489 1135 3443 +f 3444 1486 3582 1459 +f 1631 3576 1633 3446 +f 1462 3472 1498 3447 +f 1578 3542 1463 3448 +f 3450 1202 3481 1641 +f 1638 3579 1469 3451 +f 3452 1624 3569 1496 +f 1578 3538 1577 3454 +f 3455 1475 3536 1474 +f 1582 3541 1500 3457 +f 1578 3533 1586 3458 +f 1642 3491 1154 3462 +f 864 3587 1484 3464 +f 864 3582 1486 3465 +f 1491 3471 1221 3469 +f 1578 3536 1579 3473 +f 3474 1578 3535 1477 +f 1452 3520 1502 3476 +f 1570 3487 1503 3477 +f 3478 1573 3531 1574 +f 3480 1639 3580 1506 +f 864 3584 1508 3482 +f 3484 864 3586 1510 +f 1632 3576 1631 3486 +f 3488 1573 3529 1516 +f 3505 1540 3508 166 +f 3512 141 3660 1546 +f 3518 1555 3519 1556 +f 3539 864 3580 1449 +f 3548 1599 3549 1598 +f 3563 1615 3564 1379 +f 3571 1512 3572 1383 +f 3574 1633 3575 1629 +f 3588 1654 3591 1780 +f 3589 1649 3590 1651 +f 1653 3593 1650 3589 +f 3590 1781 3594 1780 +f 3591 1648 3595 1651 +f 1652 3595 1647 3594 +f 3613 1692 3614 136 +f 3632 771 3636 1739 +f 3634 1739 3636 779 +f 3635 5 3655 1771 +f 3638 1738 3639 1734 +# 3672 faces, 0 coords texture + +# End of File diff --git a/tests/example/example_tests_main.cpp b/tests/example/example_tests_main.cpp index 32e8d02b76..426d1ffef7 100644 --- a/tests/example/example_tests_main.cpp +++ b/tests/example/example_tests_main.cpp @@ -1,4 +1,3 @@ -#define CATCH_CONFIG_MAIN #include TEST_CASE("Is example succesful", "[example]") { diff --git a/tests/libslic3r/CMakeLists.txt b/tests/libslic3r/CMakeLists.txt index e34a40f346..adcb2722d8 100644 --- a/tests/libslic3r/CMakeLists.txt +++ b/tests/libslic3r/CMakeLists.txt @@ -1,4 +1,5 @@ get_filename_component(_TEST_NAME ${CMAKE_CURRENT_LIST_DIR} NAME) + add_executable(${_TEST_NAME}_tests ${_TEST_NAME}_tests.cpp test_3mf.cpp @@ -10,7 +11,13 @@ add_executable(${_TEST_NAME}_tests test_placeholder_parser.cpp test_polygon.cpp test_stl.cpp + test_meshsimplify.cpp ) + +if (TARGET OpenVDB::openvdb) + target_sources(${_TEST_NAME}_tests PRIVATE test_hollowing.cpp) +endif() + target_link_libraries(${_TEST_NAME}_tests test_common libslic3r) set_property(TARGET ${_TEST_NAME}_tests PROPERTY FOLDER "tests") diff --git a/tests/libslic3r/libslic3r_tests.cpp b/tests/libslic3r/libslic3r_tests.cpp index caf5b3b9ac..b9c615d90a 100644 --- a/tests/libslic3r/libslic3r_tests.cpp +++ b/tests/libslic3r/libslic3r_tests.cpp @@ -11,4 +11,31 @@ TEST_CASE("sort_remove_duplicates", "[utils]") { REQUIRE(data_src == data_dst); } +TEST_CASE("string_printf", "[utils]") { + SECTION("Empty format with empty data should return empty string") { + std::string outs = Slic3r::string_printf(""); + REQUIRE(outs.empty()); + } + + SECTION("String output length should be the same as input") { + std::string outs = Slic3r::string_printf("1234"); + REQUIRE(outs.size() == 4); + } + + SECTION("String format should be interpreted as with sprintf") { + std::string outs = Slic3r::string_printf("%d %f %s", 10, 11.4, " This is a string"); + char buffer[1024]; + + sprintf(buffer, "%d %f %s", 10, 11.4, " This is a string"); + + REQUIRE(outs.compare(buffer) == 0); + } + + SECTION("String format should survive large input data") { + std::string input(2048, 'A'); + std::string outs = Slic3r::string_printf("%s", input.c_str()); + REQUIRE(outs.compare(input) == 0); + } +} + } diff --git a/tests/libslic3r/test_hollowing.cpp b/tests/libslic3r/test_hollowing.cpp new file mode 100644 index 0000000000..65b87c2a23 --- /dev/null +++ b/tests/libslic3r/test_hollowing.cpp @@ -0,0 +1,46 @@ +#include +#include +#include + +#include +#include "libslic3r/SLA/Hollowing.hpp" +#include +#include "libslic3r/Format/OBJ.hpp" + +#include + +#include + +#if defined(WIN32) || defined(_WIN32) +#define PATH_SEPARATOR R"(\)" +#else +#define PATH_SEPARATOR R"(/)" +#endif + +static Slic3r::TriangleMesh load_model(const std::string &obj_filename) +{ + Slic3r::TriangleMesh mesh; + auto fpath = TEST_DATA_DIR PATH_SEPARATOR + obj_filename; + Slic3r::load_obj(fpath.c_str(), &mesh); + return mesh; +} + + +TEST_CASE("Negative 3D offset should produce smaller object.", "[Hollowing]") +{ + Slic3r::TriangleMesh in_mesh = load_model("20mm_cube.obj"); + Benchmark bench; + bench.start(); + + std::unique_ptr out_mesh_ptr = + Slic3r::sla::generate_interior(in_mesh); + + bench.stop(); + + std::cout << "Elapsed processing time: " << bench.getElapsedSec() << std::endl; + + if (out_mesh_ptr) in_mesh.merge(*out_mesh_ptr); + in_mesh.require_shared_vertices(); + in_mesh.WriteOBJFile("merged_out.obj"); +} + diff --git a/tests/libslic3r/test_meshsimplify.cpp b/tests/libslic3r/test_meshsimplify.cpp new file mode 100644 index 0000000000..d21c3a8924 --- /dev/null +++ b/tests/libslic3r/test_meshsimplify.cpp @@ -0,0 +1,11 @@ +#include +#include + +//#include + +//TEST_CASE("Mesh simplification", "[mesh_simplify]") { +// Simplify::load_obj(TEST_DATA_DIR PATH_SEPARATOR "zaba.obj"); +// Simplify::simplify_mesh_lossless(); +// Simplify::write_obj("zaba_simplified.obj"); +//} + diff --git a/tests/sla_print/CMakeLists.txt b/tests/sla_print/CMakeLists.txt index e8921ba486..9d47f3ae4d 100644 --- a/tests/sla_print/CMakeLists.txt +++ b/tests/sla_print/CMakeLists.txt @@ -1,5 +1,8 @@ get_filename_component(_TEST_NAME ${CMAKE_CURRENT_LIST_DIR} NAME) -add_executable(${_TEST_NAME}_tests ${_TEST_NAME}_tests.cpp) +add_executable(${_TEST_NAME}_tests ${_TEST_NAME}_tests_main.cpp + sla_print_tests.cpp + sla_test_utils.hpp sla_test_utils.cpp + sla_raycast_tests.cpp) target_link_libraries(${_TEST_NAME}_tests test_common libslic3r) set_property(TARGET ${_TEST_NAME}_tests PROPERTY FOLDER "tests") diff --git a/tests/sla_print/sla_print_tests.cpp b/tests/sla_print/sla_print_tests.cpp index 229eb42676..f34fb90968 100644 --- a/tests/sla_print/sla_print_tests.cpp +++ b/tests/sla_print/sla_print_tests.cpp @@ -1,376 +1,17 @@ -#include - #include #include #include -// Debug -#include +#include "sla_test_utils.hpp" -#include "libslic3r/libslic3r.h" -#include "libslic3r/Format/OBJ.hpp" -#include "libslic3r/SLAPrint.hpp" -#include "libslic3r/TriangleMesh.hpp" -#include "libslic3r/SLA/SLAPad.hpp" -#include "libslic3r/SLA/SLASupportTreeBuilder.hpp" -#include "libslic3r/SLA/SLASupportTreeBuildsteps.hpp" -#include "libslic3r/SLA/SLAAutoSupports.hpp" -#include "libslic3r/SLA/SLARaster.hpp" -#include "libslic3r/SLA/ConcaveHull.hpp" -#include "libslic3r/MTUtils.hpp" +namespace { -#include "libslic3r/SVG.hpp" -#include "libslic3r/Format/OBJ.hpp" - -#if defined(WIN32) || defined(_WIN32) -#define PATH_SEPARATOR R"(\)" -#else -#define PATH_SEPARATOR R"(/)" -#endif - -namespace { -using namespace Slic3r; - -TriangleMesh load_model(const std::string &obj_filename) -{ - TriangleMesh mesh; - auto fpath = TEST_DATA_DIR PATH_SEPARATOR + obj_filename; - load_obj(fpath.c_str(), &mesh); - return mesh; -} - -enum e_validity { - ASSUME_NO_EMPTY = 1, - ASSUME_MANIFOLD = 2, - ASSUME_NO_REPAIR = 4 -}; - -void check_validity(const TriangleMesh &input_mesh, - int flags = ASSUME_NO_EMPTY | ASSUME_MANIFOLD | - ASSUME_NO_REPAIR) -{ - TriangleMesh mesh{input_mesh}; - - if (flags & ASSUME_NO_EMPTY) { - REQUIRE_FALSE(mesh.empty()); - } else if (mesh.empty()) - return; // If it can be empty and it is, there is nothing left to do. - - REQUIRE(stl_validate(&mesh.stl)); - - bool do_update_shared_vertices = false; - mesh.repair(do_update_shared_vertices); - - if (flags & ASSUME_NO_REPAIR) { - REQUIRE_FALSE(mesh.needed_repair()); - } - - if (flags & ASSUME_MANIFOLD) { - mesh.require_shared_vertices(); - if (!mesh.is_manifold()) mesh.WriteOBJFile("non_manifold.obj"); - REQUIRE(mesh.is_manifold()); - } -} - -struct PadByproducts -{ - ExPolygons model_contours; - ExPolygons support_contours; - TriangleMesh mesh; -}; - -void _test_concave_hull(const Polygons &hull, const ExPolygons &polys) -{ - REQUIRE(polys.size() >=hull.size()); - - double polys_area = 0; - for (const ExPolygon &p : polys) polys_area += p.area(); - - double cchull_area = 0; - for (const Slic3r::Polygon &p : hull) cchull_area += p.area(); - - REQUIRE(cchull_area >= Approx(polys_area)); - - size_t cchull_holes = 0; - for (const Slic3r::Polygon &p : hull) - cchull_holes += p.is_clockwise() ? 1 : 0; - - REQUIRE(cchull_holes == 0); - - Polygons intr = diff(to_polygons(polys), hull); - REQUIRE(intr.empty()); -} - -void test_concave_hull(const ExPolygons &polys) { - sla::PadConfig pcfg; - - Slic3r::sla::ConcaveHull cchull{polys, pcfg.max_merge_dist_mm, []{}}; - - _test_concave_hull(cchull.polygons(), polys); - - coord_t delta = scaled(pcfg.brim_size_mm + pcfg.wing_distance()); - ExPolygons wafflex = sla::offset_waffle_style_ex(cchull, delta); - Polygons waffl = sla::offset_waffle_style(cchull, delta); - - _test_concave_hull(to_polygons(wafflex), polys); - _test_concave_hull(waffl, polys); -} - -void test_pad(const std::string & obj_filename, - const sla::PadConfig &padcfg, - PadByproducts & out) -{ - REQUIRE(padcfg.validate().empty()); - - TriangleMesh mesh = load_model(obj_filename); - - REQUIRE_FALSE(mesh.empty()); - - // Create pad skeleton only from the model - Slic3r::sla::pad_blueprint(mesh, out.model_contours); - - test_concave_hull(out.model_contours); - - REQUIRE_FALSE(out.model_contours.empty()); - - // Create the pad geometry for the model contours only - Slic3r::sla::create_pad({}, out.model_contours, out.mesh, padcfg); - - check_validity(out.mesh); - - auto bb = out.mesh.bounding_box(); - REQUIRE(bb.max.z() - bb.min.z() == Approx(padcfg.full_height())); -} - -void test_pad(const std::string & obj_filename, - const sla::PadConfig &padcfg = {}) -{ - PadByproducts byproducts; - test_pad(obj_filename, padcfg, byproducts); -} - -struct SupportByproducts -{ - std::string obj_fname; - std::vector slicegrid; - std::vector model_slices; - sla::SupportTreeBuilder supporttree; - TriangleMesh input_mesh; -}; - -const constexpr float CLOSING_RADIUS = 0.005f; - -void check_support_tree_integrity(const sla::SupportTreeBuilder &stree, - const sla::SupportConfig &cfg) -{ - double gnd = stree.ground_level; - double H1 = cfg.max_solo_pillar_height_mm; - double H2 = cfg.max_dual_pillar_height_mm; - - for (const sla::Head &head : stree.heads()) { - REQUIRE((!head.is_valid() || head.pillar_id != sla::ID_UNSET || - head.bridge_id != sla::ID_UNSET)); - } - - for (const sla::Pillar &pillar : stree.pillars()) { - if (std::abs(pillar.endpoint().z() - gnd) < EPSILON) { - double h = pillar.height; - - if (h > H1) REQUIRE(pillar.links >= 1); - else if(h > H2) { REQUIRE(pillar.links >= 2); } - } - - REQUIRE(pillar.links <= cfg.pillar_cascade_neighbors); - REQUIRE(pillar.bridges <= cfg.max_bridges_on_pillar); - } - - double max_bridgelen = 0.; - auto chck_bridge = [&cfg](const sla::Bridge &bridge, double &max_brlen) { - Vec3d n = bridge.endp - bridge.startp; - double d = sla::distance(n); - max_brlen = std::max(d, max_brlen); - - double z = n.z(); - double polar = std::acos(z / d); - double slope = -polar + PI / 2.; - REQUIRE(std::abs(slope) >= cfg.bridge_slope - EPSILON); - }; - - for (auto &bridge : stree.bridges()) chck_bridge(bridge, max_bridgelen); - REQUIRE(max_bridgelen <= cfg.max_bridge_length_mm); - - max_bridgelen = 0; - for (auto &bridge : stree.crossbridges()) chck_bridge(bridge, max_bridgelen); - - double md = cfg.max_pillar_link_distance_mm / std::cos(-cfg.bridge_slope); - REQUIRE(max_bridgelen <= md); -} - -void test_supports(const std::string & obj_filename, - const sla::SupportConfig &supportcfg, - SupportByproducts & out) -{ - using namespace Slic3r; - TriangleMesh mesh = load_model(obj_filename); - - REQUIRE_FALSE(mesh.empty()); - - TriangleMeshSlicer slicer{&mesh}; - - auto bb = mesh.bounding_box(); - double zmin = bb.min.z(); - double zmax = bb.max.z(); - double gnd = zmin - supportcfg.object_elevation_mm; - auto layer_h = 0.05f; - - out.slicegrid = grid(float(gnd), float(zmax), layer_h); - slicer.slice(out.slicegrid , CLOSING_RADIUS, &out.model_slices, []{}); - - // Create the special index-triangle mesh with spatial indexing which - // is the input of the support point and support mesh generators - sla::EigenMesh3D emesh{mesh}; - - // Create the support point generator - sla::SLAAutoSupports::Config autogencfg; - autogencfg.head_diameter = float(2 * supportcfg.head_front_radius_mm); - sla::SLAAutoSupports point_gen{emesh, out.model_slices, out.slicegrid, - autogencfg, [] {}, [](int) {}}; - - // Get the calculated support points. - std::vector support_points = point_gen.output(); - - int validityflags = ASSUME_NO_REPAIR; - - // If there is no elevation, support points shall be removed from the - // bottom of the object. - if (std::abs(supportcfg.object_elevation_mm) < EPSILON) { - sla::remove_bottom_points(support_points, zmin, - supportcfg.base_height_mm); - } else { - // Should be support points at least on the bottom of the model - REQUIRE_FALSE(support_points.empty()); - - // Also the support mesh should not be empty. - validityflags |= ASSUME_NO_EMPTY; - } - - // Generate the actual support tree - sla::SupportTreeBuilder treebuilder; - treebuilder.build(sla::SupportableMesh{emesh, support_points, supportcfg}); - - check_support_tree_integrity(treebuilder, supportcfg); - - const TriangleMesh &output_mesh = treebuilder.retrieve_mesh(); - - check_validity(output_mesh, validityflags); - - // Quick check if the dimensions and placement of supports are correct - auto obb = output_mesh.bounding_box(); - - double allowed_zmin = zmin - supportcfg.object_elevation_mm; - - if (std::abs(supportcfg.object_elevation_mm) < EPSILON) - allowed_zmin = zmin - 2 * supportcfg.head_back_radius_mm; - - REQUIRE(obb.min.z() >= allowed_zmin); - REQUIRE(obb.max.z() <= zmax); - - // Move out the support tree into the byproducts, we can examine it further - // in various tests. - out.obj_fname = std::move(obj_filename); - out.supporttree = std::move(treebuilder); - out.input_mesh = std::move(mesh); -} - -void test_supports(const std::string & obj_filename, - const sla::SupportConfig &supportcfg = {}) -{ - SupportByproducts byproducts; - test_supports(obj_filename, supportcfg, byproducts); -} - -void export_failed_case(const std::vector &support_slices, - const SupportByproducts &byproducts) -{ - for (size_t n = 0; n < support_slices.size(); ++n) { - const ExPolygons &sup_slice = support_slices[n]; - const ExPolygons &mod_slice = byproducts.model_slices[n]; - Polygons intersections = intersection(sup_slice, mod_slice); - - std::stringstream ss; - if (!intersections.empty()) { - ss << byproducts.obj_fname << std::setprecision(4) << n << ".svg"; - SVG svg(ss.str()); - svg.draw(sup_slice, "green"); - svg.draw(mod_slice, "blue"); - svg.draw(intersections, "red"); - svg.Close(); - } - } - - TriangleMesh m; - byproducts.supporttree.retrieve_full_mesh(m); - m.merge(byproducts.input_mesh); - m.repair(); - m.require_shared_vertices(); - m.WriteOBJFile(byproducts.obj_fname.c_str()); -} - -void test_support_model_collision( - const std::string & obj_filename, - const sla::SupportConfig &input_supportcfg = {}) -{ - SupportByproducts byproducts; - - sla::SupportConfig supportcfg = input_supportcfg; - - // Set head penetration to a small negative value which should ensure that - // the supports will not touch the model body. - supportcfg.head_penetration_mm = -0.15; - - // TODO: currently, the tailheads penetrating into the model body do not - // respect the penetration parameter properly. No issues were reported so - // far but we should definitely fix this. - supportcfg.ground_facing_only = true; - - test_supports(obj_filename, supportcfg, byproducts); - - // Slice the support mesh given the slice grid of the model. - std::vector support_slices = - byproducts.supporttree.slice(byproducts.slicegrid, CLOSING_RADIUS); - - // The slices originate from the same slice grid so the numbers must match - - bool support_mesh_is_empty = - byproducts.supporttree.retrieve_mesh(sla::MeshType::Pad).empty() && - byproducts.supporttree.retrieve_mesh(sla::MeshType::Support).empty(); - - if (support_mesh_is_empty) - REQUIRE(support_slices.empty()); - else - REQUIRE(support_slices.size() == byproducts.model_slices.size()); - - bool notouch = true; - for (size_t n = 0; notouch && n < support_slices.size(); ++n) { - const ExPolygons &sup_slice = support_slices[n]; - const ExPolygons &mod_slice = byproducts.model_slices[n]; - - Polygons intersections = intersection(sup_slice, mod_slice); - - notouch = notouch && intersections.empty(); - } - - if (!notouch) export_failed_case(support_slices, byproducts); - - REQUIRE(notouch); -} - -const char * const BELOW_PAD_TEST_OBJECTS[] = { +const char *const BELOW_PAD_TEST_OBJECTS[] = { "20mm_cube.obj", "V.obj", }; -const char * const AROUND_PAD_TEST_OBJECTS[] = { +const char *const AROUND_PAD_TEST_OBJECTS[] = { "20mm_cube.obj", "V.obj", "frog_legs.obj", @@ -392,46 +33,46 @@ template void test_pairhash() I A[nums] = {0}, B[nums] = {0}; std::unordered_set CH; std::unordered_map> ints; - + std::random_device rd; std::mt19937 gen(rd()); - + const I Ibits = int(sizeof(I) * CHAR_BIT); const II IIbits = int(sizeof(II) * CHAR_BIT); - + int bits = IIbits / 2 < Ibits ? Ibits / 2 : Ibits; if (std::is_signed::value) bits -= 1; const I Imin = 0; const I Imax = I(std::pow(2., bits) - 1); - + std::uniform_int_distribution dis(Imin, Imax); - + for (size_t i = 0; i < nums;) { I a = dis(gen); if (CH.find(a) == CH.end()) { CH.insert(a); A[i] = a; ++i; } } - + for (size_t i = 0; i < nums;) { I b = dis(gen); if (CH.find(b) == CH.end()) { CH.insert(b); B[i] = b; ++i; } } - + for (size_t i = 0; i < nums; ++i) { I a = A[i], b = B[i]; - + REQUIRE(a != b); - + II hash_ab = sla::pairhash(a, b); II hash_ba = sla::pairhash(b, a); REQUIRE(hash_ab == hash_ba); - + auto it = ints.find(hash_ab); - + if (it != ints.end()) { REQUIRE(( (it->second.first == a && it->second.second == b) || (it->second.first == b && it->second.second == a) - )); + )); } else ints[hash_ab] = std::make_pair(a, b); } @@ -444,74 +85,123 @@ TEST_CASE("Pillar pairhash should be unique", "[SLASupportGeneration]") { test_pairhash(); } +TEST_CASE("Support point generator should be deterministic if seeded", + "[SLASupportGeneration], [SLAPointGen]") { + TriangleMesh mesh = load_model("A_upsidedown.obj"); + + sla::EigenMesh3D emesh{mesh}; + + sla::SupportConfig supportcfg; + sla::SupportPointGenerator::Config autogencfg; + autogencfg.head_diameter = float(2 * supportcfg.head_front_radius_mm); + sla::SupportPointGenerator point_gen{emesh, autogencfg, [] {}, [](int) {}}; + + TriangleMeshSlicer slicer{&mesh}; + + auto bb = mesh.bounding_box(); + double zmin = bb.min.z(); + double zmax = bb.max.z(); + double gnd = zmin - supportcfg.object_elevation_mm; + auto layer_h = 0.05f; + + auto slicegrid = grid(float(gnd), float(zmax), layer_h); + std::vector slices; + slicer.slice(slicegrid, CLOSING_RADIUS, &slices, []{}); + + point_gen.seed(0); + point_gen.execute(slices, slicegrid); + + auto get_chksum = [](const std::vector &pts){ + long long chksum = 0; + for (auto &pt : pts) { + auto p = scaled(pt.pos); + chksum += p.x() + p.y() + p.z(); + } + + return chksum; + }; + + long long checksum = get_chksum(point_gen.output()); + size_t ptnum = point_gen.output().size(); + REQUIRE(point_gen.output().size() > 0); + + for (int i = 0; i < 20; ++i) { + point_gen.output().clear(); + point_gen.seed(0); + point_gen.execute(slices, slicegrid); + REQUIRE(point_gen.output().size() == ptnum); + REQUIRE(checksum == get_chksum(point_gen.output())); + } +} + TEST_CASE("Flat pad geometry is valid", "[SLASupportGeneration]") { sla::PadConfig padcfg; - + // Disable wings padcfg.wall_height_mm = .0; - + for (auto &fname : BELOW_PAD_TEST_OBJECTS) test_pad(fname, padcfg); } TEST_CASE("WingedPadGeometryIsValid", "[SLASupportGeneration]") { sla::PadConfig padcfg; - + // Add some wings to the pad to test the cavity padcfg.wall_height_mm = 1.; - + for (auto &fname : BELOW_PAD_TEST_OBJECTS) test_pad(fname, padcfg); } TEST_CASE("FlatPadAroundObjectIsValid", "[SLASupportGeneration]") { sla::PadConfig padcfg; - + // Add some wings to the pad to test the cavity padcfg.wall_height_mm = 0.; // padcfg.embed_object.stick_stride_mm = 0.; padcfg.embed_object.enabled = true; padcfg.embed_object.everywhere = true; - + for (auto &fname : AROUND_PAD_TEST_OBJECTS) test_pad(fname, padcfg); } TEST_CASE("WingedPadAroundObjectIsValid", "[SLASupportGeneration]") { sla::PadConfig padcfg; - + // Add some wings to the pad to test the cavity padcfg.wall_height_mm = 1.; padcfg.embed_object.enabled = true; padcfg.embed_object.everywhere = true; - + for (auto &fname : AROUND_PAD_TEST_OBJECTS) test_pad(fname, padcfg); } TEST_CASE("ElevatedSupportGeometryIsValid", "[SLASupportGeneration]") { sla::SupportConfig supportcfg; supportcfg.object_elevation_mm = 5.; - + for (auto fname : SUPPORT_TEST_MODELS) test_supports(fname); } TEST_CASE("FloorSupportGeometryIsValid", "[SLASupportGeneration]") { sla::SupportConfig supportcfg; supportcfg.object_elevation_mm = 0; - + for (auto &fname: SUPPORT_TEST_MODELS) test_supports(fname, supportcfg); } TEST_CASE("ElevatedSupportsDoNotPierceModel", "[SLASupportGeneration]") { - + sla::SupportConfig supportcfg; - + for (auto fname : SUPPORT_TEST_MODELS) test_support_model_collision(fname, supportcfg); } TEST_CASE("FloorSupportsDoNotPierceModel", "[SLASupportGeneration]") { - + sla::SupportConfig supportcfg; supportcfg.object_elevation_mm = 0; - + for (auto fname : SUPPORT_TEST_MODELS) test_support_model_collision(fname, supportcfg); } @@ -525,7 +215,7 @@ TEST_CASE("InitializedRasterShouldBeNONEmpty", "[SLARasterOutput]") { // Default Prusa SL1 display parameters sla::Raster::Resolution res{2560, 1440}; sla::Raster::PixelDim pixdim{120. / res.width_px, 68. / res.height_px}; - + sla::Raster raster; raster.reset(res, pixdim); REQUIRE_FALSE(raster.empty()); @@ -547,54 +237,54 @@ static void check_raster_transformations(sla::Raster::Orientation o, double disp_w = 120., disp_h = 68.; sla::Raster::Resolution res{2560, 1440}; sla::Raster::PixelDim pixdim{disp_w / res.width_px, disp_h / res.height_px}; - + auto bb = BoundingBox({0, 0}, {scaled(disp_w), scaled(disp_h)}); sla::Raster::Trafo trafo{o, mirroring}; trafo.origin_x = bb.center().x(); trafo.origin_y = bb.center().y(); - + sla::Raster raster{res, pixdim, trafo}; - + // create box of size 32x32 pixels (not 1x1 to avoid antialiasing errors) coord_t pw = 32 * coord_t(std::ceil(scaled(pixdim.w_mm))); coord_t ph = 32 * coord_t(std::ceil(scaled(pixdim.h_mm))); ExPolygon box; box.contour.points = {{-pw, -ph}, {pw, -ph}, {pw, ph}, {-pw, ph}}; - + double tr_x = scaled(20.), tr_y = tr_x; - + box.translate(tr_x, tr_y); ExPolygon expected_box = box; - + // Now calculate the position of the translated box according to output // trafo. if (o == sla::Raster::Orientation::roPortrait) expected_box.rotate(PI / 2.); - + if (mirroring[X]) for (auto &p : expected_box.contour.points) p.x() = -p.x(); - + if (mirroring[Y]) for (auto &p : expected_box.contour.points) p.y() = -p.y(); - + raster.draw(box); - + Point expected_coords = expected_box.contour.bounding_box().center(); double rx = unscaled(expected_coords.x() + bb.center().x()) / pixdim.w_mm; double ry = unscaled(expected_coords.y() + bb.center().y()) / pixdim.h_mm; auto w = size_t(std::floor(rx)); auto h = res.height_px - size_t(std::floor(ry)); - + REQUIRE((w < res.width_px && h < res.height_px)); - + auto px = raster.read_pixel(w, h); - + if (px != FullWhite) { sla::PNGImage img; std::fstream outf("out.png", std::ios::out); - + outf << img.serialize(raster); } - + REQUIRE(px == FullWhite); } @@ -603,7 +293,7 @@ TEST_CASE("MirroringShouldBeCorrect", "[SLARasterOutput]") { sla::Raster::MirrorX, sla::Raster::MirrorY, sla::Raster::MirrorXY}; - + sla::Raster::Orientation orientations[] = {sla::Raster::roLandscape, sla::Raster::roPortrait}; for (auto orientation : orientations) @@ -615,7 +305,7 @@ static ExPolygon square_with_hole(double v) { ExPolygon poly; coord_t V = scaled(v / 2.); - + poly.contour.points = {{-V, -V}, {V, -V}, {V, V}, {-V, V}}; poly.holes.emplace_back(); V = V / 2; @@ -631,16 +321,16 @@ static double pixel_area(TPixel px, const sla::Raster::PixelDim &pxdim) static double raster_white_area(const sla::Raster &raster) { if (raster.empty()) return std::nan(""); - + auto res = raster.resolution(); double a = 0; - + for (size_t x = 0; x < res.width_px; ++x) for (size_t y = 0; y < res.height_px; ++y) { auto px = raster.read_pixel(x, y); a += pixel_area(px, raster.pixel_dimensions()); } - + return a; } @@ -648,15 +338,15 @@ static double predict_error(const ExPolygon &p, const sla::Raster::PixelDim &pd) { auto lines = p.lines(); double pix_err = pixel_area(FullWhite, pd) / 2.; - + // Worst case is when a line is parallel to the shorter axis of one pixel, // when the line will be composed of the max number of pixels double pix_l = std::min(pd.h_mm, pd.w_mm); - + double error = 0.; for (auto &l : lines) error += (unscaled(l.length()) / pix_l) * pix_err; - + return error; } @@ -664,28 +354,42 @@ TEST_CASE("RasterizedPolygonAreaShouldMatch", "[SLARasterOutput]") { double disp_w = 120., disp_h = 68.; sla::Raster::Resolution res{2560, 1440}; sla::Raster::PixelDim pixdim{disp_w / res.width_px, disp_h / res.height_px}; - + sla::Raster raster{res, pixdim}; auto bb = BoundingBox({0, 0}, {scaled(disp_w), scaled(disp_h)}); - + ExPolygon poly = square_with_hole(10.); poly.translate(bb.center().x(), bb.center().y()); raster.draw(poly); - + double a = poly.area() / (scaled(1.) * scaled(1.)); double ra = raster_white_area(raster); double diff = std::abs(a - ra); - + REQUIRE(diff <= predict_error(poly, pixdim)); - + raster.clear(); poly = square_with_hole(60.); poly.translate(bb.center().x(), bb.center().y()); raster.draw(poly); - + a = poly.area() / (scaled(1.) * scaled(1.)); ra = raster_white_area(raster); diff = std::abs(a - ra); - + REQUIRE(diff <= predict_error(poly, pixdim)); } + +TEST_CASE("Triangle mesh conversions should be correct", "[SLAConversions]") +{ + sla::Contour3D cntr; + + { + std::fstream infile{"extruder_idler_quads.obj", std::ios::in}; + cntr.from_obj(infile); + } + + + + +} diff --git a/tests/sla_print/sla_print_tests_main.cpp b/tests/sla_print/sla_print_tests_main.cpp new file mode 100644 index 0000000000..b2aa80259d --- /dev/null +++ b/tests/sla_print/sla_print_tests_main.cpp @@ -0,0 +1 @@ +#include diff --git a/tests/sla_print/sla_raycast_tests.cpp b/tests/sla_print/sla_raycast_tests.cpp new file mode 100644 index 0000000000..74c7994723 --- /dev/null +++ b/tests/sla_print/sla_raycast_tests.cpp @@ -0,0 +1,96 @@ +#include +#include + +#include +#include + +#include "sla_test_utils.hpp" + +using namespace Slic3r; + +// First do a simple test of the hole raycaster. +TEST_CASE("Raycaster - find intersections of a line and cylinder") +{ + sla::DrainHole hole{Vec3f(0,0,0), Vec3f(0,0,1), 5, 10}; + std::array, 2> out; + Vec3f s; + Vec3f dir; + + // Start inside the hole and cast perpendicular to its axis. + s = {-1.f, 0, 5.f}; + dir = {1.f, 0, 0}; + hole.get_intersections(s, dir, out); + REQUIRE(out[0].first == Approx(-4.f)); + REQUIRE(out[1].first == Approx(6.f)); + + // Start outside and cast parallel to axis. + s = {0, 0, -1.f}; + dir = {0, 0, 1.f}; + hole.get_intersections(s, dir, out); + REQUIRE(std::abs(out[0].first - 1.f) < 0.001f); + REQUIRE(std::abs(out[1].first - 11.f) < 0.001f); + + // Start outside and cast so that entry is in base and exit on the cylinder + s = {0, -1.f, -1.f}; + dir = {0, 1.f, 1.f}; + dir.normalize(); + hole.get_intersections(s, dir, out); + REQUIRE(std::abs(out[0].first - std::sqrt(2.f)) < 0.001f); + REQUIRE(std::abs(out[1].first - std::sqrt(72.f)) < 0.001f); +} + + +// Create a simple scene with a 20mm cube and a big hole in the front wall +// with 5mm radius. Then shoot rays from interesting positions and see where +// they land. +TEST_CASE("Raycaster with loaded drillholes", "[sla_raycast]") +{ + // Load the cube and make it hollow. + TriangleMesh cube = load_model("20mm_cube.obj"); + sla::HollowingConfig hcfg; + std::unique_ptr cube_inside = sla::generate_interior(cube, hcfg); + REQUIRE(cube_inside); + + // Helper bb + auto boxbb = cube.bounding_box(); + + // Create the big 10mm long drainhole in the front wall. + Vec3f center = boxbb.center().cast(); + Vec3f p = {center.x(), 0., center.z()}; + Vec3f normal = {0.f, 1.f, 0.f}; + float radius = 5.f; + float hole_length = 10.; + sla::DrainHoles holes = { sla::DrainHole{p, normal, radius, hole_length} }; + + cube.merge(*cube_inside); + cube.require_shared_vertices(); + + sla::EigenMesh3D emesh{cube}; + emesh.load_holes(holes); + + Vec3d s = center.cast(); + // Fire from center, should hit the interior wall + auto hit = emesh.query_ray_hit(s, {0, 1., 0.}); + REQUIRE(hit.distance() == Approx(boxbb.size().x() / 2 - hcfg.min_thickness)); + + // Fire upward from hole center, hit distance equals the radius (hits the + // side of the hole cut. + s.y() = hcfg.min_thickness / 2; + hit = emesh.query_ray_hit(s, {0, 0., 1.}); + REQUIRE(hit.distance() == Approx(radius)); + + // Fire from outside, hit the back side of the cube interior + s.y() = -1.; + hit = emesh.query_ray_hit(s, {0, 1., 0.}); + REQUIRE(hit.distance() == Approx(boxbb.max.y() - hcfg.min_thickness - s.y())); + + // Fire downwards from above the hole cylinder. Has to go through the cyl. + // as it was not there. + s = center.cast(); + s.z() = boxbb.max.z() - hcfg.min_thickness - 1.; + hit = emesh.query_ray_hit(s, {0, 0., -1.}); + REQUIRE(hit.distance() == Approx(s.z() - boxbb.min.z() - hcfg.min_thickness)); + + // Check for support tree correctness + test_support_model_collision("20mm_cube.obj", {}, hcfg, holes); +} diff --git a/tests/sla_print/sla_test_utils.cpp b/tests/sla_print/sla_test_utils.cpp new file mode 100644 index 0000000000..0804adb4f3 --- /dev/null +++ b/tests/sla_print/sla_test_utils.cpp @@ -0,0 +1,294 @@ +#include "sla_test_utils.hpp" + +void test_support_model_collision(const std::string &obj_filename, + const sla::SupportConfig &input_supportcfg, + const sla::HollowingConfig &hollowingcfg, + const sla::DrainHoles &drainholes) +{ + SupportByproducts byproducts; + + sla::SupportConfig supportcfg = input_supportcfg; + + // Set head penetration to a small negative value which should ensure that + // the supports will not touch the model body. + supportcfg.head_penetration_mm = -0.15; + + test_supports(obj_filename, supportcfg, hollowingcfg, drainholes, byproducts); + + // Slice the support mesh given the slice grid of the model. + std::vector support_slices = + byproducts.supporttree.slice(byproducts.slicegrid, CLOSING_RADIUS); + + // The slices originate from the same slice grid so the numbers must match + + bool support_mesh_is_empty = + byproducts.supporttree.retrieve_mesh(sla::MeshType::Pad).empty() && + byproducts.supporttree.retrieve_mesh(sla::MeshType::Support).empty(); + + if (support_mesh_is_empty) + REQUIRE(support_slices.empty()); + else + REQUIRE(support_slices.size() == byproducts.model_slices.size()); + + bool notouch = true; + for (size_t n = 0; notouch && n < support_slices.size(); ++n) { + const ExPolygons &sup_slice = support_slices[n]; + const ExPolygons &mod_slice = byproducts.model_slices[n]; + + Polygons intersections = intersection(sup_slice, mod_slice); + + notouch = notouch && intersections.empty(); + } + + /*if (!notouch) */export_failed_case(support_slices, byproducts); + + REQUIRE(notouch); +} + +void export_failed_case(const std::vector &support_slices, const SupportByproducts &byproducts) +{ + for (size_t n = 0; n < support_slices.size(); ++n) { + const ExPolygons &sup_slice = support_slices[n]; + const ExPolygons &mod_slice = byproducts.model_slices[n]; + Polygons intersections = intersection(sup_slice, mod_slice); + + std::stringstream ss; + if (!intersections.empty()) { + ss << byproducts.obj_fname << std::setprecision(4) << n << ".svg"; + SVG svg(ss.str()); + svg.draw(sup_slice, "green"); + svg.draw(mod_slice, "blue"); + svg.draw(intersections, "red"); + svg.Close(); + } + } + + TriangleMesh m; + byproducts.supporttree.retrieve_full_mesh(m); + m.merge(byproducts.input_mesh); + m.repair(); + m.require_shared_vertices(); + m.WriteOBJFile(byproducts.obj_fname.c_str()); +} + +void test_supports(const std::string &obj_filename, + const sla::SupportConfig &supportcfg, + const sla::HollowingConfig &hollowingcfg, + const sla::DrainHoles &drainholes, + SupportByproducts &out) +{ + using namespace Slic3r; + TriangleMesh mesh = load_model(obj_filename); + + REQUIRE_FALSE(mesh.empty()); + + if (hollowingcfg.enabled) { + auto inside = sla::generate_interior(mesh, hollowingcfg); + REQUIRE(inside); + mesh.merge(*inside); + mesh.require_shared_vertices(); + } + + TriangleMeshSlicer slicer{&mesh}; + + auto bb = mesh.bounding_box(); + double zmin = bb.min.z(); + double zmax = bb.max.z(); + double gnd = zmin - supportcfg.object_elevation_mm; + auto layer_h = 0.05f; + + out.slicegrid = grid(float(gnd), float(zmax), layer_h); + slicer.slice(out.slicegrid , CLOSING_RADIUS, &out.model_slices, []{}); + sla::cut_drainholes(out.model_slices, out.slicegrid, CLOSING_RADIUS, drainholes, []{}); + + // Create the special index-triangle mesh with spatial indexing which + // is the input of the support point and support mesh generators + sla::EigenMesh3D emesh{mesh}; + if (hollowingcfg.enabled) + emesh.load_holes(drainholes); + + // Create the support point generator + sla::SupportPointGenerator::Config autogencfg; + autogencfg.head_diameter = float(2 * supportcfg.head_front_radius_mm); + sla::SupportPointGenerator point_gen{emesh, autogencfg, [] {}, [](int) {}}; + + point_gen.seed(0); // Make the test repeatable + point_gen.execute(out.model_slices, out.slicegrid); + + // Get the calculated support points. + std::vector support_points = point_gen.output(); + + int validityflags = ASSUME_NO_REPAIR; + + // If there is no elevation, support points shall be removed from the + // bottom of the object. + if (std::abs(supportcfg.object_elevation_mm) < EPSILON) { + sla::remove_bottom_points(support_points, zmin, + supportcfg.base_height_mm); + } else { + // Should be support points at least on the bottom of the model + REQUIRE_FALSE(support_points.empty()); + + // Also the support mesh should not be empty. + validityflags |= ASSUME_NO_EMPTY; + } + + // Generate the actual support tree + sla::SupportTreeBuilder treebuilder; + treebuilder.build(sla::SupportableMesh{emesh, support_points, supportcfg}); + + check_support_tree_integrity(treebuilder, supportcfg); + + const TriangleMesh &output_mesh = treebuilder.retrieve_mesh(); + + check_validity(output_mesh, validityflags); + + // Quick check if the dimensions and placement of supports are correct + auto obb = output_mesh.bounding_box(); + + double allowed_zmin = zmin - supportcfg.object_elevation_mm; + + if (std::abs(supportcfg.object_elevation_mm) < EPSILON) + allowed_zmin = zmin - 2 * supportcfg.head_back_radius_mm; + + REQUIRE(obb.min.z() >= allowed_zmin); + REQUIRE(obb.max.z() <= zmax); + + // Move out the support tree into the byproducts, we can examine it further + // in various tests. + out.obj_fname = std::move(obj_filename); + out.supporttree = std::move(treebuilder); + out.input_mesh = std::move(mesh); +} + +void check_support_tree_integrity(const sla::SupportTreeBuilder &stree, + const sla::SupportConfig &cfg) +{ + double gnd = stree.ground_level; + double H1 = cfg.max_solo_pillar_height_mm; + double H2 = cfg.max_dual_pillar_height_mm; + + for (const sla::Head &head : stree.heads()) { + REQUIRE((!head.is_valid() || head.pillar_id != sla::ID_UNSET || + head.bridge_id != sla::ID_UNSET)); + } + + for (const sla::Pillar &pillar : stree.pillars()) { + if (std::abs(pillar.endpoint().z() - gnd) < EPSILON) { + double h = pillar.height; + + if (h > H1) REQUIRE(pillar.links >= 1); + else if(h > H2) { REQUIRE(pillar.links >= 2); } + } + + REQUIRE(pillar.links <= cfg.pillar_cascade_neighbors); + REQUIRE(pillar.bridges <= cfg.max_bridges_on_pillar); + } + + double max_bridgelen = 0.; + auto chck_bridge = [&cfg](const sla::Bridge &bridge, double &max_brlen) { + Vec3d n = bridge.endp - bridge.startp; + double d = sla::distance(n); + max_brlen = std::max(d, max_brlen); + + double z = n.z(); + double polar = std::acos(z / d); + double slope = -polar + PI / 2.; + REQUIRE(std::abs(slope) >= cfg.bridge_slope - EPSILON); + }; + + for (auto &bridge : stree.bridges()) chck_bridge(bridge, max_bridgelen); + REQUIRE(max_bridgelen <= cfg.max_bridge_length_mm); + + max_bridgelen = 0; + for (auto &bridge : stree.crossbridges()) chck_bridge(bridge, max_bridgelen); + + double md = cfg.max_pillar_link_distance_mm / std::cos(-cfg.bridge_slope); + REQUIRE(max_bridgelen <= md); +} + +void test_pad(const std::string &obj_filename, const sla::PadConfig &padcfg, PadByproducts &out) +{ + REQUIRE(padcfg.validate().empty()); + + TriangleMesh mesh = load_model(obj_filename); + + REQUIRE_FALSE(mesh.empty()); + + // Create pad skeleton only from the model + Slic3r::sla::pad_blueprint(mesh, out.model_contours); + + test_concave_hull(out.model_contours); + + REQUIRE_FALSE(out.model_contours.empty()); + + // Create the pad geometry for the model contours only + Slic3r::sla::create_pad({}, out.model_contours, out.mesh, padcfg); + + check_validity(out.mesh); + + auto bb = out.mesh.bounding_box(); + REQUIRE(bb.max.z() - bb.min.z() == Approx(padcfg.full_height())); +} + +static void _test_concave_hull(const Polygons &hull, const ExPolygons &polys) +{ + REQUIRE(polys.size() >=hull.size()); + + double polys_area = 0; + for (const ExPolygon &p : polys) polys_area += p.area(); + + double cchull_area = 0; + for (const Slic3r::Polygon &p : hull) cchull_area += p.area(); + + REQUIRE(cchull_area >= Approx(polys_area)); + + size_t cchull_holes = 0; + for (const Slic3r::Polygon &p : hull) + cchull_holes += p.is_clockwise() ? 1 : 0; + + REQUIRE(cchull_holes == 0); + + Polygons intr = diff(to_polygons(polys), hull); + REQUIRE(intr.empty()); +} + +void test_concave_hull(const ExPolygons &polys) { + sla::PadConfig pcfg; + + Slic3r::sla::ConcaveHull cchull{polys, pcfg.max_merge_dist_mm, []{}}; + + _test_concave_hull(cchull.polygons(), polys); + + coord_t delta = scaled(pcfg.brim_size_mm + pcfg.wing_distance()); + ExPolygons wafflex = sla::offset_waffle_style_ex(cchull, delta); + Polygons waffl = sla::offset_waffle_style(cchull, delta); + + _test_concave_hull(to_polygons(wafflex), polys); + _test_concave_hull(waffl, polys); +} + +void check_validity(const TriangleMesh &input_mesh, int flags) +{ + TriangleMesh mesh{input_mesh}; + + if (flags & ASSUME_NO_EMPTY) { + REQUIRE_FALSE(mesh.empty()); + } else if (mesh.empty()) + return; // If it can be empty and it is, there is nothing left to do. + + REQUIRE(stl_validate(&mesh.stl)); + + bool do_update_shared_vertices = false; + mesh.repair(do_update_shared_vertices); + + if (flags & ASSUME_NO_REPAIR) { + REQUIRE_FALSE(mesh.needed_repair()); + } + + if (flags & ASSUME_MANIFOLD) { + mesh.require_shared_vertices(); + if (!mesh.is_manifold()) mesh.WriteOBJFile("non_manifold.obj"); + REQUIRE(mesh.is_manifold()); + } +} diff --git a/tests/sla_print/sla_test_utils.hpp b/tests/sla_print/sla_test_utils.hpp new file mode 100644 index 0000000000..dcb4934ef6 --- /dev/null +++ b/tests/sla_print/sla_test_utils.hpp @@ -0,0 +1,112 @@ +#ifndef SLA_TEST_UTILS_HPP +#define SLA_TEST_UTILS_HPP + +#include +#include + +// Debug +#include + +#include "libslic3r/libslic3r.h" +#include "libslic3r/Format/OBJ.hpp" +#include "libslic3r/SLAPrint.hpp" +#include "libslic3r/TriangleMesh.hpp" +#include "libslic3r/SLA/Pad.hpp" +#include "libslic3r/SLA/SupportTreeBuilder.hpp" +#include "libslic3r/SLA/SupportTreeBuildsteps.hpp" +#include "libslic3r/SLA/SupportPointGenerator.hpp" +#include "libslic3r/SLA/Raster.hpp" +#include "libslic3r/SLA/ConcaveHull.hpp" +#include "libslic3r/MTUtils.hpp" + +#include "libslic3r/SVG.hpp" +#include "libslic3r/Format/OBJ.hpp" + +using namespace Slic3r; + +enum e_validity { + ASSUME_NO_EMPTY = 1, + ASSUME_MANIFOLD = 2, + ASSUME_NO_REPAIR = 4 +}; + +void check_validity(const TriangleMesh &input_mesh, + int flags = ASSUME_NO_EMPTY | ASSUME_MANIFOLD | + ASSUME_NO_REPAIR); + +struct PadByproducts +{ + ExPolygons model_contours; + ExPolygons support_contours; + TriangleMesh mesh; +}; + +void test_concave_hull(const ExPolygons &polys); + +void test_pad(const std::string & obj_filename, + const sla::PadConfig &padcfg, + PadByproducts & out); + +inline void test_pad(const std::string & obj_filename, + const sla::PadConfig &padcfg = {}) +{ + PadByproducts byproducts; + test_pad(obj_filename, padcfg, byproducts); +} + +struct SupportByproducts +{ + std::string obj_fname; + std::vector slicegrid; + std::vector model_slices; + sla::SupportTreeBuilder supporttree; + TriangleMesh input_mesh; +}; + +const constexpr float CLOSING_RADIUS = 0.005f; + +void check_support_tree_integrity(const sla::SupportTreeBuilder &stree, + const sla::SupportConfig &cfg); + +void test_supports(const std::string &obj_filename, + const sla::SupportConfig &supportcfg, + const sla::HollowingConfig &hollowingcfg, + const sla::DrainHoles &drainholes, + SupportByproducts &out); + +inline void test_supports(const std::string &obj_filename, + const sla::SupportConfig &supportcfg, + SupportByproducts &out) +{ + sla::HollowingConfig hcfg; + hcfg.enabled = false; + test_supports(obj_filename, supportcfg, hcfg, {}, out); +} + +inline void test_supports(const std::string &obj_filename, + const sla::SupportConfig &supportcfg = {}) +{ + SupportByproducts byproducts; + test_supports(obj_filename, supportcfg, byproducts); +} + +void export_failed_case(const std::vector &support_slices, + const SupportByproducts &byproducts); + + +void test_support_model_collision( + const std::string &obj_filename, + const sla::SupportConfig &input_supportcfg, + const sla::HollowingConfig &hollowingcfg, + const sla::DrainHoles &drainholes); + +inline void test_support_model_collision( + const std::string &obj_filename, + const sla::SupportConfig &input_supportcfg = {}) +{ + sla::HollowingConfig hcfg; + hcfg.enabled = false; + test_support_model_collision(obj_filename, input_supportcfg, hcfg, {}); +} + +#endif // SLA_TEST_UTILS_HPP diff --git a/tests/test_utils.hpp b/tests/test_utils.hpp new file mode 100644 index 0000000000..b129cc79f1 --- /dev/null +++ b/tests/test_utils.hpp @@ -0,0 +1,21 @@ +#ifndef SLIC3R_TEST_UTILS +#define SLIC3R_TEST_UTILS + +#include +#include + +#if defined(WIN32) || defined(_WIN32) +#define PATH_SEPARATOR R"(\)" +#else +#define PATH_SEPARATOR R"(/)" +#endif + +inline Slic3r::TriangleMesh load_model(const std::string &obj_filename) +{ + Slic3r::TriangleMesh mesh; + auto fpath = TEST_DATA_DIR PATH_SEPARATOR + obj_filename; + Slic3r::load_obj(fpath.c_str(), &mesh); + return mesh; +} + +#endif // SLIC3R_TEST_UTILS