diff --git a/.gitignore b/.gitignore index c4df3f3f8..704289a22 100644 --- a/.gitignore +++ b/.gitignore @@ -18,4 +18,4 @@ local-lib build-linux/* deps/build-linux/* **/.DS_Store -/.idea/ +**/.idea/ diff --git a/CMakeLists.txt b/CMakeLists.txt index 002cd3456..ac058c39c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -458,9 +458,8 @@ if (NOT EIGEN3_FOUND) endif () include_directories(BEFORE SYSTEM ${EIGEN3_INCLUDE_DIR}) -# Find expat or use bundled version -# Always use the system libexpat on Linux. - +# Find expat. We have our overriden FindEXPAT which exports libexpat target +# no matter what. find_package(EXPAT REQUIRED) add_library(libexpat INTERFACE) diff --git a/cmake/modules/FindCURL.cmake b/cmake/modules/FindCURL.cmake index e0deafa45..ced591134 100644 --- a/cmake/modules/FindCURL.cmake +++ b/cmake/modules/FindCURL.cmake @@ -30,82 +30,101 @@ # ``CURL_VERSION_STRING`` # The version of curl found. -# Look for the header file. -find_path(CURL_INCLUDE_DIR NAMES curl/curl.h) -mark_as_advanced(CURL_INCLUDE_DIR) +# First, prefer config scripts +set(_q "") +if(CURL_FIND_QUIETLY) + set(_q QUIET) +endif() +find_package(CURL ${CURL_FIND_VERSION} CONFIG ${_q}) -if(NOT CURL_LIBRARY) - # Look for the library (sorted from most current/relevant entry to least). - find_library(CURL_LIBRARY_RELEASE NAMES - curl - # Windows MSVC prebuilts: - curllib - libcurl_imp - curllib_static - # Windows older "Win32 - MSVC" prebuilts (libcurl.lib, e.g. libcurl-7.15.5-win32-msvc.zip): - libcurl - # Static library on Windows - libcurl_a - ) - mark_as_advanced(CURL_LIBRARY_RELEASE) - - find_library(CURL_LIBRARY_DEBUG NAMES - # Windows MSVC CMake builds in debug configuration on vcpkg: - libcurl-d_imp - libcurl-d - # Static library on Windows, compiled in debug mode - libcurl_a_debug - ) - mark_as_advanced(CURL_LIBRARY_DEBUG) - - include(${CMAKE_CURRENT_LIST_DIR}/SelectLibraryConfigurations_SLIC3R.cmake) - select_library_configurations_SLIC3R(CURL) +if(NOT CURL_FIND_QUIETLY) + if (NOT CURL_FOUND) + message(STATUS "Falling back to MODULE search for CURL...") + else() + message(STATUS "CURL found in ${CURL_DIR}") + endif() endif() -if(CURL_INCLUDE_DIR) - foreach(_curl_version_header curlver.h curl.h) - if(EXISTS "${CURL_INCLUDE_DIR}/curl/${_curl_version_header}") - file(STRINGS "${CURL_INCLUDE_DIR}/curl/${_curl_version_header}" curl_version_str REGEX "^#define[\t ]+LIBCURL_VERSION[\t ]+\".*\"") +if (NOT CURL_FOUND) - string(REGEX REPLACE "^#define[\t ]+LIBCURL_VERSION[\t ]+\"([^\"]*)\".*" "\\1" CURL_VERSION_STRING "${curl_version_str}") - unset(curl_version_str) - break() - endif() - endforeach() -endif() + # Look for the header file. + find_path(CURL_INCLUDE_DIR NAMES curl/curl.h) + mark_as_advanced(CURL_INCLUDE_DIR) -include(${CMAKE_CURRENT_LIST_DIR}/FindPackageHandleStandardArgs_SLIC3R.cmake) -FIND_PACKAGE_HANDLE_STANDARD_ARGS_SLIC3R(CURL - REQUIRED_VARS CURL_LIBRARY CURL_INCLUDE_DIR - VERSION_VAR CURL_VERSION_STRING) + if(NOT CURL_LIBRARY) + # Look for the library (sorted from most current/relevant entry to least). + find_library(CURL_LIBRARY_RELEASE NAMES + curl + # Windows MSVC prebuilts: + curllib + libcurl_imp + curllib_static + # Windows older "Win32 - MSVC" prebuilts (libcurl.lib, e.g. libcurl-7.15.5-win32-msvc.zip): + libcurl + # Static library on Windows + libcurl_a + ) + mark_as_advanced(CURL_LIBRARY_RELEASE) -if(CURL_FOUND) - set(CURL_LIBRARIES ${CURL_LIBRARY}) - set(CURL_INCLUDE_DIRS ${CURL_INCLUDE_DIR}) + find_library(CURL_LIBRARY_DEBUG NAMES + # Windows MSVC CMake builds in debug configuration on vcpkg: + libcurl-d_imp + libcurl-d + # Static library on Windows, compiled in debug mode + libcurl_a_debug + ) + mark_as_advanced(CURL_LIBRARY_DEBUG) - if(NOT TARGET CURL::libcurl) - add_library(CURL::libcurl UNKNOWN IMPORTED) - set_target_properties(CURL::libcurl PROPERTIES - INTERFACE_INCLUDE_DIRECTORIES "${CURL_INCLUDE_DIRS}") + include(${CMAKE_CURRENT_LIST_DIR}/SelectLibraryConfigurations_SLIC3R.cmake) + select_library_configurations_SLIC3R(CURL) + endif() - if(EXISTS "${CURL_LIBRARY}") + if(CURL_INCLUDE_DIR) + foreach(_curl_version_header curlver.h curl.h) + if(EXISTS "${CURL_INCLUDE_DIR}/curl/${_curl_version_header}") + file(STRINGS "${CURL_INCLUDE_DIR}/curl/${_curl_version_header}" curl_version_str REGEX "^#define[\t ]+LIBCURL_VERSION[\t ]+\".*\"") + + string(REGEX REPLACE "^#define[\t ]+LIBCURL_VERSION[\t ]+\"([^\"]*)\".*" "\\1" CURL_VERSION_STRING "${curl_version_str}") + unset(curl_version_str) + break() + endif() + endforeach() + endif() + + include(${CMAKE_CURRENT_LIST_DIR}/FindPackageHandleStandardArgs_SLIC3R.cmake) + FIND_PACKAGE_HANDLE_STANDARD_ARGS_SLIC3R(CURL + REQUIRED_VARS CURL_LIBRARY CURL_INCLUDE_DIR + VERSION_VAR CURL_VERSION_STRING) + + if(CURL_FOUND) + set(CURL_LIBRARIES ${CURL_LIBRARY}) + set(CURL_INCLUDE_DIRS ${CURL_INCLUDE_DIR}) + + if(NOT TARGET CURL::libcurl) + add_library(CURL::libcurl UNKNOWN IMPORTED) set_target_properties(CURL::libcurl PROPERTIES - IMPORTED_LINK_INTERFACE_LANGUAGES "C" - IMPORTED_LOCATION "${CURL_LIBRARY}") - endif() - if(CURL_LIBRARY_RELEASE) - set_property(TARGET CURL::libcurl APPEND PROPERTY - IMPORTED_CONFIGURATIONS RELEASE) - set_target_properties(CURL::libcurl PROPERTIES - IMPORTED_LINK_INTERFACE_LANGUAGES "C" - IMPORTED_LOCATION_RELEASE "${CURL_LIBRARY_RELEASE}") - endif() - if(CURL_LIBRARY_DEBUG) - set_property(TARGET CURL::libcurl APPEND PROPERTY - IMPORTED_CONFIGURATIONS DEBUG) - set_target_properties(CURL::libcurl PROPERTIES - IMPORTED_LINK_INTERFACE_LANGUAGES "C" - IMPORTED_LOCATION_DEBUG "${CURL_LIBRARY_DEBUG}") + INTERFACE_INCLUDE_DIRECTORIES "${CURL_INCLUDE_DIRS}") + + if(EXISTS "${CURL_LIBRARY}") + set_target_properties(CURL::libcurl PROPERTIES + IMPORTED_LINK_INTERFACE_LANGUAGES "C" + IMPORTED_LOCATION "${CURL_LIBRARY}") + endif() + if(CURL_LIBRARY_RELEASE) + set_property(TARGET CURL::libcurl APPEND PROPERTY + IMPORTED_CONFIGURATIONS RELEASE) + set_target_properties(CURL::libcurl PROPERTIES + IMPORTED_LINK_INTERFACE_LANGUAGES "C" + IMPORTED_LOCATION_RELEASE "${CURL_LIBRARY_RELEASE}") + endif() + if(CURL_LIBRARY_DEBUG) + set_property(TARGET CURL::libcurl APPEND PROPERTY + IMPORTED_CONFIGURATIONS DEBUG) + set_target_properties(CURL::libcurl PROPERTIES + IMPORTED_LINK_INTERFACE_LANGUAGES "C" + IMPORTED_LOCATION_DEBUG "${CURL_LIBRARY_DEBUG}") + endif() endif() endif() -endif() + +endif (NOT CURL_FOUND) diff --git a/deps/Boost/Boost.cmake b/deps/Boost/Boost.cmake index fd2f202e9..d819e28cf 100644 --- a/deps/Boost/Boost.cmake +++ b/deps/Boost/Boost.cmake @@ -75,7 +75,9 @@ file(TO_NATIVE_PATH ${DESTDIR}/usr/local/ _prefix) set(_boost_flags "") if (UNIX) set(_boost_flags "cflags=-fPIC;cxxflags=-fPIC") -elseif(APPLE) +endif () + +if(APPLE) set(_boost_flags "cflags=-fPIC -mmacosx-version-min=${DEP_OSX_TARGET};" "cxxflags=-fPIC -mmacosx-version-min=${DEP_OSX_TARGET};" diff --git a/deps/CMakeLists.txt b/deps/CMakeLists.txt index d129ff1c2..eb0c420fa 100644 --- a/deps/CMakeLists.txt +++ b/deps/CMakeLists.txt @@ -188,6 +188,7 @@ endif () include(JPEG/JPEG.cmake) include(TIFF/TIFF.cmake) +include(NanoSVG/NanoSVG.cmake) include(wxWidgets/wxWidgets.cmake) set(_dep_list diff --git a/deps/GLEW/GLEW.cmake b/deps/GLEW/GLEW.cmake index ae2832577..76ffc0a8d 100644 --- a/deps/GLEW/GLEW.cmake +++ b/deps/GLEW/GLEW.cmake @@ -4,8 +4,8 @@ find_package(OpenGL QUIET REQUIRED) prusaslicer_add_cmake_project( GLEW - URL https://sourceforge.net/projects/glew/files/glew/2.1.0/glew-2.1.0.zip - URL_HASH SHA256=2700383d4de2455f06114fbaf872684f15529d4bdc5cdea69b5fb0e9aa7763f1 + URL https://sourceforge.net/projects/glew/files/glew/2.2.0/glew-2.2.0.zip + URL_HASH SHA256=a9046a913774395a095edcc0b0ac2d81c3aacca61787b39839b941e9be14e0d4 SOURCE_SUBDIR build/cmake CMAKE_ARGS -DBUILD_UTILS=OFF diff --git a/deps/NanoSVG/NanoSVG.cmake b/deps/NanoSVG/NanoSVG.cmake new file mode 100644 index 000000000..9623d3226 --- /dev/null +++ b/deps/NanoSVG/NanoSVG.cmake @@ -0,0 +1,4 @@ +prusaslicer_add_cmake_project(NanoSVG + URL https://github.com/memononen/nanosvg/archive/4c8f0139b62c6e7faa3b67ce1fbe6e63590ed148.zip + URL_HASH SHA256=584e084af1a75bf633f79753ce2f6f6ec8686002ca27f35f1037c25675fecfb6 +) \ No newline at end of file diff --git a/deps/wxWidgets/wxWidgets.cmake b/deps/wxWidgets/wxWidgets.cmake index bf5fd6289..e2d48ce65 100644 --- a/deps/wxWidgets/wxWidgets.cmake +++ b/deps/wxWidgets/wxWidgets.cmake @@ -1,5 +1,3 @@ -set(_wx_git_tag v3.1.4-patched) - set(_wx_toolkit "") if(CMAKE_SYSTEM_NAME STREQUAL "Linux") set(_gtk_ver 2) @@ -15,11 +13,9 @@ if (UNIX AND NOT APPLE) # wxWidgets will not use char as the underlying type for endif() prusaslicer_add_cmake_project(wxWidgets - # GIT_REPOSITORY "https://github.com/prusa3d/wxWidgets" - # GIT_TAG tm_cross_compile #${_wx_git_tag} - URL https://github.com/prusa3d/wxWidgets/archive/489f6118256853cf5b299d595868641938566cdb.zip - URL_HASH SHA256=5b22d465377cedd8044bba69bea958b248953fd3628c1de4913a84d4e6f6175b - DEPENDS ${PNG_PKG} ${ZLIB_PKG} ${EXPAT_PKG} dep_TIFF dep_JPEG + URL https://github.com/prusa3d/wxWidgets/archive/e616df45b3a8aedc31beb5540df793dd1f0f3914.zip + URL_HASH SHA256=30bc6cad64dce5cdc755e3a4119cfeb24c12d43279e1e062d8ac350d3880f315 + DEPENDS ${PNG_PKG} ${ZLIB_PKG} ${EXPAT_PKG} dep_TIFF dep_JPEG dep_NanoSVG CMAKE_ARGS -DwxBUILD_PRECOMP=ON ${_wx_toolkit} @@ -32,13 +28,16 @@ prusaslicer_add_cmake_project(wxWidgets -DwxUSE_OPENGL=ON -DwxUSE_LIBPNG=sys -DwxUSE_ZLIB=sys - -DwxUSE_REGEX=builtin + -DwxUSE_NANOSVG=sys + -DwxUSE_NANOSVG_EXTERNAL=ON + -DwxUSE_REGEX=OFF -DwxUSE_LIBXPM=builtin -DwxUSE_LIBJPEG=sys -DwxUSE_LIBTIFF=sys -DwxUSE_EXPAT=sys -DwxUSE_LIBSDL=OFF -DwxUSE_XTEST=OFF + -DwxUSE_GLCANVAS_EGL=OFF ) if (MSVC) diff --git a/resources/profiles/Creality.idx b/resources/profiles/Creality.idx index 1ff148aad..8f7dd0e8c 100644 --- a/resources/profiles/Creality.idx +++ b/resources/profiles/Creality.idx @@ -1,3 +1,5 @@ +min_slic3r_version = 2.5.0-alpha0 +0.1.5 Added Ender-3 S1 Pro min_slic3r_version = 2.4.1 0.1.4 Added Ender-3 Pro. Added M25 support for some printers. min_slic3r_version = 2.4.0-rc diff --git a/resources/profiles/Creality.ini b/resources/profiles/Creality.ini index 0fa14d8a8..ce39cd6d6 100644 --- a/resources/profiles/Creality.ini +++ b/resources/profiles/Creality.ini @@ -5,7 +5,7 @@ name = Creality # Configuration version of this file. Config file will only be installed, if the config_version differs. # This means, the server may force the PrusaSlicer configuration to be downgraded. -config_version = 0.1.4 +config_version = 0.1.5 # Where to get the updates from? config_update_url = https://files.prusa3d.com/wp-content/uploads/repository/PrusaSlicer-settings-master/live/Creality/ # changelog_url = https://files.prusa3d.com/?latest=slicer-profiles&lng=%1% @@ -21,7 +21,7 @@ technology = FFF family = ENDER bed_model = ender3_bed.stl bed_texture = ender3.svg -default_materials = Generic PLA @CREALITY; Generic PETG @CREALITY; Generic ABS @CREALITY; Creality PLA @CREALITY; Prusament PLA @CREALITY; Prusament PETG @CREALITY; AzureFilm PLA @CREALITY; Devil Design PLA @CREALITY; Devil Design PLA Matt @CREALITY; Devil Design PLA Galaxy @CREALITY; Extrudr PLA NX2 @CREALITY; Real Filament PLA @CREALITY; Velleman PLA @CREALITY; 3DJAKE ecoPLA @CREALITY; 3DJAKE ecoPLA Matt @CREALITY; 3DJAKE ecoPLA Tough @CREALITY; 123-3D Jupiter PLA @CREALITY +default_materials = Generic PLA @CREALITY; Generic PETG @CREALITY; Generic ABS @CREALITY; Creality PLA @CREALITY; Prusament PLA @CREALITY; Prusament PETG @CREALITY; AzureFilm PLA @CREALITY; Devil Design PLA @CREALITY; Devil Design PLA Matt @CREALITY; Devil Design PLA Galaxy @CREALITY; Extrudr PLA NX2 @CREALITY; Real Filament PLA @CREALITY; Velleman PLA @CREALITY; 3DJAKE ecoPLA @CREALITY; 3DJAKE ecoPLA Matt @CREALITY; 3DJAKE ecoPLA Tough @CREALITY; 123-3D Jupiter PLA @CREALITY; Verbatim PLA @CREALITY [printer_model:ENDER3BLTOUCH] name = Creality Ender-3 BLTouch @@ -30,7 +30,7 @@ technology = FFF family = ENDER bed_model = ender3_bed.stl bed_texture = ender3.svg -default_materials = Generic PLA @CREALITY; Generic PETG @CREALITY; Generic ABS @CREALITY; Creality PLA @CREALITY; Prusament PLA @CREALITY; Prusament PETG @CREALITY; AzureFilm PLA @CREALITY; Devil Design PLA @CREALITY; Devil Design PLA Matt @CREALITY; Devil Design PLA Galaxy @CREALITY; Extrudr PLA NX2 @CREALITY; Real Filament PLA @CREALITY; Velleman PLA @CREALITY; 3DJAKE ecoPLA @CREALITY; 3DJAKE ecoPLA Matt @CREALITY; 3DJAKE ecoPLA Tough @CREALITY; 123-3D Jupiter PLA @CREALITY +default_materials = Generic PLA @CREALITY; Generic PETG @CREALITY; Generic ABS @CREALITY; Creality PLA @CREALITY; Prusament PLA @CREALITY; Prusament PETG @CREALITY; AzureFilm PLA @CREALITY; Devil Design PLA @CREALITY; Devil Design PLA Matt @CREALITY; Devil Design PLA Galaxy @CREALITY; Extrudr PLA NX2 @CREALITY; Real Filament PLA @CREALITY; Velleman PLA @CREALITY; 3DJAKE ecoPLA @CREALITY; 3DJAKE ecoPLA Matt @CREALITY; 3DJAKE ecoPLA Tough @CREALITY; 123-3D Jupiter PLA @CREALITY; Verbatim PLA @CREALITY [printer_model:ENDER3PRO] name = Creality Ender-3 Pro @@ -39,7 +39,7 @@ technology = FFF family = ENDER bed_model = ender3_bed.stl bed_texture = ender3.svg -default_materials = Generic PLA @CREALITY; Generic PETG @CREALITY; Generic ABS @CREALITY; Creality PLA @CREALITY; Prusament PLA @CREALITY; Prusament PETG @CREALITY; AzureFilm PLA @CREALITY; Devil Design PLA @CREALITY; Devil Design PLA Matt @CREALITY; Devil Design PLA Galaxy @CREALITY; Extrudr PLA NX2 @CREALITY; Real Filament PLA @CREALITY; Velleman PLA @CREALITY; 3DJAKE ecoPLA @CREALITY; 3DJAKE ecoPLA Matt @CREALITY; 3DJAKE ecoPLA Tough @CREALITY; 123-3D Jupiter PLA @CREALITY +default_materials = Generic PLA @CREALITY; Generic PETG @CREALITY; Generic ABS @CREALITY; Creality PLA @CREALITY; Prusament PLA @CREALITY; Prusament PETG @CREALITY; AzureFilm PLA @CREALITY; Devil Design PLA @CREALITY; Devil Design PLA Matt @CREALITY; Devil Design PLA Galaxy @CREALITY; Extrudr PLA NX2 @CREALITY; Real Filament PLA @CREALITY; Velleman PLA @CREALITY; 3DJAKE ecoPLA @CREALITY; 3DJAKE ecoPLA Matt @CREALITY; 3DJAKE ecoPLA Tough @CREALITY; 123-3D Jupiter PLA @CREALITY; Verbatim PLA @CREALITY [printer_model:ENDER3V2] name = Creality Ender-3 V2 @@ -48,7 +48,7 @@ technology = FFF family = ENDER bed_model = ender3v2_bed.stl bed_texture = ender3v2.svg -default_materials = Generic PLA @CREALITY; Generic PETG @CREALITY; Generic ABS @CREALITY; Creality PLA @CREALITY; Prusament PLA @CREALITY; Prusament PETG @CREALITY; AzureFilm PLA @CREALITY; Devil Design PLA @CREALITY; Devil Design PLA Matt @CREALITY; Devil Design PLA Galaxy @CREALITY; Extrudr PLA NX2 @CREALITY; Real Filament PLA @CREALITY; Velleman PLA @CREALITY; 3DJAKE ecoPLA @CREALITY; 3DJAKE ecoPLA Matt @CREALITY; 3DJAKE ecoPLA Tough @CREALITY; 123-3D Jupiter PLA @CREALITY +default_materials = Generic PLA @CREALITY; Generic PETG @CREALITY; Generic ABS @CREALITY; Creality PLA @CREALITY; Prusament PLA @CREALITY; Prusament PETG @CREALITY; AzureFilm PLA @CREALITY; Devil Design PLA @CREALITY; Devil Design PLA Matt @CREALITY; Devil Design PLA Galaxy @CREALITY; Extrudr PLA NX2 @CREALITY; Real Filament PLA @CREALITY; Velleman PLA @CREALITY; 3DJAKE ecoPLA @CREALITY; 3DJAKE ecoPLA Matt @CREALITY; 3DJAKE ecoPLA Tough @CREALITY; 123-3D Jupiter PLA @CREALITY; Verbatim PLA @CREALITY [printer_model:ENDER3S1] name = Creality Ender-3 S1 @@ -57,7 +57,16 @@ technology = FFF family = ENDER bed_model = ender3v2_bed.stl bed_texture = ender3v2.svg -default_materials = Generic PLA @CREALITY; Generic PETG @CREALITY; Generic ABS @CREALITY; Creality PLA @CREALITY; Prusament PLA @CREALITY; Prusament PETG @CREALITY; AzureFilm PLA @CREALITY; Devil Design PLA @CREALITY; Devil Design PLA Matt @CREALITY; Devil Design PLA Galaxy @CREALITY; Extrudr PLA NX2 @CREALITY; Real Filament PLA @CREALITY; Velleman PLA @CREALITY; 3DJAKE ecoPLA @CREALITY; 3DJAKE ecoPLA Matt @CREALITY; 3DJAKE ecoPLA Tough @CREALITY; 123-3D Jupiter PLA @CREALITY +default_materials = Generic PLA @CREALITY; Generic PETG @CREALITY; Generic ABS @CREALITY; Creality PLA @CREALITY; Prusament PLA @CREALITY; Prusament PETG @CREALITY; AzureFilm PLA @CREALITY; Devil Design PLA @CREALITY; Devil Design PLA Matt @CREALITY; Devil Design PLA Galaxy @CREALITY; Extrudr PLA NX2 @CREALITY; Real Filament PLA @CREALITY; Velleman PLA @CREALITY; 3DJAKE ecoPLA @CREALITY; 3DJAKE ecoPLA Matt @CREALITY; 3DJAKE ecoPLA Tough @CREALITY; 123-3D Jupiter PLA @CREALITY; Verbatim PLA @CREALITY + +[printer_model:ENDER3S1PRO] +name = Creality Ender-3 S1 Pro +variants = 0.4 +technology = FFF +family = ENDER +bed_model = ender3v2_bed.stl +bed_texture = ender3v2.svg +default_materials = Generic PLA @CREALITY; Generic PETG @CREALITY; Generic ABS @CREALITY; Creality PLA @CREALITY; Prusament PLA @CREALITY; Prusament PETG @CREALITY; AzureFilm PLA @CREALITY; Devil Design PLA @CREALITY; Devil Design PLA Matt @CREALITY; Devil Design PLA Galaxy @CREALITY; Extrudr PLA NX2 @CREALITY; Real Filament PLA @CREALITY; Velleman PLA @CREALITY; 3DJAKE ecoPLA @CREALITY; 3DJAKE ecoPLA Matt @CREALITY; 3DJAKE ecoPLA Tough @CREALITY; 123-3D Jupiter PLA @CREALITY; Verbatim PLA @CREALITY [printer_model:ENDER3MAX] name = Creality Ender-3 Max @@ -66,7 +75,7 @@ technology = FFF family = ENDER bed_model = cr10v2_bed.stl bed_texture = cr10spro.svg -default_materials = Generic PLA @CREALITY; Generic PETG @CREALITY; Generic ABS @CREALITY; Creality PLA @CREALITY; Prusament PLA @CREALITY; Prusament PETG @CREALITY; AzureFilm PLA @CREALITY; Devil Design PLA @CREALITY; Devil Design PLA Matt @CREALITY; Devil Design PLA Galaxy @CREALITY; Extrudr PLA NX2 @CREALITY; Real Filament PLA @CREALITY; Velleman PLA @CREALITY; 3DJAKE ecoPLA @CREALITY; 3DJAKE ecoPLA Matt @CREALITY; 3DJAKE ecoPLA Tough @CREALITY; 123-3D Jupiter PLA @CREALITY +default_materials = Generic PLA @CREALITY; Generic PETG @CREALITY; Generic ABS @CREALITY; Creality PLA @CREALITY; Prusament PLA @CREALITY; Prusament PETG @CREALITY; AzureFilm PLA @CREALITY; Devil Design PLA @CREALITY; Devil Design PLA Matt @CREALITY; Devil Design PLA Galaxy @CREALITY; Extrudr PLA NX2 @CREALITY; Real Filament PLA @CREALITY; Velleman PLA @CREALITY; 3DJAKE ecoPLA @CREALITY; 3DJAKE ecoPLA Matt @CREALITY; 3DJAKE ecoPLA Tough @CREALITY; 123-3D Jupiter PLA @CREALITY; Verbatim PLA @CREALITY [printer_model:ENDER4] name = Creality Ender-4 @@ -75,7 +84,7 @@ technology = FFF family = ENDER bed_model = ender3v2_bed.stl bed_texture = ender3v2.svg -default_materials = Generic PLA @CREALITY; Generic PETG @CREALITY; Generic ABS @CREALITY; Creality PLA @CREALITY; Prusament PLA @CREALITY; Prusament PETG @CREALITY; AzureFilm PLA @CREALITY; Devil Design PLA @CREALITY; Devil Design PLA Matt @CREALITY; Devil Design PLA Galaxy @CREALITY; Extrudr PLA NX2 @CREALITY; Real Filament PLA @CREALITY; Velleman PLA @CREALITY; 3DJAKE ecoPLA @CREALITY; 3DJAKE ecoPLA Matt @CREALITY; 3DJAKE ecoPLA Tough @CREALITY; 123-3D Jupiter PLA @CREALITY +default_materials = Generic PLA @CREALITY; Generic PETG @CREALITY; Generic ABS @CREALITY; Creality PLA @CREALITY; Prusament PLA @CREALITY; Prusament PETG @CREALITY; AzureFilm PLA @CREALITY; Devil Design PLA @CREALITY; Devil Design PLA Matt @CREALITY; Devil Design PLA Galaxy @CREALITY; Extrudr PLA NX2 @CREALITY; Real Filament PLA @CREALITY; Velleman PLA @CREALITY; 3DJAKE ecoPLA @CREALITY; 3DJAKE ecoPLA Matt @CREALITY; 3DJAKE ecoPLA Tough @CREALITY; 123-3D Jupiter PLA @CREALITY; Verbatim PLA @CREALITY [printer_model:ENDER5] name = Creality Ender-5 @@ -84,7 +93,7 @@ technology = FFF family = ENDER bed_model = ender3_bed.stl bed_texture = ender3.svg -default_materials = Generic PLA @CREALITY; Generic PETG @CREALITY; Generic ABS @CREALITY; Creality PLA @CREALITY; Prusament PLA @CREALITY; Prusament PETG @CREALITY; AzureFilm PLA @CREALITY; Devil Design PLA @CREALITY; Devil Design PLA Matt @CREALITY; Devil Design PLA Galaxy @CREALITY; Extrudr PLA NX2 @CREALITY; Real Filament PLA @CREALITY; Velleman PLA @CREALITY; 3DJAKE ecoPLA @CREALITY; 3DJAKE ecoPLA Matt @CREALITY; 3DJAKE ecoPLA Tough @CREALITY; 123-3D Jupiter PLA @CREALITY +default_materials = Generic PLA @CREALITY; Generic PETG @CREALITY; Generic ABS @CREALITY; Creality PLA @CREALITY; Prusament PLA @CREALITY; Prusament PETG @CREALITY; AzureFilm PLA @CREALITY; Devil Design PLA @CREALITY; Devil Design PLA Matt @CREALITY; Devil Design PLA Galaxy @CREALITY; Extrudr PLA NX2 @CREALITY; Real Filament PLA @CREALITY; Velleman PLA @CREALITY; 3DJAKE ecoPLA @CREALITY; 3DJAKE ecoPLA Matt @CREALITY; 3DJAKE ecoPLA Tough @CREALITY; 123-3D Jupiter PLA @CREALITY; Verbatim PLA @CREALITY [printer_model:ENDER5PLUS] name = Creality Ender-5 Plus @@ -93,7 +102,7 @@ technology = FFF family = ENDER bed_model = ender5plus_bed.stl bed_texture = ender5plus.svg -default_materials = Generic PLA @CREALITY; Generic PETG @CREALITY; Generic ABS @CREALITY; Creality PLA @CREALITY; Prusament PLA @CREALITY; Prusament PETG @CREALITY; AzureFilm PLA @CREALITY; Devil Design PLA @CREALITY; Devil Design PLA Matt @CREALITY; Devil Design PLA Galaxy @CREALITY; Extrudr PLA NX2 @CREALITY; Real Filament PLA @CREALITY; Velleman PLA @CREALITY; 3DJAKE ecoPLA @CREALITY; 3DJAKE ecoPLA Matt @CREALITY; 3DJAKE ecoPLA Tough @CREALITY; 123-3D Jupiter PLA @CREALITY +default_materials = Generic PLA @CREALITY; Generic PETG @CREALITY; Generic ABS @CREALITY; Creality PLA @CREALITY; Prusament PLA @CREALITY; Prusament PETG @CREALITY; AzureFilm PLA @CREALITY; Devil Design PLA @CREALITY; Devil Design PLA Matt @CREALITY; Devil Design PLA Galaxy @CREALITY; Extrudr PLA NX2 @CREALITY; Real Filament PLA @CREALITY; Velleman PLA @CREALITY; 3DJAKE ecoPLA @CREALITY; 3DJAKE ecoPLA Matt @CREALITY; 3DJAKE ecoPLA Tough @CREALITY; 123-3D Jupiter PLA @CREALITY; Verbatim PLA @CREALITY [printer_model:ENDER6] name = Creality Ender-6 @@ -102,7 +111,7 @@ technology = FFF family = ENDER bed_model = ender6_bed.stl bed_texture = ender6.svg -default_materials = Generic PLA @CREALITY; Generic PETG @CREALITY; Generic ABS @CREALITY; Creality PLA @CREALITY; Prusament PLA @CREALITY; Prusament PETG @CREALITY; AzureFilm PLA @CREALITY; Devil Design PLA @CREALITY; Devil Design PLA Matt @CREALITY; Devil Design PLA Galaxy @CREALITY; Extrudr PLA NX2 @CREALITY; Real Filament PLA @CREALITY; Velleman PLA @CREALITY; 3DJAKE ecoPLA @CREALITY; 3DJAKE ecoPLA Matt @CREALITY; 3DJAKE ecoPLA Tough @CREALITY; 123-3D Jupiter PLA @CREALITY +default_materials = Generic PLA @CREALITY; Generic PETG @CREALITY; Generic ABS @CREALITY; Creality PLA @CREALITY; Prusament PLA @CREALITY; Prusament PETG @CREALITY; AzureFilm PLA @CREALITY; Devil Design PLA @CREALITY; Devil Design PLA Matt @CREALITY; Devil Design PLA Galaxy @CREALITY; Extrudr PLA NX2 @CREALITY; Real Filament PLA @CREALITY; Velleman PLA @CREALITY; 3DJAKE ecoPLA @CREALITY; 3DJAKE ecoPLA Matt @CREALITY; 3DJAKE ecoPLA Tough @CREALITY; 123-3D Jupiter PLA @CREALITY; Verbatim PLA @CREALITY [printer_model:ENDER7] name = Creality Ender-7 @@ -111,7 +120,7 @@ technology = FFF family = ENDER bed_model = ender7_bed.stl bed_texture = ender7.svg -default_materials = Generic PLA @CREALITY; Generic PETG @CREALITY; Generic ABS @CREALITY; Creality PLA @CREALITY; Prusament PLA @CREALITY; Prusament PETG @CREALITY; AzureFilm PLA @CREALITY; Devil Design PLA @CREALITY; Devil Design PLA Matt @CREALITY; Devil Design PLA Galaxy @CREALITY; Extrudr PLA NX2 @CREALITY; Real Filament PLA @CREALITY; Velleman PLA @CREALITY; 3DJAKE ecoPLA @CREALITY; 3DJAKE ecoPLA Matt @CREALITY; 3DJAKE ecoPLA Tough @CREALITY; 123-3D Jupiter PLA @CREALITY +default_materials = Generic PLA @CREALITY; Generic PETG @CREALITY; Generic ABS @CREALITY; Creality PLA @CREALITY; Prusament PLA @CREALITY; Prusament PETG @CREALITY; AzureFilm PLA @CREALITY; Devil Design PLA @CREALITY; Devil Design PLA Matt @CREALITY; Devil Design PLA Galaxy @CREALITY; Extrudr PLA NX2 @CREALITY; Real Filament PLA @CREALITY; Velleman PLA @CREALITY; 3DJAKE ecoPLA @CREALITY; 3DJAKE ecoPLA Matt @CREALITY; 3DJAKE ecoPLA Tough @CREALITY; 123-3D Jupiter PLA @CREALITY; Verbatim PLA @CREALITY [printer_model:ENDER2] name = Creality Ender-2 @@ -120,7 +129,7 @@ technology = FFF family = ENDER bed_model = ender2_bed.stl bed_texture = ender2.svg -default_materials = Generic PLA @CREALITY; Generic PETG @CREALITY; Generic ABS @CREALITY; Creality PLA @CREALITY; Prusament PLA @CREALITY; Prusament PETG @CREALITY; AzureFilm PLA @CREALITY; Devil Design PLA @CREALITY; Devil Design PLA Matt @CREALITY; Devil Design PLA Galaxy @CREALITY; Extrudr PLA NX2 @CREALITY; Real Filament PLA @CREALITY; Velleman PLA @CREALITY; 3DJAKE ecoPLA @CREALITY; 3DJAKE ecoPLA Matt @CREALITY; 3DJAKE ecoPLA Tough @CREALITY; 123-3D Jupiter PLA @CREALITY +default_materials = Generic PLA @CREALITY; Generic PETG @CREALITY; Generic ABS @CREALITY; Creality PLA @CREALITY; Prusament PLA @CREALITY; Prusament PETG @CREALITY; AzureFilm PLA @CREALITY; Devil Design PLA @CREALITY; Devil Design PLA Matt @CREALITY; Devil Design PLA Galaxy @CREALITY; Extrudr PLA NX2 @CREALITY; Real Filament PLA @CREALITY; Velleman PLA @CREALITY; 3DJAKE ecoPLA @CREALITY; 3DJAKE ecoPLA Matt @CREALITY; 3DJAKE ecoPLA Tough @CREALITY; 123-3D Jupiter PLA @CREALITY; Verbatim PLA @CREALITY [printer_model:ENDER2PRO] name = Creality Ender-2 Pro @@ -129,7 +138,7 @@ technology = FFF family = ENDER bed_model = ender2pro_bed.stl bed_texture = ender2pro.svg -default_materials = Generic PLA @CREALITY; Generic PETG @CREALITY; Generic ABS @CREALITY; Creality PLA @CREALITY; Prusament PLA @CREALITY; Prusament PETG @CREALITY; AzureFilm PLA @CREALITY; Devil Design PLA @CREALITY; Devil Design PLA Matt @CREALITY; Devil Design PLA Galaxy @CREALITY; Extrudr PLA NX2 @CREALITY; Real Filament PLA @CREALITY; Velleman PLA @CREALITY; 3DJAKE ecoPLA @CREALITY; 3DJAKE ecoPLA Matt @CREALITY; 3DJAKE ecoPLA Tough @CREALITY; 123-3D Jupiter PLA @CREALITY +default_materials = Generic PLA @CREALITY; Generic PETG @CREALITY; Generic ABS @CREALITY; Creality PLA @CREALITY; Prusament PLA @CREALITY; Prusament PETG @CREALITY; AzureFilm PLA @CREALITY; Devil Design PLA @CREALITY; Devil Design PLA Matt @CREALITY; Devil Design PLA Galaxy @CREALITY; Extrudr PLA NX2 @CREALITY; Real Filament PLA @CREALITY; Velleman PLA @CREALITY; 3DJAKE ecoPLA @CREALITY; 3DJAKE ecoPLA Matt @CREALITY; 3DJAKE ecoPLA Tough @CREALITY; 123-3D Jupiter PLA @CREALITY; Verbatim PLA @CREALITY [printer_model:CR5PRO] name = Creality CR-5 Pro @@ -138,7 +147,7 @@ technology = FFF family = CR bed_model = cr5pro_bed.stl bed_texture = cr5pro.svg -default_materials = Generic PLA @CREALITY; Generic PETG @CREALITY; Generic ABS @CREALITY; Creality PLA @CREALITY; Prusament PLA @CREALITY; Prusament PETG @CREALITY; AzureFilm PLA @CREALITY; Devil Design PLA @CREALITY; Devil Design PLA Matt @CREALITY; Devil Design PLA Galaxy @CREALITY; Extrudr PLA NX2 @CREALITY; Real Filament PLA @CREALITY; Velleman PLA @CREALITY; 3DJAKE ecoPLA @CREALITY; 3DJAKE ecoPLA Matt @CREALITY; 3DJAKE ecoPLA Tough @CREALITY; 123-3D Jupiter PLA @CREALITY +default_materials = Generic PLA @CREALITY; Generic PETG @CREALITY; Generic ABS @CREALITY; Creality PLA @CREALITY; Prusament PLA @CREALITY; Prusament PETG @CREALITY; AzureFilm PLA @CREALITY; Devil Design PLA @CREALITY; Devil Design PLA Matt @CREALITY; Devil Design PLA Galaxy @CREALITY; Extrudr PLA NX2 @CREALITY; Real Filament PLA @CREALITY; Velleman PLA @CREALITY; 3DJAKE ecoPLA @CREALITY; 3DJAKE ecoPLA Matt @CREALITY; 3DJAKE ecoPLA Tough @CREALITY; 123-3D Jupiter PLA @CREALITY; Verbatim PLA @CREALITY [printer_model:CR5PROH] name = Creality CR-5 Pro H @@ -147,7 +156,7 @@ technology = FFF family = CR bed_model = cr5pro_bed.stl bed_texture = cr5pro.svg -default_materials = Generic PLA @CREALITY; Generic PETG @CREALITY; Generic ABS @CREALITY; Creality PLA @CREALITY; Prusament PLA @CREALITY; Prusament PETG @CREALITY; AzureFilm PLA @CREALITY; Devil Design PLA @CREALITY; Devil Design PLA Matt @CREALITY; Devil Design PLA Galaxy @CREALITY; Extrudr PLA NX2 @CREALITY; Real Filament PLA @CREALITY; Velleman PLA @CREALITY; 3DJAKE ecoPLA @CREALITY; 3DJAKE ecoPLA Matt @CREALITY; 3DJAKE ecoPLA Tough @CREALITY; 123-3D Jupiter PLA @CREALITY +default_materials = Generic PLA @CREALITY; Generic PETG @CREALITY; Generic ABS @CREALITY; Creality PLA @CREALITY; Prusament PLA @CREALITY; Prusament PETG @CREALITY; AzureFilm PLA @CREALITY; Devil Design PLA @CREALITY; Devil Design PLA Matt @CREALITY; Devil Design PLA Galaxy @CREALITY; Extrudr PLA NX2 @CREALITY; Real Filament PLA @CREALITY; Velleman PLA @CREALITY; 3DJAKE ecoPLA @CREALITY; 3DJAKE ecoPLA Matt @CREALITY; 3DJAKE ecoPLA Tough @CREALITY; 123-3D Jupiter PLA @CREALITY; Verbatim PLA @CREALITY [printer_model:CR6SE] name = Creality CR-6 SE @@ -156,7 +165,7 @@ technology = FFF family = CR bed_model = cr6se_bed.stl bed_texture = cr6se.svg -default_materials = Generic PLA @CREALITY; Generic PETG @CREALITY; Generic ABS @CREALITY; Creality PLA @CREALITY; Prusament PLA @CREALITY; Prusament PETG @CREALITY; AzureFilm PLA @CREALITY; Devil Design PLA @CREALITY; Devil Design PLA Matt @CREALITY; Devil Design PLA Galaxy @CREALITY; Extrudr PLA NX2 @CREALITY; Real Filament PLA @CREALITY; Velleman PLA @CREALITY; 3DJAKE ecoPLA @CREALITY; 3DJAKE ecoPLA Matt @CREALITY; 3DJAKE ecoPLA Tough @CREALITY; 123-3D Jupiter PLA @CREALITY +default_materials = Generic PLA @CREALITY; Generic PETG @CREALITY; Generic ABS @CREALITY; Creality PLA @CREALITY; Prusament PLA @CREALITY; Prusament PETG @CREALITY; AzureFilm PLA @CREALITY; Devil Design PLA @CREALITY; Devil Design PLA Matt @CREALITY; Devil Design PLA Galaxy @CREALITY; Extrudr PLA NX2 @CREALITY; Real Filament PLA @CREALITY; Velleman PLA @CREALITY; 3DJAKE ecoPLA @CREALITY; 3DJAKE ecoPLA Matt @CREALITY; 3DJAKE ecoPLA Tough @CREALITY; 123-3D Jupiter PLA @CREALITY; Verbatim PLA @CREALITY [printer_model:CR6MAX] name = Creality CR-6 Max @@ -165,7 +174,7 @@ technology = FFF family = CR bed_model = cr10s4_bed.stl bed_texture = cr10s4.svg -default_materials = Generic PLA @CREALITY; Generic PETG @CREALITY; Generic ABS @CREALITY; Creality PLA @CREALITY; Prusament PLA @CREALITY; Prusament PETG @CREALITY; AzureFilm PLA @CREALITY; Devil Design PLA @CREALITY; Devil Design PLA Matt @CREALITY; Devil Design PLA Galaxy @CREALITY; Extrudr PLA NX2 @CREALITY; Real Filament PLA @CREALITY; Velleman PLA @CREALITY; 3DJAKE ecoPLA @CREALITY; 3DJAKE ecoPLA Matt @CREALITY; 3DJAKE ecoPLA Tough @CREALITY; 123-3D Jupiter PLA @CREALITY +default_materials = Generic PLA @CREALITY; Generic PETG @CREALITY; Generic ABS @CREALITY; Creality PLA @CREALITY; Prusament PLA @CREALITY; Prusament PETG @CREALITY; AzureFilm PLA @CREALITY; Devil Design PLA @CREALITY; Devil Design PLA Matt @CREALITY; Devil Design PLA Galaxy @CREALITY; Extrudr PLA NX2 @CREALITY; Real Filament PLA @CREALITY; Velleman PLA @CREALITY; 3DJAKE ecoPLA @CREALITY; 3DJAKE ecoPLA Matt @CREALITY; 3DJAKE ecoPLA Tough @CREALITY; 123-3D Jupiter PLA @CREALITY; Verbatim PLA @CREALITY [printer_model:CR10SMART] name = Creality CR-10 SMART @@ -174,7 +183,7 @@ technology = FFF family = CR bed_model = cr10v2_bed.stl bed_texture = cr10spro.svg -default_materials = Generic PLA @CREALITY; Generic PETG @CREALITY; Generic ABS @CREALITY; Creality PLA @CREALITY; Prusament PLA @CREALITY; Prusament PETG @CREALITY; AzureFilm PLA @CREALITY; Devil Design PLA @CREALITY; Devil Design PLA Matt @CREALITY; Devil Design PLA Galaxy @CREALITY; Extrudr PLA NX2 @CREALITY; Real Filament PLA @CREALITY; Velleman PLA @CREALITY; 3DJAKE ecoPLA @CREALITY; 3DJAKE ecoPLA Matt @CREALITY; 3DJAKE ecoPLA Tough @CREALITY; 123-3D Jupiter PLA @CREALITY +default_materials = Generic PLA @CREALITY; Generic PETG @CREALITY; Generic ABS @CREALITY; Creality PLA @CREALITY; Prusament PLA @CREALITY; Prusament PETG @CREALITY; AzureFilm PLA @CREALITY; Devil Design PLA @CREALITY; Devil Design PLA Matt @CREALITY; Devil Design PLA Galaxy @CREALITY; Extrudr PLA NX2 @CREALITY; Real Filament PLA @CREALITY; Velleman PLA @CREALITY; 3DJAKE ecoPLA @CREALITY; 3DJAKE ecoPLA Matt @CREALITY; 3DJAKE ecoPLA Tough @CREALITY; 123-3D Jupiter PLA @CREALITY; Verbatim PLA @CREALITY [printer_model:CR10MINI] name = Creality CR-10 Mini @@ -183,7 +192,7 @@ technology = FFF family = CR bed_model = cr10mini_bed.stl bed_texture = cr10mini.svg -default_materials = Generic PLA @CREALITY; Generic PETG @CREALITY; Generic ABS @CREALITY; Creality PLA @CREALITY; Prusament PLA @CREALITY; Prusament PETG @CREALITY; AzureFilm PLA @CREALITY; Devil Design PLA @CREALITY; Devil Design PLA Matt @CREALITY; Devil Design PLA Galaxy @CREALITY; Extrudr PLA NX2 @CREALITY; Real Filament PLA @CREALITY; Velleman PLA @CREALITY; 3DJAKE ecoPLA @CREALITY; 3DJAKE ecoPLA Matt @CREALITY; 3DJAKE ecoPLA Tough @CREALITY; 123-3D Jupiter PLA @CREALITY +default_materials = Generic PLA @CREALITY; Generic PETG @CREALITY; Generic ABS @CREALITY; Creality PLA @CREALITY; Prusament PLA @CREALITY; Prusament PETG @CREALITY; AzureFilm PLA @CREALITY; Devil Design PLA @CREALITY; Devil Design PLA Matt @CREALITY; Devil Design PLA Galaxy @CREALITY; Extrudr PLA NX2 @CREALITY; Real Filament PLA @CREALITY; Velleman PLA @CREALITY; 3DJAKE ecoPLA @CREALITY; 3DJAKE ecoPLA Matt @CREALITY; 3DJAKE ecoPLA Tough @CREALITY; 123-3D Jupiter PLA @CREALITY; Verbatim PLA @CREALITY [printer_model:CR10MAX] name = Creality CR-10 Max @@ -192,7 +201,7 @@ technology = FFF family = CR bed_model = cr10max_bed.stl bed_texture = cr10max.svg -default_materials = Generic PLA @CREALITY; Generic PETG @CREALITY; Generic ABS @CREALITY; Creality PLA @CREALITY; Prusament PLA @CREALITY; Prusament PETG @CREALITY; AzureFilm PLA @CREALITY; Devil Design PLA @CREALITY; Devil Design PLA Matt @CREALITY; Devil Design PLA Galaxy @CREALITY; Extrudr PLA NX2 @CREALITY; Real Filament PLA @CREALITY; Velleman PLA @CREALITY; 3DJAKE ecoPLA @CREALITY; 3DJAKE ecoPLA Matt @CREALITY; 3DJAKE ecoPLA Tough @CREALITY; 123-3D Jupiter PLA @CREALITY +default_materials = Generic PLA @CREALITY; Generic PETG @CREALITY; Generic ABS @CREALITY; Creality PLA @CREALITY; Prusament PLA @CREALITY; Prusament PETG @CREALITY; AzureFilm PLA @CREALITY; Devil Design PLA @CREALITY; Devil Design PLA Matt @CREALITY; Devil Design PLA Galaxy @CREALITY; Extrudr PLA NX2 @CREALITY; Real Filament PLA @CREALITY; Velleman PLA @CREALITY; 3DJAKE ecoPLA @CREALITY; 3DJAKE ecoPLA Matt @CREALITY; 3DJAKE ecoPLA Tough @CREALITY; 123-3D Jupiter PLA @CREALITY; Verbatim PLA @CREALITY [printer_model:CR10] name = Creality CR-10 @@ -201,7 +210,7 @@ technology = FFF family = CR bed_model = cr10_bed.stl bed_texture = cr10.svg -default_materials = Generic PLA @CREALITY; Generic PETG @CREALITY; Generic ABS @CREALITY; Creality PLA @CREALITY; Prusament PLA @CREALITY; Prusament PETG @CREALITY; AzureFilm PLA @CREALITY; Devil Design PLA @CREALITY; Devil Design PLA Matt @CREALITY; Devil Design PLA Galaxy @CREALITY; Extrudr PLA NX2 @CREALITY; Real Filament PLA @CREALITY; Velleman PLA @CREALITY; 3DJAKE ecoPLA @CREALITY; 3DJAKE ecoPLA Matt @CREALITY; 3DJAKE ecoPLA Tough @CREALITY; 123-3D Jupiter PLA @CREALITY +default_materials = Generic PLA @CREALITY; Generic PETG @CREALITY; Generic ABS @CREALITY; Creality PLA @CREALITY; Prusament PLA @CREALITY; Prusament PETG @CREALITY; AzureFilm PLA @CREALITY; Devil Design PLA @CREALITY; Devil Design PLA Matt @CREALITY; Devil Design PLA Galaxy @CREALITY; Extrudr PLA NX2 @CREALITY; Real Filament PLA @CREALITY; Velleman PLA @CREALITY; 3DJAKE ecoPLA @CREALITY; 3DJAKE ecoPLA Matt @CREALITY; 3DJAKE ecoPLA Tough @CREALITY; 123-3D Jupiter PLA @CREALITY; Verbatim PLA @CREALITY [printer_model:CR10V2] name = Creality CR-10 V2 @@ -210,7 +219,7 @@ technology = FFF family = CR bed_model = cr10v2_bed.stl bed_texture = cr10.svg -default_materials = Generic PLA @CREALITY; Generic PETG @CREALITY; Generic ABS @CREALITY; Creality PLA @CREALITY; Prusament PLA @CREALITY; Prusament PETG @CREALITY; AzureFilm PLA @CREALITY; Devil Design PLA @CREALITY; Devil Design PLA Matt @CREALITY; Devil Design PLA Galaxy @CREALITY; Extrudr PLA NX2 @CREALITY; Real Filament PLA @CREALITY; Velleman PLA @CREALITY; 3DJAKE ecoPLA @CREALITY; 3DJAKE ecoPLA Matt @CREALITY; 3DJAKE ecoPLA Tough @CREALITY; 123-3D Jupiter PLA @CREALITY +default_materials = Generic PLA @CREALITY; Generic PETG @CREALITY; Generic ABS @CREALITY; Creality PLA @CREALITY; Prusament PLA @CREALITY; Prusament PETG @CREALITY; AzureFilm PLA @CREALITY; Devil Design PLA @CREALITY; Devil Design PLA Matt @CREALITY; Devil Design PLA Galaxy @CREALITY; Extrudr PLA NX2 @CREALITY; Real Filament PLA @CREALITY; Velleman PLA @CREALITY; 3DJAKE ecoPLA @CREALITY; 3DJAKE ecoPLA Matt @CREALITY; 3DJAKE ecoPLA Tough @CREALITY; 123-3D Jupiter PLA @CREALITY; Verbatim PLA @CREALITY [printer_model:CR10V3] name = Creality CR-10 V3 @@ -219,7 +228,7 @@ technology = FFF family = CR bed_model = cr10v2_bed.stl bed_texture = cr10.svg -default_materials = Generic PLA @CREALITY; Generic PETG @CREALITY; Generic ABS @CREALITY; Creality PLA @CREALITY; Prusament PLA @CREALITY; Prusament PETG @CREALITY; AzureFilm PLA @CREALITY; Devil Design PLA @CREALITY; Devil Design PLA Matt @CREALITY; Devil Design PLA Galaxy @CREALITY; Extrudr PLA NX2 @CREALITY; Real Filament PLA @CREALITY; Velleman PLA @CREALITY; 3DJAKE ecoPLA @CREALITY; 3DJAKE ecoPLA Matt @CREALITY; 3DJAKE ecoPLA Tough @CREALITY; 123-3D Jupiter PLA @CREALITY +default_materials = Generic PLA @CREALITY; Generic PETG @CREALITY; Generic ABS @CREALITY; Creality PLA @CREALITY; Prusament PLA @CREALITY; Prusament PETG @CREALITY; AzureFilm PLA @CREALITY; Devil Design PLA @CREALITY; Devil Design PLA Matt @CREALITY; Devil Design PLA Galaxy @CREALITY; Extrudr PLA NX2 @CREALITY; Real Filament PLA @CREALITY; Velleman PLA @CREALITY; 3DJAKE ecoPLA @CREALITY; 3DJAKE ecoPLA Matt @CREALITY; 3DJAKE ecoPLA Tough @CREALITY; 123-3D Jupiter PLA @CREALITY; Verbatim PLA @CREALITY [printer_model:CR10S] name = Creality CR-10 S @@ -228,7 +237,7 @@ technology = FFF family = CR bed_model = cr10_bed.stl bed_texture = cr10.svg -default_materials = Generic PLA @CREALITY; Generic PETG @CREALITY; Generic ABS @CREALITY; Creality PLA @CREALITY; Prusament PLA @CREALITY; Prusament PETG @CREALITY; AzureFilm PLA @CREALITY; Devil Design PLA @CREALITY; Devil Design PLA Matt @CREALITY; Devil Design PLA Galaxy @CREALITY; Extrudr PLA NX2 @CREALITY; Real Filament PLA @CREALITY; Velleman PLA @CREALITY; 3DJAKE ecoPLA @CREALITY; 3DJAKE ecoPLA Matt @CREALITY; 3DJAKE ecoPLA Tough @CREALITY; 123-3D Jupiter PLA @CREALITY +default_materials = Generic PLA @CREALITY; Generic PETG @CREALITY; Generic ABS @CREALITY; Creality PLA @CREALITY; Prusament PLA @CREALITY; Prusament PETG @CREALITY; AzureFilm PLA @CREALITY; Devil Design PLA @CREALITY; Devil Design PLA Matt @CREALITY; Devil Design PLA Galaxy @CREALITY; Extrudr PLA NX2 @CREALITY; Real Filament PLA @CREALITY; Velleman PLA @CREALITY; 3DJAKE ecoPLA @CREALITY; 3DJAKE ecoPLA Matt @CREALITY; 3DJAKE ecoPLA Tough @CREALITY; 123-3D Jupiter PLA @CREALITY; Verbatim PLA @CREALITY [printer_model:CR10SPRO] name = Creality CR-10 S Pro @@ -237,7 +246,7 @@ technology = FFF family = CR bed_model = cr10v2_bed.stl bed_texture = cr10spro.svg -default_materials = Generic PLA @CREALITY; Generic PETG @CREALITY; Generic ABS @CREALITY; Creality PLA @CREALITY; Prusament PLA @CREALITY; Prusament PETG @CREALITY; AzureFilm PLA @CREALITY; Devil Design PLA @CREALITY; Devil Design PLA Matt @CREALITY; Devil Design PLA Galaxy @CREALITY; Extrudr PLA NX2 @CREALITY; Real Filament PLA @CREALITY; Velleman PLA @CREALITY; 3DJAKE ecoPLA @CREALITY; 3DJAKE ecoPLA Matt @CREALITY; 3DJAKE ecoPLA Tough @CREALITY; 123-3D Jupiter PLA @CREALITY +default_materials = Generic PLA @CREALITY; Generic PETG @CREALITY; Generic ABS @CREALITY; Creality PLA @CREALITY; Prusament PLA @CREALITY; Prusament PETG @CREALITY; AzureFilm PLA @CREALITY; Devil Design PLA @CREALITY; Devil Design PLA Matt @CREALITY; Devil Design PLA Galaxy @CREALITY; Extrudr PLA NX2 @CREALITY; Real Filament PLA @CREALITY; Velleman PLA @CREALITY; 3DJAKE ecoPLA @CREALITY; 3DJAKE ecoPLA Matt @CREALITY; 3DJAKE ecoPLA Tough @CREALITY; 123-3D Jupiter PLA @CREALITY; Verbatim PLA @CREALITY [printer_model:CR10SPROV2] name = Creality CR-10 S Pro V2 @@ -246,7 +255,7 @@ technology = FFF family = CR bed_model = cr10v2_bed.stl bed_texture = cr10.svg -default_materials = Generic PLA @CREALITY; Generic PETG @CREALITY; Generic ABS @CREALITY; Creality PLA @CREALITY; Prusament PLA @CREALITY; Prusament PETG @CREALITY; AzureFilm PLA @CREALITY; Devil Design PLA @CREALITY; Devil Design PLA Matt @CREALITY; Devil Design PLA Galaxy @CREALITY; Extrudr PLA NX2 @CREALITY; Real Filament PLA @CREALITY; Velleman PLA @CREALITY; 3DJAKE ecoPLA @CREALITY; 3DJAKE ecoPLA Matt @CREALITY; 3DJAKE ecoPLA Tough @CREALITY; 123-3D Jupiter PLA @CREALITY +default_materials = Generic PLA @CREALITY; Generic PETG @CREALITY; Generic ABS @CREALITY; Creality PLA @CREALITY; Prusament PLA @CREALITY; Prusament PETG @CREALITY; AzureFilm PLA @CREALITY; Devil Design PLA @CREALITY; Devil Design PLA Matt @CREALITY; Devil Design PLA Galaxy @CREALITY; Extrudr PLA NX2 @CREALITY; Real Filament PLA @CREALITY; Velleman PLA @CREALITY; 3DJAKE ecoPLA @CREALITY; 3DJAKE ecoPLA Matt @CREALITY; 3DJAKE ecoPLA Tough @CREALITY; 123-3D Jupiter PLA @CREALITY; Verbatim PLA @CREALITY [printer_model:CR10S4] name = Creality CR-10 S4 @@ -255,7 +264,7 @@ technology = FFF family = CR bed_model = cr10s4_bed.stl bed_texture = cr10s4.svg -default_materials = Generic PLA @CREALITY; Generic PETG @CREALITY; Generic ABS @CREALITY; Creality PLA @CREALITY; Prusament PLA @CREALITY; Prusament PETG @CREALITY; AzureFilm PLA @CREALITY; Devil Design PLA @CREALITY; Devil Design PLA Matt @CREALITY; Devil Design PLA Galaxy @CREALITY; Extrudr PLA NX2 @CREALITY; Real Filament PLA @CREALITY; Velleman PLA @CREALITY; 3DJAKE ecoPLA @CREALITY; 3DJAKE ecoPLA Matt @CREALITY; 3DJAKE ecoPLA Tough @CREALITY; 123-3D Jupiter PLA @CREALITY +default_materials = Generic PLA @CREALITY; Generic PETG @CREALITY; Generic ABS @CREALITY; Creality PLA @CREALITY; Prusament PLA @CREALITY; Prusament PETG @CREALITY; AzureFilm PLA @CREALITY; Devil Design PLA @CREALITY; Devil Design PLA Matt @CREALITY; Devil Design PLA Galaxy @CREALITY; Extrudr PLA NX2 @CREALITY; Real Filament PLA @CREALITY; Velleman PLA @CREALITY; 3DJAKE ecoPLA @CREALITY; 3DJAKE ecoPLA Matt @CREALITY; 3DJAKE ecoPLA Tough @CREALITY; 123-3D Jupiter PLA @CREALITY; Verbatim PLA @CREALITY [printer_model:CR10S5] name = Creality CR-10 S5 @@ -264,7 +273,7 @@ technology = FFF family = CR bed_model = cr10s5_bed.stl bed_texture = cr10s5.svg -default_materials = Generic PLA @CREALITY; Generic PETG @CREALITY; Generic ABS @CREALITY; Creality PLA @CREALITY; Prusament PLA @CREALITY; Prusament PETG @CREALITY; AzureFilm PLA @CREALITY; Devil Design PLA @CREALITY; Devil Design PLA Matt @CREALITY; Devil Design PLA Galaxy @CREALITY; Extrudr PLA NX2 @CREALITY; Real Filament PLA @CREALITY; Velleman PLA @CREALITY; 3DJAKE ecoPLA @CREALITY; 3DJAKE ecoPLA Matt @CREALITY; 3DJAKE ecoPLA Tough @CREALITY; 123-3D Jupiter PLA @CREALITY +default_materials = Generic PLA @CREALITY; Generic PETG @CREALITY; Generic ABS @CREALITY; Creality PLA @CREALITY; Prusament PLA @CREALITY; Prusament PETG @CREALITY; AzureFilm PLA @CREALITY; Devil Design PLA @CREALITY; Devil Design PLA Matt @CREALITY; Devil Design PLA Galaxy @CREALITY; Extrudr PLA NX2 @CREALITY; Real Filament PLA @CREALITY; Velleman PLA @CREALITY; 3DJAKE ecoPLA @CREALITY; 3DJAKE ecoPLA Matt @CREALITY; 3DJAKE ecoPLA Tough @CREALITY; 123-3D Jupiter PLA @CREALITY; Verbatim PLA @CREALITY [printer_model:CR20] name = Creality CR-20 @@ -273,7 +282,7 @@ technology = FFF family = CR bed_model = ender3_bed.stl bed_texture = cr20.svg -default_materials = Generic PLA @CREALITY; Generic PETG @CREALITY; Generic ABS @CREALITY; Creality PLA @CREALITY; Prusament PLA @CREALITY; Prusament PETG @CREALITY; AzureFilm PLA @CREALITY; Devil Design PLA @CREALITY; Devil Design PLA Matt @CREALITY; Devil Design PLA Galaxy @CREALITY; Extrudr PLA NX2 @CREALITY; Real Filament PLA @CREALITY; Velleman PLA @CREALITY; 3DJAKE ecoPLA @CREALITY; 3DJAKE ecoPLA Matt @CREALITY; 3DJAKE ecoPLA Tough @CREALITY; 123-3D Jupiter PLA @CREALITY +default_materials = Generic PLA @CREALITY; Generic PETG @CREALITY; Generic ABS @CREALITY; Creality PLA @CREALITY; Prusament PLA @CREALITY; Prusament PETG @CREALITY; AzureFilm PLA @CREALITY; Devil Design PLA @CREALITY; Devil Design PLA Matt @CREALITY; Devil Design PLA Galaxy @CREALITY; Extrudr PLA NX2 @CREALITY; Real Filament PLA @CREALITY; Velleman PLA @CREALITY; 3DJAKE ecoPLA @CREALITY; 3DJAKE ecoPLA Matt @CREALITY; 3DJAKE ecoPLA Tough @CREALITY; 123-3D Jupiter PLA @CREALITY; Verbatim PLA @CREALITY [printer_model:CR20PRO] name = Creality CR-20 Pro @@ -282,7 +291,7 @@ technology = FFF family = CR bed_model = ender3_bed.stl bed_texture = cr20.svg -default_materials = Generic PLA @CREALITY; Generic PETG @CREALITY; Generic ABS @CREALITY; Creality PLA @CREALITY; Prusament PLA @CREALITY; Prusament PETG @CREALITY; AzureFilm PLA @CREALITY; Devil Design PLA @CREALITY; Devil Design PLA Matt @CREALITY; Devil Design PLA Galaxy @CREALITY; Extrudr PLA NX2 @CREALITY; Real Filament PLA @CREALITY; Velleman PLA @CREALITY; 3DJAKE ecoPLA @CREALITY; 3DJAKE ecoPLA Matt @CREALITY; 3DJAKE ecoPLA Tough @CREALITY; 123-3D Jupiter PLA @CREALITY +default_materials = Generic PLA @CREALITY; Generic PETG @CREALITY; Generic ABS @CREALITY; Creality PLA @CREALITY; Prusament PLA @CREALITY; Prusament PETG @CREALITY; AzureFilm PLA @CREALITY; Devil Design PLA @CREALITY; Devil Design PLA Matt @CREALITY; Devil Design PLA Galaxy @CREALITY; Extrudr PLA NX2 @CREALITY; Real Filament PLA @CREALITY; Velleman PLA @CREALITY; 3DJAKE ecoPLA @CREALITY; 3DJAKE ecoPLA Matt @CREALITY; 3DJAKE ecoPLA Tough @CREALITY; 123-3D Jupiter PLA @CREALITY; Verbatim PLA @CREALITY [printer_model:CR200B] name = Creality CR-200B @@ -291,7 +300,7 @@ technology = FFF family = CR bed_model = cr200b_bed.stl bed_texture = cr200b.svg -default_materials = Generic PLA @CREALITY; Generic PETG @CREALITY; Generic ABS @CREALITY; Creality PLA @CREALITY; Prusament PLA @CREALITY; Prusament PETG @CREALITY; AzureFilm PLA @CREALITY; Devil Design PLA @CREALITY; Devil Design PLA Matt @CREALITY; Devil Design PLA Galaxy @CREALITY; Extrudr PLA NX2 @CREALITY; Real Filament PLA @CREALITY; Velleman PLA @CREALITY; 3DJAKE ecoPLA @CREALITY; 3DJAKE ecoPLA Matt @CREALITY; 3DJAKE ecoPLA Tough @CREALITY; 123-3D Jupiter PLA @CREALITY +default_materials = Generic PLA @CREALITY; Generic PETG @CREALITY; Generic ABS @CREALITY; Creality PLA @CREALITY; Prusament PLA @CREALITY; Prusament PETG @CREALITY; AzureFilm PLA @CREALITY; Devil Design PLA @CREALITY; Devil Design PLA Matt @CREALITY; Devil Design PLA Galaxy @CREALITY; Extrudr PLA NX2 @CREALITY; Real Filament PLA @CREALITY; Velleman PLA @CREALITY; 3DJAKE ecoPLA @CREALITY; 3DJAKE ecoPLA Matt @CREALITY; 3DJAKE ecoPLA Tough @CREALITY; 123-3D Jupiter PLA @CREALITY; Verbatim PLA @CREALITY [printer_model:CR8] name = Creality CR-8 @@ -300,7 +309,7 @@ technology = FFF family = CR bed_model = cr8_bed.stl bed_texture = cr8.svg -default_materials = Generic PLA @CREALITY; Generic PETG @CREALITY; Generic ABS @CREALITY; Creality PLA @CREALITY; Prusament PLA @CREALITY; Prusament PETG @CREALITY; AzureFilm PLA @CREALITY; Devil Design PLA @CREALITY; Devil Design PLA Matt @CREALITY; Devil Design PLA Galaxy @CREALITY; Extrudr PLA NX2 @CREALITY; Real Filament PLA @CREALITY; Velleman PLA @CREALITY; 3DJAKE ecoPLA @CREALITY; 3DJAKE ecoPLA Matt @CREALITY; 3DJAKE ecoPLA Tough @CREALITY; 123-3D Jupiter PLA @CREALITY +default_materials = Generic PLA @CREALITY; Generic PETG @CREALITY; Generic ABS @CREALITY; Creality PLA @CREALITY; Prusament PLA @CREALITY; Prusament PETG @CREALITY; AzureFilm PLA @CREALITY; Devil Design PLA @CREALITY; Devil Design PLA Matt @CREALITY; Devil Design PLA Galaxy @CREALITY; Extrudr PLA NX2 @CREALITY; Real Filament PLA @CREALITY; Velleman PLA @CREALITY; 3DJAKE ecoPLA @CREALITY; 3DJAKE ecoPLA Matt @CREALITY; 3DJAKE ecoPLA Tough @CREALITY; 123-3D Jupiter PLA @CREALITY; Verbatim PLA @CREALITY #[printer_model:CRX] #name = Creality CR-X @@ -309,7 +318,7 @@ default_materials = Generic PLA @CREALITY; Generic PETG @CREALITY; Generic ABS @ #family = CR-X #bed_model = cr10v2_bed.stl #bed_texture = cr10spro.svg -#default_materials = Generic PLA @CREALITY; Generic PETG @CREALITY; Generic ABS @CREALITY; Creality PLA @CREALITY; Prusament PLA @CREALITY; Prusament PETG @CREALITY; AzureFilm PLA @CREALITY; Devil Design PLA @CREALITY; Devil Design PLA Matt @CREALITY; Devil Design PLA Galaxy @CREALITY; Extrudr PLA NX2 @CREALITY; Real Filament PLA @CREALITY; Velleman PLA @CREALITY; 3DJAKE ecoPLA @CREALITY; 3DJAKE ecoPLA Matt @CREALITY; 3DJAKE ecoPLA Tough @CREALITY; 123-3D Jupiter PLA @CREALITY +#default_materials = Generic PLA @CREALITY; Generic PETG @CREALITY; Generic ABS @CREALITY; Creality PLA @CREALITY; Prusament PLA @CREALITY; Prusament PETG @CREALITY; AzureFilm PLA @CREALITY; Devil Design PLA @CREALITY; Devil Design PLA Matt @CREALITY; Devil Design PLA Galaxy @CREALITY; Extrudr PLA NX2 @CREALITY; Real Filament PLA @CREALITY; Velleman PLA @CREALITY; 3DJAKE ecoPLA @CREALITY; 3DJAKE ecoPLA Matt @CREALITY; 3DJAKE ecoPLA Tough @CREALITY; 123-3D Jupiter PLA @CREALITY; Verbatim PLA @CREALITY #[printer_model:CRXPRO] #name = Creality CR-X Pro @@ -318,7 +327,7 @@ default_materials = Generic PLA @CREALITY; Generic PETG @CREALITY; Generic ABS @ #family = CR-X #bed_model = cr10v2_bed.stl #bed_texture = cr10spro.svg -#default_materials = Generic PLA @CREALITY; Generic PETG @CREALITY; Generic ABS @CREALITY; Creality PLA @CREALITY; Prusament PLA @CREALITY; Prusament PETG @CREALITY; AzureFilm PLA @CREALITY; Devil Design PLA @CREALITY; Devil Design PLA Matt @CREALITY; Devil Design PLA Galaxy @CREALITY; Extrudr PLA NX2 @CREALITY; Real Filament PLA @CREALITY; Velleman PLA @CREALITY; 3DJAKE ecoPLA @CREALITY; 3DJAKE ecoPLA Matt @CREALITY; 3DJAKE ecoPLA Tough @CREALITY; 123-3D Jupiter PLA @CREALITY +#default_materials = Generic PLA @CREALITY; Generic PETG @CREALITY; Generic ABS @CREALITY; Creality PLA @CREALITY; Prusament PLA @CREALITY; Prusament PETG @CREALITY; AzureFilm PLA @CREALITY; Devil Design PLA @CREALITY; Devil Design PLA Matt @CREALITY; Devil Design PLA Galaxy @CREALITY; Extrudr PLA NX2 @CREALITY; Real Filament PLA @CREALITY; Velleman PLA @CREALITY; 3DJAKE ecoPLA @CREALITY; 3DJAKE ecoPLA Matt @CREALITY; 3DJAKE ecoPLA Tough @CREALITY; 123-3D Jupiter PLA @CREALITY; Verbatim PLA @CREALITY [printer_model:SERMOOND1] name = Creality Sermoon-D1 @@ -327,7 +336,7 @@ technology = FFF family = SERMOON bed_model = sermoond1_bed.stl bed_texture = sermoond1.svg -default_materials = Generic PLA @CREALITY; Generic PETG @CREALITY; Generic ABS @CREALITY; Creality PLA @CREALITY; Prusament PLA @CREALITY; Prusament PETG @CREALITY; AzureFilm PLA @CREALITY; Devil Design PLA @CREALITY; Devil Design PLA Matt @CREALITY; Devil Design PLA Galaxy @CREALITY; Extrudr PLA NX2 @CREALITY; Real Filament PLA @CREALITY; Velleman PLA @CREALITY; 3DJAKE ecoPLA @CREALITY; 3DJAKE ecoPLA Matt @CREALITY; 3DJAKE ecoPLA Tough @CREALITY; 123-3D Jupiter PLA @CREALITY +default_materials = Generic PLA @CREALITY; Generic PETG @CREALITY; Generic ABS @CREALITY; Creality PLA @CREALITY; Prusament PLA @CREALITY; Prusament PETG @CREALITY; AzureFilm PLA @CREALITY; Devil Design PLA @CREALITY; Devil Design PLA Matt @CREALITY; Devil Design PLA Galaxy @CREALITY; Extrudr PLA NX2 @CREALITY; Real Filament PLA @CREALITY; Velleman PLA @CREALITY; 3DJAKE ecoPLA @CREALITY; 3DJAKE ecoPLA Matt @CREALITY; 3DJAKE ecoPLA Tough @CREALITY; 123-3D Jupiter PLA @CREALITY; Verbatim PLA @CREALITY # All presets starting with asterisk, for example *common*, are intermediate and they will # not make it into the user interface. @@ -842,6 +851,17 @@ filament_cost = 27.44 filament_density = 1.29 filament_colour = #C7F935 +[filament:Verbatim PLA @CREALITY] +inherits = *PLA* +filament_vendor = Verbatim +temperature = 205 +bed_temperature = 60 +first_layer_temperature = 210 +first_layer_bed_temperature = 60 +filament_cost = 22.99 +filament_density = 1.24 +filament_colour = #001ca8 + # Common printer preset [printer:*common*] printer_technology = FFF @@ -970,10 +990,7 @@ renamed_from = "Creality ENDER-3 BLTouch" printer_model = ENDER3BLTOUCH [printer:Creality Ender-3 Pro] -inherits = *common*; *pauseprint* -renamed_from = "Creality Ender-3 Pro" -bed_shape = 5x0,215x0,215x220,5x220 -max_print_height = 250 +inherits = Creality Ender-3; *pauseprint* printer_model = ENDER3PRO printer_notes = Don't remove the following keywords! These keywords are used in the "compatible printer" condition of the print and filament profiles to link the particular print and filament profiles to this printer profile.\nPRINTER_VENDOR_CREALITY\nPRINTER_MODEL_ENDER3PRO\nPRINTER_HAS_BOWDEN @@ -992,6 +1009,13 @@ max_print_height = 270 printer_model = ENDER3S1 printer_notes = Don't remove the following keywords! These keywords are used in the "compatible printer" condition of the print and filament profiles to link the particular print and filament profiles to this printer profile.\nPRINTER_VENDOR_CREALITY\nPRINTER_MODEL_ENDER3S1 +[printer:Creality Ender-3 S1 Pro] +inherits = *common*; *pauseprint*; *spriteextruder* +bed_shape = 5x0,215x0,215x220,5x220 +max_print_height = 270 +printer_model = ENDER3S1PRO +printer_notes = Don't remove the following keywords! These keywords are used in the "compatible printer" condition of the print and filament profiles to link the particular print and filament profiles to this printer profile.\nPRINTER_VENDOR_CREALITY\nPRINTER_MODEL_ENDER3S1PRO + [printer:Creality Ender-3 Max] inherits = *common*; *pauseprint* retract_length = 6 diff --git a/resources/profiles/Creality/ENDER3S1PRO_thumbnail.png b/resources/profiles/Creality/ENDER3S1PRO_thumbnail.png new file mode 100644 index 000000000..d11396b05 Binary files /dev/null and b/resources/profiles/Creality/ENDER3S1PRO_thumbnail.png differ diff --git a/resources/profiles/INAT.idx b/resources/profiles/INAT.idx index a756b34b5..0446399a1 100644 --- a/resources/profiles/INAT.idx +++ b/resources/profiles/INAT.idx @@ -1,4 +1,6 @@ -min_slic3r_version = 2.3.1-beta +min_slic3r_version = 2.5.0-alpha0 +0.0.4 Improve Proton X profiles, Add Proton XE-750 printer +min_slic3r_version = 2.4.1 0.0.3 Set default filament profile. 0.0.2 Improved start gcode, changed filename format 0.0.1 Initial version diff --git a/resources/profiles/INAT.ini b/resources/profiles/INAT.ini index 3c1a753b5..0bba8c976 100644 --- a/resources/profiles/INAT.ini +++ b/resources/profiles/INAT.ini @@ -3,7 +3,7 @@ [vendor] # Vendor name will be shown by the Config Wizard. name = INAT -config_version = 0.0.3 +config_version = 0.0.4 config_update_url = https://files.prusa3d.com/wp-content/uploads/repository/PrusaSlicer-settings-master/live/INAT/ ### @@ -24,9 +24,16 @@ technology = FFF family = Proton default_materials = PLA @PROTON_X +[printer_model:PROTON_XE750] +name = INAT Proton XE-750 +variants = 0.4 +technology = FFF +family = Proton +default_materials = PLA @PROTON_XE750 + ### -### QUALITY DEFINITIONS +### COMMON QUALITY DEFINITIONS ### [print:*common*] @@ -35,14 +42,15 @@ layer_height = 0.2 first_layer_height = 0.2 perimeters = 3 spiral_vase = 0 -top_solid_layers = 4 +top_solid_layers = 5 bottom_solid_layers = 3 -top_solid_min_thickness = 0.8 +top_solid_min_thickness = 1 bottom_solid_min_thickness = 0.6 extra_perimeters = 1 ensure_vertical_shell_thickness = 1 avoid_crossing_perimeters = 0 thin_walls = 0 +thick_bridges = 0 overhangs = 1 seam_position = aligned external_perimeters_first = 0 @@ -76,7 +84,9 @@ support_material_auto = 1 support_material_threshold = 0 support_material_enforce_layers = 0 raft_layers = 0 -support_material_contact_distance = 0.2 +support_material_style = grid +support_material_contact_distance = 0.25 +support_material_bottom_contact_distance = 0.3 support_material_pattern = rectilinear support_material_with_sheath = 0 support_material_spacing = 5 @@ -92,8 +102,8 @@ perimeter_speed = 60 small_perimeter_speed = 75% external_perimeter_speed = 50% infill_speed = 80 -solid_infill_speed = 100% -top_solid_infill_speed = 30 +solid_infill_speed = 80% +top_solid_infill_speed = 20 support_material_speed = 80 support_material_interface_speed = 100% bridge_speed = 60 @@ -126,7 +136,7 @@ infill_overlap = 25% bridge_flow_ratio = 1 slice_closing_radius = 0.049 resolution = 0 -xy_size_compensation = 0 +xy_size_compensation = -0.05 elefant_foot_compensation = 0.3 clip_multipart_objects = 0 #output @@ -138,47 +148,50 @@ gcode_label_objects = 0 output_filename_format = {input_filename_base}_{filament_type[0]}.gcode -[print:0.2mm Standard @PROTON_X] +[print:*common 0.2mm Standard @INAT*] inherits = *common* -[print:0.2mm Strong @PROTON_X] +[print:*common 0.2mm Strong @INAT*] inherits = *common* fill_density = 50% perimeters = 6 -[print:0.2mm Advanced Material @PROTON_X] +[print:*common 0.2mm Advanced Material @INAT*] inherits = *common* bottom_solid_layers = 5 top_solid_layers = 6 skirts = 0 -brim_width = 30 +brim_width = 20 infill_speed = 60 support_material_speed = 60 travel_speed = 100 first_layer_speed = 20 elefant_foot_compensation = 0 -[print:0.12mm Fine @PROTON_X] +[print:*common 0.12mm Fine @INAT*] inherits = *common* +layer_height = 0.12 bottom_solid_layers = 7 top_solid_layers = 7 infill_every_layers = 2 perimeter_speed = 50 infill_speed = 50 -[print:0.32mm Draft @PROTON_X] +[print:*common 0.32mm Draft @INAT*] inherits = *common* +layer_height = 0.32 perimeter_speed = 80 external_perimeter_speed = 75% infill_speed = 100 top_solid_infill_speed = 60 fill_density = 15% +support_material_style = snug ### -### PRINTER DEFINITIONS +### COMMON PRINTER DEFINITIONS ### -[printer:*common*] +[printer:*proton_x_common*] printer_vendor = INAT s.r.o. default_filament_profile = "PLA @PROTON_X" #general @@ -206,14 +219,14 @@ machine_max_feedrate_z = 10,10 machine_max_feedrate_e = 100,100 machine_max_acceleration_x = 500,500 machine_max_acceleration_y = 500,500 -machine_max_acceleration_z = 100,100 -machine_max_acceleration_e = 2000,2000 +machine_max_acceleration_z = 200,200 +machine_max_acceleration_e = 8000,8000 machine_max_acceleration_extruding = 1000,1000 -machine_max_acceleration_retracting = 1500,1500 +machine_max_acceleration_retracting = 8000,8000 machine_max_jerk_x = 8,8 machine_max_jerk_y = 8,8 -machine_max_jerk_z = 1,1 -machine_max_jerk_e = 2.5,2.5 +machine_max_jerk_z = 3,3 +machine_max_jerk_e = 10,10 machine_min_extruding_rate = 5 #extruder 1 nozzle_diameter = 0.4 @@ -233,24 +246,65 @@ wipe = 1 retract_before_wipe = 100% -[printer:Proton X Rail] -inherits = *common* -printer_model = PROTON_X_RAIL -printer_variant = 0.4 -default_print_profile = 0.2mm Standard @PROTON_X -gcode_flavor = marlin -machine_max_acceleration_y = 800,800 - -[printer:Proton X Rod] -inherits = *common* -printer_model = PROTON_X_ROD -printer_variant = 0.4 -default_print_profile = 0.2mm Standard @PROTON_X +[printer:*proton_xe750_common*] +printer_vendor = INAT s.r.o. +default_filament_profile = "PLA @PROTON_XE750" +#general +printer_technology = FFF +bed_shape = 0x0,600x0,600x500,0x500 +max_print_height = 750 +z_offset = 0 +extruders_count = 2 gcode_flavor = marlin +silent_mode = 0 +remaining_times = 1 +use_relative_e_distances = 0 +use_firmware_retraction = 0 +use_volumetric_e = 0 +variable_layer_height = 1 +#gcodes +start_gcode = G28 ;Home\nG0 Z10 F1000\nG29\nG0 X0 Y0 Z30 F6000\nM84 E\nM0\nG1 Z15.0 F6000 ;Move the platform down 15mm\n +end_gcode = M400\nM104 S0\nM140 S0\nM107\n;Retract the filament\nG92 E1\nG1 E-1 F300\nG28 X R5\nG0 Y300 F2000\nM84\nG4 S180\nM81 S30\n +color_change_gcode = M600 +#limits +machine_limits_usage = emit_to_gcode +machine_max_feedrate_x = 200,200 +machine_max_feedrate_y = 200,200 +machine_max_feedrate_z = 10,10 +machine_max_feedrate_e = 100,100 +machine_max_acceleration_x = 1500,1500 +machine_max_acceleration_y = 1500,1500 +machine_max_acceleration_z = 500,500 +machine_max_acceleration_e = 20000,20000 +machine_max_acceleration_extruding = 2000,2000 +machine_max_acceleration_retracting = 8000,8000 +machine_max_jerk_x = 12,12 +machine_max_jerk_y = 12,12 +machine_max_jerk_z = 3,3 +machine_max_jerk_e = 20,20 +machine_min_extruding_rate = 5 +#extruder 1 +nozzle_diameter = 0.4,0.4 +min_layer_height = 0.05,0.05 +max_layer_height = 0.33,0.33 +extruder_offset = 0x0,0x0 +retract_length = 1.5,1.5 +retract_lift = 0.6,0.6 +retract_lift_above = 0,0 +retract_lift_below = 0,0 +retract_speed = 45,45 +deretract_speed = 0,0 +retract_restart_extra = 0,0 +retract_before_travel = 2,2 +retract_layer_change = 0,0 +wipe = 1,1 +retract_before_wipe = 100%,100% +retract_length_toolchange = 37,37 +extruder_colour = #33CC33;#3399FF ### -### MATERIAL DEFINITIONS +### COMMON MATERIAL DEFINITIONS ### [filament:*common*] @@ -272,7 +326,7 @@ min_print_speed = 10 filament_soluble = 0 -[filament:PLA @PROTON_X] +[filament:*common PLA @INAT*] inherits = *common* temperature = 210 bed_temperature = 60 @@ -281,11 +335,12 @@ first_layer_bed_temperature = 60 filament_type = PLA filament_cost = 20 filament_density = 1.25 +fan_always_on = 1 min_fan_speed = 50 max_fan_speed = 100 -[filament:PETG @PROTON_X] +[filament:*common PETG @INAT*] inherits = *common* temperature = 240 bed_temperature = 80 @@ -294,10 +349,11 @@ first_layer_bed_temperature = 80 filament_type = PETG filament_cost = 25 filament_density = 1.27 -min_fan_speed = 0 +fan_always_on = 1 +min_fan_speed = 25 max_fan_speed = 50 -[filament:ABS @PROTON_X] +[filament:*common ABS @INAT*] inherits = *common* temperature = 235 bed_temperature = 100 @@ -309,7 +365,7 @@ filament_density = 1.01 cooling = 0 bridge_fan_speed = 0 -[filament:ASA @PROTON_X] +[filament:*common ASA @INAT*] inherits = *common* temperature = 240 bed_temperature = 110 @@ -320,7 +376,7 @@ filament_cost = 22 filament_density = 1.07 cooling = 0 -[filament:TPE @PROTON_X] +[filament:*common TPE @INAT*] inherits = *common* temperature = 220 bed_temperature = 40 @@ -334,7 +390,7 @@ max_fan_speed = 50 filament_retract_length = 0.8 filament_retract_speed = 25 -[filament:HIPS @PROTON_X] +[filament:*common HIPS @INAT*] inherits = *common* temperature = 245 bed_temperature = 100 @@ -347,7 +403,7 @@ min_fan_speed = 0 max_fan_speed = 50 filament_soluble = 1 -[filament:Nylon @PROTON_X] +[filament:*common Nylon @INAT*] inherits = *common* temperature = 235 bed_temperature = 130 @@ -359,19 +415,19 @@ filament_density = 1.01 cooling = 0 bridge_fan_speed = 0 -[filament:PC @PROTON_X] +[filament:*common PC @INAT*] inherits = *common* temperature = 270 -bed_temperature = 130 +bed_temperature = 115 first_layer_temperature = 270 -first_layer_bed_temperature = 130 +first_layer_bed_temperature = 115 filament_type = PC filament_cost = 65 filament_density = 1.19 cooling = 0 bridge_fan_speed = 0 -[filament:CPE @PROTON_X] +[filament:*common CPE @INAT*] inherits = *common* temperature = 280 bed_temperature = 90 @@ -383,7 +439,7 @@ filament_density = 1.27 cooling = 0 bridge_fan_speed = 0 -[filament:PEEK @PROTON_X] +[filament:*common PEEK @INAT*] inherits = *common* temperature = 440 bed_temperature = 150 @@ -395,7 +451,7 @@ filament_density = 1.3 cooling = 0 bridge_fan_speed = 0 -[filament:PEI @PROTON_X] +[filament:*common PEI @INAT*] inherits = *common* temperature = 400 bed_temperature = 150 @@ -407,7 +463,7 @@ filament_density = 1.27 cooling = 0 bridge_fan_speed = 0 -[filament:Polymaker PolyMide CoPA @PROTON_X] +[filament:*common Polymaker PolyMide CoPA @INAT*] inherits = *common* filament_vendor = Polymaker temperature = 265 @@ -419,7 +475,7 @@ filament_cost = 93 filament_density = 1.12 cooling = 0 -[filament:Polymaker PolyMide PA6-CF @PROTON_X] +[filament:*common Polymaker PolyMide PA6-CF @INAT*] inherits = *common* filament_vendor = Polymaker temperature = 300 @@ -431,7 +487,7 @@ filament_cost = 95 filament_density = 1.17 cooling = 0 -[filament:Polymaker PolyMide PA6-GF @PROTON_X] +[filament:*common Polymaker PolyMide PA6-GF @INAT*] inherits = *common* filament_vendor = Polymaker temperature = 300 @@ -443,20 +499,21 @@ filament_cost = 95 filament_density = 1.2 cooling = 0 -[filament:Devil Design PETG @PROTON_X] +[filament:*common Devil Design PETG @INAT*] inherits = *common* filament_vendor = Devil Design -temperature = 250 +temperature = 245 bed_temperature = 80 -first_layer_temperature = 250 +first_layer_temperature = 245 first_layer_bed_temperature = 80 filament_type = PETG filament_cost = 22 filament_density = 1.23 -min_fan_speed = 0 +fan_always_on = 1 +min_fan_speed = 25 max_fan_speed = 50 -[filament:Filament PM PETG FRJet @PROTON_X] +[filament:*common Filament PM PETG FRJet @INAT*] inherits = *common* filament_vendor = Filament PM temperature = 250 @@ -467,3 +524,207 @@ filament_type = PETG filament_cost = 45.5 filament_density = 1.27 cooling = 0 + + +###### +###### PROTON X PRINTERS +###### + +[printer:Proton X Rail] +inherits = *proton_x_common* +printer_model = PROTON_X_RAIL +printer_variant = 0.4 +default_print_profile = 0.2mm Standard @PROTON_X +gcode_flavor = marlin +machine_max_acceleration_x = 800,800 +machine_max_acceleration_y = 800,800 +machine_max_jerk_x = 10,10 +machine_max_jerk_y = 10,10 + +[printer:Proton X Rod] +inherits = *proton_x_common* +printer_model = PROTON_X_ROD +printer_variant = 0.4 +default_print_profile = 0.2mm Standard @PROTON_X +gcode_flavor = marlin + +[print:0.2mm Standard @PROTON_X] +inherits = *common 0.2mm Standard @INAT* +compatible_printers = "Proton X Rail";"Proton X Rod" + +[print:0.2mm Strong @PROTON_X] +inherits = *common 0.2mm Strong @INAT* +compatible_printers = "Proton X Rail";"Proton X Rod" + +[print:0.2mm Advanced Material @PROTON_X] +inherits = *common 0.2mm Advanced Material @INAT* +compatible_printers = "Proton X Rail";"Proton X Rod" + +[print:0.12mm Fine @PROTON_X] +inherits = *common 0.12mm Fine @INAT* +compatible_printers = "Proton X Rail";"Proton X Rod" + +[print:0.32mm Draft @PROTON_X] +inherits = *common 0.32mm Draft @INAT* +compatible_printers = "Proton X Rail";"Proton X Rod" + + + +[filament:PLA @PROTON_X] +inherits =*common PLA @INAT* +compatible_printers = "Proton X Rail";"Proton X Rod" + +[filament:PETG @PROTON_X] +inherits =*common PETG @INAT* +compatible_printers = "Proton X Rail";"Proton X Rod" + +[filament:ABS @PROTON_X] +inherits =*common ABS @INAT* +compatible_printers = "Proton X Rail";"Proton X Rod" + +[filament:ASA @PROTON_X] +inherits =*common ASA @INAT* +compatible_printers = "Proton X Rail";"Proton X Rod" + +[filament:TPE @PROTON_X] +inherits =*common TPE @INAT* +compatible_printers = "Proton X Rail";"Proton X Rod" + +[filament:HIPS @PROTON_X] +inherits =*common HIPS @INAT* +compatible_printers = "Proton X Rail";"Proton X Rod" + +[filament:Nylon @PROTON_X] +inherits =*common Nylon @INAT* +compatible_printers = "Proton X Rail";"Proton X Rod" + +[filament:PC @PROTON_X] +inherits =*common PC @INAT* +compatible_printers = "Proton X Rail";"Proton X Rod" + +[filament:CPE @PROTON_X] +inherits =*common CPE @INAT* +compatible_printers = "Proton X Rail";"Proton X Rod" + +[filament:PEEK @PROTON_X] +inherits =*common PEEK @INAT* +compatible_printers = "Proton X Rail";"Proton X Rod" + +[filament:PEI @PROTON_X] +inherits =*common PEI @INAT* +compatible_printers = "Proton X Rail";"Proton X Rod" + +[filament:Polymaker PolyMide CoPA @PROTON_X] +inherits =*common Polymaker PolyMide CoPA @INAT* +compatible_printers = "Proton X Rail";"Proton X Rod" + +[filament:Polymaker PolyMide PA6-CF @PROTON_X] +inherits =*common Polymaker PolyMide PA6-CF @INAT* +compatible_printers = "Proton X Rail";"Proton X Rod" + +[filament:Polymaker PolyMide PA6-GF @PROTON_X] +inherits =*common Polymaker PolyMide PA6-GF @INAT* +compatible_printers = "Proton X Rail";"Proton X Rod" + +[filament:Devil Design PETG @PROTON_X] +inherits =*common Devil Design PETG @INAT* +compatible_printers = "Proton X Rail";"Proton X Rod" + +[filament:Filament PM PETG FRJet @PROTON_X] +inherits =*common Filament PM PETG FRJet @INAT* +compatible_printers = "Proton X Rail";"Proton X Rod" + + + + +######### ######### +######### Proton XE 750 ######### +######### ######### + +[printer:Proton XE-750] +inherits = *proton_xe750_common* +printer_model = PROTON_XE750 +printer_variant = 0.4 +default_print_profile = 0.2mm Standard @PROTON_XE750 +gcode_flavor = marlin + + +[print:0.2mm Standard @PROTON_XE750] +inherits = *common 0.2mm Standard @INAT* +compatible_printers = "Proton XE-750" + +[print:0.2mm Strong @PROTON_XE750] +inherits = *common 0.2mm Strong @INAT* +compatible_printers = "Proton XE-750" + +[print:0.2mm Advanced Material @PROTON_XE750] +inherits = *common 0.2mm Advanced Material @INAT* +compatible_printers = "Proton XE-750" + +[print:0.12mm Fine @PROTON_XE750] +inherits = *common 0.12mm Fine @INAT* +compatible_printers = "Proton XE-750" + +[print:0.32mm Draft @PROTON_XE750] +inherits = *common 0.32mm Draft @INAT* +compatible_printers = "Proton XE-750" + + + + + + +[filament:*start_end_gcode @PROTON_XE750*] +start_filament_gcode = "; Filament start gcode BEGIN\nM104 S[temperature[current_extruder]]\nG4 S20\n; Filament start gcode END\n" +end_filament_gcode = "; Filament end gcode BEGIN\nG0 X-5 Y250 F10000\nM104 S{temperature[current_extruder] - 50}\n; Filament end gcode END\n" +compatible_printers = "Proton XE-750" + + + +[filament:PLA @PROTON_XE750] +inherits =*common PLA @INAT*; *start_end_gcode @PROTON_XE750* + +[filament:PETG @PROTON_XE750] +inherits =*common PETG @INAT*; *start_end_gcode @PROTON_XE750* + +[filament:ABS @PROTON_XE750] +inherits =*common ABS @INAT*; *start_end_gcode @PROTON_XE750* + +[filament:ASA @PROTON_XE750] +inherits =*common ASA @INAT*; *start_end_gcode @PROTON_XE750* + +[filament:TPE @PROTON_XE750] +inherits =*common TPE @INAT*; *start_end_gcode @PROTON_XE750* + +[filament:HIPS @PROTON_XE750] +inherits =*common HIPS @INAT*; *start_end_gcode @PROTON_XE750* + +[filament:Nylon @PROTON_XE750] +inherits =*common Nylon @INAT*; *start_end_gcode @PROTON_XE750* + +[filament:PC @PROTON_XE750] +inherits =*common PC @INAT*; *start_end_gcode @PROTON_XE750* + +[filament:CPE @PROTON_XE750] +inherits =*common CPE @INAT*; *start_end_gcode @PROTON_XE750* + +[filament:PEEK @PROTON_XE750] +inherits =*common PEEK @INAT*; *start_end_gcode @PROTON_XE750* + +[filament:PEI @PROTON_XE750] +inherits =*common PEI @INAT*; *start_end_gcode @PROTON_XE750* + +[filament:Polymaker PolyMide CoPA @PROTON_XE750] +inherits =*common Polymaker PolyMide CoPA @INAT*; *start_end_gcode @PROTON_XE750* + +[filament:Polymaker PolyMide PA6-CF @PROTON_XE750] +inherits =*common Polymaker PolyMide PA6-CF @INAT*; *start_end_gcode @PROTON_XE750* + +[filament:Polymaker PolyMide PA6-GF @PROTON_XE750] +inherits =*common Polymaker PolyMide PA6-GF @INAT*; *start_end_gcode @PROTON_XE750* + +[filament:Devil Design PETG @PROTON_XE750] +inherits =*common Devil Design PETG @INAT*; *start_end_gcode @PROTON_XE750* + +[filament:Filament PM PETG FRJet @PROTON_XE750] +inherits =*common Filament PM PETG FRJet @INAT*; *start_end_gcode @PROTON_XE750* diff --git a/resources/profiles/INAT/PROTON_XE750_thumbnail.png b/resources/profiles/INAT/PROTON_XE750_thumbnail.png new file mode 100644 index 000000000..114247ade Binary files /dev/null and b/resources/profiles/INAT/PROTON_XE750_thumbnail.png differ diff --git a/resources/profiles/Infinity3D.idx b/resources/profiles/Infinity3D.idx new file mode 100644 index 000000000..0af9cc73b --- /dev/null +++ b/resources/profiles/Infinity3D.idx @@ -0,0 +1,2 @@ +min_slic3r_version = 2.5.0-alpha0 +1.0.0 Initial Infinity3D bundle diff --git a/resources/profiles/Infinity3D.ini b/resources/profiles/Infinity3D.ini new file mode 100644 index 000000000..73703b43d --- /dev/null +++ b/resources/profiles/Infinity3D.ini @@ -0,0 +1,812 @@ +# Infinity3D profiles + +[vendor] +# Vendor name will be shown by the Config Wizard. +name = Infinity3D +# Configuration version of this file. Config file will only be installed, if the config_version differs. +# This means, the server may force the Slic3r configuration to be downgraded. +config_version = 1.0.0 +# Where to get the updates from? +config_update_url = https://files.prusa3d.com/wp-content/uploads/repository/PrusaSlicer-settings-master/live/Infinity3D/ +# The printer models will be shown by the Configuration Wizard in this order, + +[printer_model:DEV-200] +name = Infinity3D DEV-200 +variants = 0.4 +technology = FFF +bed_model = DEV_200_bed.stl +bed_texture = DEV_200_texture.svg +default_materials = Generic PLA @Infinity3D; Generic PETG @Infinity3D + +[printer_model:DEV-350] +name = Infinity3D DEV-350 +variants = 0.4 +technology = FFF +bed_model = DEV_350_bed.stl +bed_texture = DEV_350_texture.svg +default_materials = Generic PLA @Infinity3D; Generic PETG @Infinity3D + +[print:*common*] +avoid_crossing_perimeters = 1 +avoid_crossing_perimeters_max_detour = 0 +bottom_fill_pattern = monotonic +bottom_solid_layers = 4 +bottom_solid_min_thickness = 0 +bridge_acceleration = 0 +bridge_angle = 0 +bridge_flow_ratio = 1 +bridge_speed = 60 +brim_separation = 0 +brim_type = outer_only +brim_width = 0 +clip_multipart_objects = 1 +complete_objects = 0 +default_acceleration = 0 +dont_support_bridges = 1 +draft_shield = disabled +elefant_foot_compensation = 0.1 +ensure_vertical_shell_thickness = 1 +external_perimeter_extrusion_width = 0.45 +external_perimeter_speed = 80% +external_perimeters_first = 0 +extra_perimeters = 0 +extruder_clearance_height = 25 +extruder_clearance_radius = 75 +extrusion_width = 0.45 +fill_angle = 45 +fill_density = 20% +fill_pattern = gyroid +first_layer_acceleration = 0 +first_layer_acceleration_over_raft = 0 +first_layer_extrusion_width = 0.45 +first_layer_height = 0.2 +first_layer_speed = 45 +first_layer_speed_over_raft = 45 +fuzzy_skin = none +fuzzy_skin_point_dist = 0.8 +fuzzy_skin_thickness = 0.3 +gap_fill_enabled = 1 +gap_fill_speed = 20 +gcode_comments = 0 +gcode_label_objects = 0 +gcode_resolution = 0.0125 +gcode_substitutions = +infill_acceleration = 0 +infill_anchor = 600% +infill_anchor_max = 50 +infill_every_layers = 1 +infill_extruder = 1 +infill_extrusion_width = 0.45 +infill_first = 0 +infill_only_where_needed = 0 +infill_overlap = 25% +infill_speed = 60 +inherits = +interface_shells = 0 +ironing = 0 +ironing_flowrate = 15% +ironing_spacing = 0.25 +ironing_speed = 30 +ironing_type = top +layer_height = 0.2 +max_print_speed = 100 +max_volumetric_speed = 0 +min_skirt_length = 4 +mmu_segmented_region_max_width = 0 +notes = +only_retract_when_crossing_perimeters = 0 +ooze_prevention = 0 +output_filename_format = {input_filename_base}_{layer_height}mm_{filament_type[0]}_{printer_model}_{print_time}.gcode +overhangs = 0 +perimeter_acceleration = 0 +perimeter_extruder = 1 +perimeter_extrusion_width = 0.45 +perimeter_speed = 60 +perimeters = 2 +post_process = +print_settings_id = +raft_contact_distance = 0.1 +raft_expansion = 1.5 +raft_first_layer_density = 90% +raft_first_layer_expansion = 3 +raft_layers = 0 +resolution = 0 +seam_position = nearest +single_extruder_multi_material_priming = 1 +skirt_distance = 5 +skirt_height = 1 +skirts = 3 +slice_closing_radius = 0.049 +slicing_mode = regular +small_perimeter_speed = 70% +solid_infill_below_area = 0 +solid_infill_every_layers = 0 +solid_infill_extruder = 1 +solid_infill_extrusion_width= 0.45 +solid_infill_speed = 80% +spiral_vase = 0 +standby_temperature_delta = -5 +support_material = 0 +support_material_angle = 0 +support_material_auto = 1 +support_material_bottom_contact_distance = 0 +support_material_bottom_interface_layers = -1 +support_material_buildplate_only = 0 +support_material_closing_radius = 2 +support_material_contact_distance = 0.15 +support_material_enforce_layers = 0 +support_material_extruder = 0 +support_material_extrusion_width = 0.38 +support_material_interface_contact_loops = 0 +support_material_interface_extruder = 0 +support_material_interface_layers = 2 +support_material_interface_pattern = rectilinear +support_material_interface_spacing = 0.2 +support_material_interface_speed = 100% +support_material_pattern = rectilinear +support_material_spacing = 2 +support_material_speed = 60 +support_material_style = grid +support_material_synchronize_layers = 0 +support_material_threshold = 45 +support_material_with_sheath = 0 +support_material_xy_spacing = 60% +thick_bridges = 1 +thin_walls = 0 +threads = 8 +top_fill_pattern = monotonic +top_infill_extrusion_width = 0.45 +top_solid_infill_speed = 60% +top_solid_layers = 4 +top_solid_min_thickness = 0 +travel_speed = 70 +travel_speed_z = 0 +wipe_tower = 0 +wipe_tower_bridging = 10 +wipe_tower_brim_width = 2 +wipe_tower_no_sparse_layers = 0 +wipe_tower_rotation_angle = 0 +wipe_tower_width = 60 +wipe_tower_x = 170 +wipe_tower_y = 140 +xy_size_compensation = 0 +compatible_printers_condition = nozzle_diameter[0]==0.4 + +[print:0.06mm SUPERFINE @Infinity3D_DEV_200] +inherits = *common* +layer_height = 0.06 +bottom_solid_layers = 12 +top_solid_layers = 12 +top_solid_min_thickness = 0.72 +bottom_solid_min_thickness = 0.72 +bridge_speed = 45 +infill_speed = 60 +perimeter_speed = 50 +support_material_speed = 50 +max_print_speed = 60 +skirt_distance = 10 +first_layer_speed = 80% +first_layer_extrusion_width = 0.45 +perimeter_extrusion_width = 0.4 +external_perimeter_extrusion_width = 0.4 +infill_extrusion_width = 0.4 +solid_infill_extrusion_width = 0.4 +top_infill_extrusion_width = 0.4 +compatible_printers_condition = nozzle_diameter[0]==0.4 and printer_model=="DEV-200" + +[print:0.10mm Fine @Infinity3D_DEV_200] +inherits = *common* +layer_height = 0.10 +top_solid_layers = 8 +bottom_solid_layers = 8 +bridge_speed = 45 +infill_speed = 60 +perimeter_speed = 50 +support_material_speed = 50 +max_print_speed = 60 +skirt_distance = 10 +first_layer_speed = 80% +first_layer_extrusion_width = 0.45 +perimeter_extrusion_width = 0.4 +external_perimeter_extrusion_width = 0.4 +infill_extrusion_width = 0.4 +solid_infill_extrusion_width = 0.4 +top_infill_extrusion_width = 0.4 +compatible_printers_condition = nozzle_diameter[0]==0.4 and printer_model=="DEV-200" + +[print:0.20mm GOOD @Infinity3D_DEV_200] +inherits = *common* +layer_height = 0.20 +top_solid_layers = 5 +bottom_solid_layers = 5 +bridge_speed = 45 +infill_speed = 60 +perimeter_speed = 50 +support_material_speed = 50 +max_print_speed = 60 +skirt_distance = 10 +first_layer_speed = 80% +first_layer_extrusion_width = 0.45 +perimeter_extrusion_width = 0.4 +external_perimeter_extrusion_width = 0.4 +infill_extrusion_width = 0.4 +solid_infill_extrusion_width = 0.4 +top_infill_extrusion_width = 0.4 +compatible_printers_condition = nozzle_diameter[0]==0.4 and printer_model=="DEV-200" + +[print:0.30mm RAPID @Infinity3D_DEV_200] +inherits = *common* +layer_height = 0.30 +top_solid_layers = 3 +bottom_solid_layers = 3 +bridge_speed = 45 +infill_speed = 60 +perimeter_speed = 50 +support_material_speed = 50 +max_print_speed = 60 +skirt_distance = 10 +first_layer_speed = 80% +compatible_printers_condition = nozzle_diameter[0]==0.4 and printer_model=="DEV-200" + +[print:0.40mm FAST @Infinity3D_DEV_200] +inherits = *common* +layer_height = 0.40 +top_solid_layers = 3 +bottom_solid_layers = 3 +bridge_speed = 45 +infill_speed = 60 +perimeter_speed = 50 +support_material_speed = 50 +max_print_speed = 60 +skirt_distance = 10 +first_layer_speed = 80% +support_material_extrusion_width = 0.45 +compatible_printers_condition = nozzle_diameter[0]==0.4 and printer_model=="DEV-200" + + +[print:0.06mm SUPERFINE @Infinity3D_DEV_350] +inherits = *common* +layer_height = 0.06 +bottom_solid_layers = 12 +top_solid_layers = 12 +top_solid_min_thickness = 0.72 +bottom_solid_min_thickness = 0.72 +bridge_speed = 45 +infill_speed = 60 +perimeter_speed = 50 +support_material_speed = 50 +max_print_speed = 60 +skirt_distance = 10 +first_layer_speed = 80% +first_layer_extrusion_width = 0.45 +perimeter_extrusion_width = 0.4 +external_perimeter_extrusion_width = 0.4 +infill_extrusion_width = 0.4 +solid_infill_extrusion_width = 0.4 +top_infill_extrusion_width = 0.4 +compatible_printers_condition = nozzle_diameter[0]==0.4 and printer_model=="DEV-350" + +[print:0.10mm Fine @Infinity3D_DEV_350] +inherits = *common* +layer_height = 0.10 +top_solid_layers = 8 +bottom_solid_layers = 8 +bridge_speed = 45 +infill_speed = 60 +perimeter_speed = 50 +support_material_speed = 50 +max_print_speed = 60 +skirt_distance = 10 +first_layer_speed = 80% +first_layer_extrusion_width = 0.45 +perimeter_extrusion_width = 0.4 +external_perimeter_extrusion_width = 0.4 +infill_extrusion_width = 0.4 +solid_infill_extrusion_width = 0.4 +top_infill_extrusion_width = 0.4 +compatible_printers_condition = nozzle_diameter[0]==0.4 and printer_model=="DEV-350" + +[print:0.20mm GOOD @Infinity3D_DEV_350] +inherits = *common* +layer_height = 0.20 +top_solid_layers = 5 +bottom_solid_layers = 5 +bridge_speed = 45 +infill_speed = 60 +perimeter_speed = 50 +support_material_speed = 50 +max_print_speed = 60 +skirt_distance = 10 +first_layer_speed = 80% +first_layer_extrusion_width = 0.45 +perimeter_extrusion_width = 0.4 +external_perimeter_extrusion_width = 0.4 +infill_extrusion_width = 0.4 +solid_infill_extrusion_width = 0.4 +top_infill_extrusion_width = 0.4 +compatible_printers_condition = nozzle_diameter[0]==0.4 and printer_model=="DEV-350" + +[print:0.30mm RAPID @Infinity3D_DEV_350] +inherits = *common* +layer_height = 0.30 +top_solid_layers = 3 +bottom_solid_layers = 3 +bridge_speed = 45 +infill_speed = 60 +perimeter_speed = 50 +support_material_speed = 50 +max_print_speed = 60 +skirt_distance = 10 +first_layer_speed = 80% +compatible_printers_condition = nozzle_diameter[0]==0.4 and printer_model=="DEV-350" + +[print:0.40mm FAST @Infinity3D_DEV_350] +inherits = *common* +layer_height = 0.40 +top_solid_layers = 3 +bottom_solid_layers = 3 +bridge_speed = 45 +infill_speed = 60 +perimeter_speed = 50 +support_material_speed = 50 +max_print_speed = 60 +skirt_distance = 10 +first_layer_speed = 80% +support_material_extrusion_width = 0.45 +compatible_printers_condition = nozzle_diameter[0]==0.4 and printer_model=="DEV-350" + + +[filament:*common*] +bed_temperature = 60 +bridge_fan_speed = 100 +compatible_printers = +compatible_printers_condition = +compatible_prints = +compatible_prints_condition = +cooling = 1 +disable_fan_first_layers = 2 +end_filament_gcode = "; Filament-specific end gcode \n;END gcode for filament\n" +extrusion_multiplier = 1 +fan_always_on = 1 +fan_below_layer_time = 60 +filament_colour = #29B2B2 +filament_cooling_final_speed = 3.4 +filament_cooling_initial_speed = 2.2 +filament_cooling_moves = 4 +filament_cost = 0 +filament_density = 0 +filament_deretract_speed = nil +filament_diameter = 1.75 +filament_load_time = 0 +filament_loading_speed = 28 +filament_loading_speed_start = 3 +filament_max_volumetric_speed = 0 +filament_minimal_purge_on_wipe_tower = 15 +filament_notes = "" +filament_ramming_parameters = "120 100 6.6 6.8 7.2 7.6 7.9 8.2 8.7 9.4 9.9 10.0| 0.05 6.6 0.45 6.8 0.95 7.8 1.45 8.3 1.95 9.7 2.45 10 2.95 7.6 3.45 7.6 3.95 7.6 4.45 7.6 4.95 7.6" +filament_retract_before_travel = nil +filament_retract_before_wipe = nil +filament_retract_layer_change = nil +filament_retract_length = nil +filament_retract_lift = nil +filament_retract_lift_above = nil +filament_retract_lift_below = nil +filament_retract_restart_extra = nil +filament_retract_speed = nil +filament_settings_id = "" +filament_soluble = 0 +filament_spool_weight = 0 +filament_toolchange_delay = 0 +filament_type = PLA +filament_unload_time = 0 +filament_unloading_speed = 90 +filament_unloading_speed_start = 100 +filament_wipe = nil +first_layer_bed_temperature = 60 +first_layer_temperature = 210 +full_fan_speed_layer = 0 +inherits = +max_fan_speed = 100 +min_fan_speed = 35 +min_print_speed = 10 +slowdown_below_layer_time = 5 +start_filament_gcode = "; Filament gcode\n" +temperature = 210 + +[filament:*PLA*] +inherits = *common* +bed_temperature = 60 +fan_below_layer_time = 100 +filament_colour = #DDDDDD +filament_max_volumetric_speed = 15 +filament_type = PLA +filament_density = 1.24 +filament_cost = 20 +first_layer_bed_temperature = 60 +first_layer_temperature = 210 +fan_always_on = 1 +cooling = 1 +max_fan_speed = 100 +min_fan_speed = 100 +bridge_fan_speed = 100 +disable_fan_first_layers = 1 +temperature = 205 + +[filament:*PET*] +inherits = *common* +bed_temperature = 70 +cooling = 1 +disable_fan_first_layers = 3 +fan_below_layer_time = 20 +filament_colour = #DDDDDD +filament_max_volumetric_speed = 8 +filament_type = PETG +filament_density = 1.27 +filament_cost = 30 +first_layer_bed_temperature = 70 +first_layer_temperature = 240 +fan_always_on = 1 +max_fan_speed = 50 +min_fan_speed = 20 +bridge_fan_speed = 100 +temperature = 240 + +[filament:*ABS*] +inherits = *common* +bed_temperature = 100 +cooling = 0 +disable_fan_first_layers = 3 +fan_below_layer_time = 20 +filament_colour = #DDDDDD +filament_max_volumetric_speed = 11 +filament_type = ABS +filament_density = 1.04 +filament_cost = 20 +first_layer_bed_temperature = 100 +first_layer_temperature = 245 +fan_always_on = 0 +max_fan_speed = 0 +min_fan_speed = 0 +bridge_fan_speed = 30 +top_fan_speed = 0 +temperature = 245 + +[filament:Generic PLA @Infinity3D] +inherits = *PLA* +renamed_from = "Generic PLA @Infinity3D" +filament_vendor = Generic + +[filament:Generic PETG @Infinity3D] +inherits = *PET* +renamed_from = "Generic PETG @Infinity3D" +filament_vendor = Generic + +[filament:Generic ABS @Infinity3D] +inherits = *ABS* +renamed_from = "Generic ABS @Infinity3D" +first_layer_bed_temperature = 90 +bed_temperature = 90 +filament_vendor = Generic + +[filament:Infinity3D PLA @Infinity3D] +inherits = *PLA* +renamed_from = "Infinity3D PLA @Infinity3D" +filament_vendor = Infinity3D +temperature = 200 +bed_temperature = 60 +first_layer_temperature = 205 +first_layer_bed_temperature = 60 +filament_colour = #42BDD8 + +[filament:Infinity3D PETG @Infinity3D] +inherits = *PET* +renamed_from = "Infinity3D PETG @Infinity3D" +filament_vendor = Infinity3D +temperature = 240 +bed_temperature = 70 +first_layer_temperature = 240 +first_layer_bed_temperature = 70 +max_fan_speed = 40 +min_fan_speed = 20 +filament_colour = #42BDD8 + +[filament:Infinity3D ABS @Infinity3D] +inherits = *ABS* +renamed_from = "Infinity3D ABS @Infinity3D" +filament_vendor = Infinity3D +temperature = 240 +bed_temperature = 90 +first_layer_temperature = 240 +first_layer_bed_temperature = 90 +filament_colour = #42BDD8 + +[filament:Prusament PLA @Infinity3D] +inherits = *PLA* +renamed_from = "Prusament PLA @Infinity3D" +filament_vendor = Prusa Polymers +temperature = 210 +bed_temperature = 60 +first_layer_temperature = 215 +first_layer_bed_temperature = 60 +filament_cost = 24.99 +filament_density = 1.24 +filament_colour = #F94D0C + +[filament:Prusament PETG @Infinity3D] +inherits = *PET* +renamed_from = "Prusament PETG @Infinity3D" +filament_vendor = Prusa Polymers +temperature = 245 +bed_temperature = 70 +first_layer_temperature = 245 +first_layer_bed_temperature = 70 +filament_cost = 24.99 +filament_density = 1.27 +filament_colour = #F94D0C + +[filament:AzureFilm PLA @Infinity3D] +inherits = *PLA* +filament_vendor = AzureFilm +temperature = 210 +bed_temperature = 60 +first_layer_temperature = 215 +first_layer_bed_temperature = 60 +filament_cost = 19.97 +filament_density = 1.24 +filament_colour = #006AA6 + +[filament:Devil Design PLA @Infinity3D] +inherits = *PLA* +filament_vendor = Devil Design +temperature = 215 +bed_temperature = 60 +first_layer_temperature = 215 +first_layer_bed_temperature = 60 +filament_cost = 19.00 +filament_density = 1.24 +filament_colour = #FF0000 +filament_spool_weight = 256 + +[filament:Devil Design PLA Matt @Infinity3D] +inherits = *PLA* +filament_vendor = Devil Design +temperature = 205 +bed_temperature = 60 +first_layer_temperature = 205 +first_layer_bed_temperature = 60 +filament_cost = 20.00 +filament_density = 1.38 +filament_colour = #FF0000 +filament_spool_weight = 256 + +[filament:Devil Design PLA Galaxy @Infinity3D] +inherits = *PLA* +renamed_from = "Devil Design PLA (Galaxy) @Infinity3D" +filament_vendor = Devil Design +temperature = 225 +bed_temperature = 65 +first_layer_temperature = 225 +first_layer_bed_temperature = 65 +filament_cost = 19.00 +filament_density = 1.24 +filament_colour = #FF0000 +filament_spool_weight = 256 + +[filament:Extrudr PLA NX2 @Infinity3D] +inherits = *PLA* +filament_vendor = Extrudr +temperature = 200 +bed_temperature = 60 +first_layer_temperature = 205 +first_layer_bed_temperature = 60 +filament_cost = 23.63 +filament_density = 1.3 +filament_colour = #3C4547 +filament_spool_weight = 256 + +[filament:Extrudr GreenTEC Pro @Infinity3D] +inherits = *PLA* +filament_vendor = Extrudr +temperature = 210 +bed_temperature = 60 +first_layer_temperature = 215 +first_layer_bed_temperature = 60 +filament_cost = 56.24 +filament_density = 1.35 +filament_colour = #3C4547 + +[filament:Real Filament PLA @Infinity3D] +inherits = *PLA* +filament_vendor = Real Filament +temperature = 195 +bed_temperature = 60 +first_layer_temperature = 200 +first_layer_bed_temperature = 60 +filament_cost = 24.99 +filament_density = 1.24 +filament_colour = #007ABF + +[filament:Velleman PLA @Infinity3D] +inherits = *PLA* +filament_vendor = Velleman +temperature = 200 +bed_temperature = 60 +first_layer_temperature = 205 +first_layer_bed_temperature = 60 +filament_cost = 27.99 +filament_density = 1.24 +filament_colour = #7EA60D + +[filament:3DJAKE ecoPLA @Infinity3D] +inherits = *PLA* +filament_vendor = 3DJAKE +temperature = 200 +bed_temperature = 60 +first_layer_temperature = 205 +first_layer_bed_temperature = 60 +filament_cost = 21.99 +filament_density = 1.24 +filament_colour = #125467 +filament_spool_weight = 238 + +[filament:3DJAKE ecoPLA Matt @Infinity3D] +inherits = *PLA* +filament_vendor = 3DJAKE +temperature = 195 +bed_temperature = 60 +first_layer_temperature = 195 +first_layer_bed_temperature = 60 +filament_cost = 24.99 +filament_density = 1.38 +filament_colour = #125467 +filament_spool_weight = 238 + +[filament:3DJAKE ecoPLA Tough @Infinity3D] +inherits = *PLA* +filament_vendor = 3DJAKE +temperature = 215 +bed_temperature = 60 +first_layer_temperature = 215 +first_layer_bed_temperature = 60 +filament_cost = 29.99 +filament_density = 1.21 +filament_colour = #125467 + +[filament:FormFutura Tough PLA @Infinity3D] +inherits = *PLA* +filament_vendor = FormFutura +temperature = 215 +bed_temperature = 60 +first_layer_temperature = 215 +first_layer_bed_temperature = 60 +filament_cost = 46.65 +filament_density = 1.21 +filament_colour = #ed000e + +[filament:123-3D Jupiter PLA @Infinity3D] +inherits = *PLA* +filament_vendor = 123-3D +temperature = 200 +bed_temperature = 60 +first_layer_temperature = 205 +first_layer_bed_temperature = 60 +filament_cost = 19.50 +filament_density = 1.24 +filament_colour = #FFE200 + +[filament:Das Filament PLA @Infinity3D] +inherits = *PLA* +filament_vendor = Das Filament +temperature = 210 +bed_temperature = 60 +first_layer_temperature = 215 +first_layer_bed_temperature = 60 +filament_cost = 20.56 +filament_density = 1.24 +filament_colour = #C7F935 + +[filament:Das Filament PETG @Infinity3D] +inherits = *PET* +filament_vendor = Das Filament +temperature = 240 +bed_temperature = 70 +first_layer_temperature = 240 +first_layer_bed_temperature = 70 +filament_cost = 27.44 +filament_density = 1.29 +filament_colour = #C7F935 + +[filament:Verbatim PLA @Infinity3D] +inherits = *PLA* +filament_vendor = Verbatim +temperature = 205 +bed_temperature = 60 +first_layer_temperature = 210 +first_layer_bed_temperature = 60 +filament_cost = 22.99 +filament_density = 1.24 +filament_colour = #001ca8 + +# Common printer preset +[printer:*common*] +bed_shape = 0x0,350x0,350x350,0x350 +color_change_gcode = M600 +cooling_tube_length = 5 +cooling_tube_retraction = 91.5 +default_filament_profile = "" +default_print_profile = +end_gcode = ;End GCode begin\nM140 S0 ;Heated bed heater off\nM104 S0 ;Extruder heater off\nG90 ;absolute positioning\nG92 E0 ;Retract the filament\nG1 E-1 F300 ;retract the filament a bit before lifting the nozzle, to release some of the pressure\nG1 Z350 E-1 F3000 ;move Z up a bit and retract filament even more\nG1 X0 F3000 ;move X to min endstops, so the head is out of the way\nG1 Y350 F3000 ;so the head is out of the way and Plate is moved forward\nM84 ;stepper off\nM107 ; fan off\nM82 ; absolute extrusion\n;End GCode end +extra_loading_move = -2 +extruder_colour = "" +extruder_offset = 0x0 +gcode_flavor = marlin +high_current_on_filament_swap = 0 +machine_limits_usage = time_estimate_only +machine_max_acceleration_e = 10000 +machine_max_acceleration_extruding = 1000 +machine_max_acceleration_retracting = 1000 +machine_max_acceleration_travel = 1500 +machine_max_acceleration_x = 3000 +machine_max_acceleration_y = 3000 +machine_max_acceleration_z = 100 +machine_max_feedrate_e = 25 +machine_max_feedrate_x = 150 +machine_max_feedrate_y = 150 +machine_max_feedrate_z = 50 +machine_max_jerk_e = 2.5 +machine_max_jerk_x = 10 +machine_max_jerk_y = 10 +machine_max_jerk_z = 0.2 +machine_min_extruding_rate = 0 +machine_min_travel_rate = 0 +max_layer_height = 0.4 +max_print_height = 350 +min_layer_height = 0.08 +nozzle_diameter = 0.4 +parking_pos_retraction = 92 +pause_print_gcode = +printer_technology = FFF +remaining_times = 0 +retract_before_travel = 2 +retract_before_wipe = 0% +retract_layer_change = 1 +retract_length = 2 +retract_length_toolchange = 10 +retract_lift = 0 +retract_lift_above = 0 +retract_lift_below = 328 +retract_restart_extra = 0 +retract_restart_extra_toolchange = 0 +retract_speed = 60 +deretract_speed = 40 +silent_mode = 0 +single_extruder_multi_material = 0 +start_gcode = Start GCode begin\nM140 S[first_layer_bed_temperature] ;Start Warming Bed\nM104 S[first_layer_temperature] ;Preheat\nG28 ;home\nG29 ;Auto Bed-level\nG90 ;absolute positioning\nG1 X-10 Y-10 F3000 ;Move to corner\nM190 S[first_layer_bed_temperature] ;Wait For Bed Temperature\nM109 S[first_layer_temperature] ;Wait for Hotend Temperature\nG92 E0 ;Zero set extruder position\nG1 E3 F200 ;Feed filament to clear nozzle\nG92 E0 ;Zero set extruder position +thumbnails = 16x16,220x124 +use_firmware_retraction = 0 +use_relative_e_distances = 0 +use_volumetric_e = 0 +variable_layer_height = 1 +wipe = 0 +z_offset = 0 + +[printer:Infinity3D DEV-350] +inherits = *common* +printer_model = DEV-350 +printer_variant = 0.4 +default_filament_profile = Generic PLA @Infinity3D +default_print_profile = 0.20mm GOOD @Infinity3D_DEV_350 + +[printer:Infinity3D DEV-200] +inherits = *common* +printer_model = DEV-200 +printer_variant = 0.4 +bed_shape = 0x0,200x0,200x200,0x200 +thumbnails = +variable_layer_height = 0 +retract_lift_below = 0 +max_print_height = 235 +start_gcode = Start GCode begin\nM140 S[first_layer_bed_temperature] ;Start Warming Bed\nM104 S[first_layer_temperature] ;Preheat\nG28 ;home\nG29 ;Auto Bed-level\nG90 ;absolute positioning\nG1 X-10 Y-10 F3000 ;Move to corner\nM190 S[first_layer_bed_temperature] ;Wait For Bed Temperature\nM109 S[first_layer_temperature] ;Wait for Hotend Temperature\nG92 E0 ;Zero set extruder position\nG1 E3 F200 ;Feed filament to clear nozzle\nG92 E0 ;Zero set extruder position +end_gcode = ;End GCode begin\nM140 S0 ;Heated bed heater off\nM104 S0 ;Extruder heater off\nG90 ;absolute positioning\nG92 E0 ;Retract the filament\nG1 E-1 F300 ;retract the filament a bit before lifting the nozzle, to release some of the pressure\nG1 Z240 E-1 F3000 ;move Z up a bit and retract filament even more\nG1 X0 F3000 ;move X to min endstops, so the head is out of the way\nG1 Y200 F3000 ;so the head is out of the way and Plate is moved forward\nM84 ;stepper off\nM107 ; fan off\nM82 ; absolute extrusion\n;End GCode end +default_filament_profile = Generic PLA @Infinity3D +default_print_profile = 0.20mm GOOD @Infinity3D_DEV_200 diff --git a/resources/profiles/Infinity3D/DEV-200_thumbnail.png b/resources/profiles/Infinity3D/DEV-200_thumbnail.png new file mode 100644 index 000000000..937944773 Binary files /dev/null and b/resources/profiles/Infinity3D/DEV-200_thumbnail.png differ diff --git a/resources/profiles/Infinity3D/DEV-350_thumbnail.png b/resources/profiles/Infinity3D/DEV-350_thumbnail.png new file mode 100644 index 000000000..02ae09fa1 Binary files /dev/null and b/resources/profiles/Infinity3D/DEV-350_thumbnail.png differ diff --git a/resources/profiles/Infinity3D/DEV_200_bed.stl b/resources/profiles/Infinity3D/DEV_200_bed.stl new file mode 100644 index 000000000..b07905150 Binary files /dev/null and b/resources/profiles/Infinity3D/DEV_200_bed.stl differ diff --git a/resources/profiles/Infinity3D/DEV_200_texture.svg b/resources/profiles/Infinity3D/DEV_200_texture.svg new file mode 100644 index 000000000..c2ee3a68b --- /dev/null +++ b/resources/profiles/Infinity3D/DEV_200_texture.svg @@ -0,0 +1,487 @@ + + + + + + + + diff --git a/resources/profiles/Infinity3D/DEV_350_bed.stl b/resources/profiles/Infinity3D/DEV_350_bed.stl new file mode 100644 index 000000000..7250f3867 Binary files /dev/null and b/resources/profiles/Infinity3D/DEV_350_bed.stl differ diff --git a/resources/profiles/Infinity3D/DEV_350_texture.svg b/resources/profiles/Infinity3D/DEV_350_texture.svg new file mode 100644 index 000000000..bbfde73d2 --- /dev/null +++ b/resources/profiles/Infinity3D/DEV_350_texture.svg @@ -0,0 +1,589 @@ + + + + + + + + diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 801760b8c..8a093f639 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -105,7 +105,10 @@ if (SLIC3R_GUI) # wrong libs for opengl in the link line and it does not link to it by himself. # libslic3r_gui will link to opengl anyway, so lets override wx list(FILTER wxWidgets_LIBRARIES EXCLUDE REGEX OpenGL) - + + if (UNIX AND NOT APPLE) + list(APPEND wxWidgets_LIBRARIES X11 wayland-client wayland-egl EGL) + endif () # list(REMOVE_ITEM wxWidgets_LIBRARIES oleacc) message(STATUS "wx libs: ${wxWidgets_LIBRARIES}") diff --git a/src/PrusaSlicer_app_msvc.cpp b/src/PrusaSlicer_app_msvc.cpp index 2ccf2f1ff..83f24c307 100644 --- a/src/PrusaSlicer_app_msvc.cpp +++ b/src/PrusaSlicer_app_msvc.cpp @@ -66,16 +66,25 @@ public: return this->success; } - void unload_opengl_dll() + bool unload_opengl_dll() { - if (this->hOpenGL) { - BOOL released = FreeLibrary(this->hOpenGL); - if (released) - printf("System OpenGL library released\n"); + if (this->hOpenGL != nullptr) { + if (::FreeLibrary(this->hOpenGL) != FALSE) { + if (::GetModuleHandle(L"opengl32.dll") == nullptr) { + printf("System OpenGL library successfully released\n"); + this->hOpenGL = nullptr; + return true; + } + else + printf("System OpenGL library released but not removed\n"); + } else printf("System OpenGL library NOT released\n"); - this->hOpenGL = nullptr; + + return false; } + + return true; } bool is_version_greater_or_equal_to(unsigned int major, unsigned int minor) const @@ -270,20 +279,26 @@ int wmain(int argc, wchar_t **argv) // https://wiki.qt.io/Cross_compiling_Mesa_for_Windows // http://download.qt.io/development_releases/prebuilt/llvmpipe/windows/ if (load_mesa) { - opengl_version_check.unload_opengl_dll(); - wchar_t path_to_mesa[MAX_PATH + 1] = { 0 }; - wcscpy(path_to_mesa, path_to_exe); - wcscat(path_to_mesa, L"mesa\\opengl32.dll"); - printf("Loading MESA OpenGL library: %S\n", path_to_mesa); - HINSTANCE hInstance_OpenGL = LoadLibraryExW(path_to_mesa, nullptr, 0); - if (hInstance_OpenGL == nullptr) { - printf("MESA OpenGL library was not loaded\n"); - } else - printf("MESA OpenGL library was loaded sucessfully\n"); + bool res = opengl_version_check.unload_opengl_dll(); + if (!res) { + MessageBox(nullptr, L"PrusaSlicer was unable to automatically switch to MESA OpenGL library\nPlease, try to run the application using the '--sw-renderer' option.\n", + L"PrusaSlicer Warning", MB_OK); + return -1; + } + else { + wchar_t path_to_mesa[MAX_PATH + 1] = { 0 }; + wcscpy(path_to_mesa, path_to_exe); + wcscat(path_to_mesa, L"mesa\\opengl32.dll"); + printf("Loading MESA OpenGL library: %S\n", path_to_mesa); + HINSTANCE hInstance_OpenGL = LoadLibraryExW(path_to_mesa, nullptr, 0); + if (hInstance_OpenGL == nullptr) + printf("MESA OpenGL library was not loaded\n"); + else + printf("MESA OpenGL library was loaded sucessfully\n"); + } } #endif /* SLIC3R_GUI */ - wchar_t path_to_slic3r[MAX_PATH + 1] = { 0 }; wcscpy(path_to_slic3r, path_to_exe); wcscat(path_to_slic3r, L"PrusaSlicer.dll"); diff --git a/src/clipper/clipper.cpp b/src/clipper/clipper.cpp index 84d68b1e6..84109398a 100644 --- a/src/clipper/clipper.cpp +++ b/src/clipper/clipper.cpp @@ -2290,6 +2290,12 @@ void Clipper::ProcessHorizontal(TEdge *horzEdge) if (horzEdge->OutIdx >= 0 && !IsOpen) //note: may be done multiple times { +#ifdef CLIPPERLIB_USE_XYZ + if (dir == dLeftToRight) + SetZ(e->Curr, *horzEdge, *e); + else + SetZ(e->Curr, *e, *horzEdge); +#endif op1 = AddOutPt(horzEdge, e->Curr); TEdge* eNextHorz = m_SortedEdges; while (eNextHorz) @@ -2614,6 +2620,9 @@ void Clipper::ProcessEdgesAtTopOfScanbeam(const cInt topY) { e->Curr.x() = TopX( *e, topY ); e->Curr.y() = topY; +#ifdef CLIPPERLIB_USE_XYZ + e->Curr.z() = topY == e->Top.y() ? e->Top.z() : (topY == e->Bot.y() ? e->Bot.z() : 0); +#endif } //When StrictlySimple and 'e' is being touched by another edge, then diff --git a/src/libslic3r/AABBTreeIndirect.hpp b/src/libslic3r/AABBTreeIndirect.hpp index 014b49af1..f2847579c 100644 --- a/src/libslic3r/AABBTreeIndirect.hpp +++ b/src/libslic3r/AABBTreeIndirect.hpp @@ -446,6 +446,57 @@ namespace detail { } } + // Real-time collision detection, Ericson, Chapter 5 + template + static inline Vector closest_point_to_triangle(const Vector &p, const Vector &a, const Vector &b, const Vector &c) + { + using Scalar = typename Vector::Scalar; + // Check if P in vertex region outside A + Vector ab = b - a; + Vector ac = c - a; + Vector ap = p - a; + Scalar d1 = ab.dot(ap); + Scalar d2 = ac.dot(ap); + if (d1 <= 0 && d2 <= 0) + return a; + // Check if P in vertex region outside B + Vector bp = p - b; + Scalar d3 = ab.dot(bp); + Scalar d4 = ac.dot(bp); + if (d3 >= 0 && d4 <= d3) + return b; + // Check if P in edge region of AB, if so return projection of P onto AB + Scalar vc = d1*d4 - d3*d2; + if (a != b && vc <= 0 && d1 >= 0 && d3 <= 0) { + Scalar v = d1 / (d1 - d3); + return a + v * ab; + } + // Check if P in vertex region outside C + Vector cp = p - c; + Scalar d5 = ab.dot(cp); + Scalar d6 = ac.dot(cp); + if (d6 >= 0 && d5 <= d6) + return c; + // Check if P in edge region of AC, if so return projection of P onto AC + Scalar vb = d5*d2 - d1*d6; + if (vb <= 0 && d2 >= 0 && d6 <= 0) { + Scalar w = d2 / (d2 - d6); + return a + w * ac; + } + // Check if P in edge region of BC, if so return projection of P onto BC + Scalar va = d3*d6 - d5*d4; + if (va <= 0 && (d4 - d3) >= 0 && (d5 - d6) >= 0) { + Scalar w = (d4 - d3) / ((d4 - d3) + (d5 - d6)); + return b + w * (c - b); + } + // P inside face region. Compute Q through its barycentric coordinates (u,v,w) + Scalar denom = Scalar(1.0) / (va + vb + vc); + Scalar v = vb * denom; + Scalar w = vc * denom; + return a + ab * v + ac * w; // = u*a + v*b + w*c, u = va * denom = 1.0-v-w + }; + + // Nothing to do with COVID-19 social distancing. template struct IndexedTriangleSetDistancer { @@ -453,74 +504,36 @@ namespace detail { using IndexedFaceType = AIndexedFaceType; using TreeType = ATreeType; using VectorType = AVectorType; + using ScalarType = typename VectorType::Scalar; const std::vector &vertices; const std::vector &faces; const TreeType &tree; const VectorType origin; + + inline VectorType closest_point_to_origin(size_t primitive_index, + ScalarType& squared_distance){ + const auto &triangle = this->faces[primitive_index]; + VectorType closest_point = closest_point_to_triangle(origin, + this->vertices[triangle(0)].template cast(), + this->vertices[triangle(1)].template cast(), + this->vertices[triangle(2)].template cast()); + squared_distance = (origin - closest_point).squaredNorm(); + return closest_point; + } }; - // Real-time collision detection, Ericson, Chapter 5 - template - static inline Vector closest_point_to_triangle(const Vector &p, const Vector &a, const Vector &b, const Vector &c) - { - using Scalar = typename Vector::Scalar; - // Check if P in vertex region outside A - Vector ab = b - a; - Vector ac = c - a; - Vector ap = p - a; - Scalar d1 = ab.dot(ap); - Scalar d2 = ac.dot(ap); - if (d1 <= 0 && d2 <= 0) - return a; - // Check if P in vertex region outside B - Vector bp = p - b; - Scalar d3 = ab.dot(bp); - Scalar d4 = ac.dot(bp); - if (d3 >= 0 && d4 <= d3) - return b; - // Check if P in edge region of AB, if so return projection of P onto AB - Scalar vc = d1*d4 - d3*d2; - if (a != b && vc <= 0 && d1 >= 0 && d3 <= 0) { - Scalar v = d1 / (d1 - d3); - return a + v * ab; - } - // Check if P in vertex region outside C - Vector cp = p - c; - Scalar d5 = ab.dot(cp); - Scalar d6 = ac.dot(cp); - if (d6 >= 0 && d5 <= d6) - return c; - // Check if P in edge region of AC, if so return projection of P onto AC - Scalar vb = d5*d2 - d1*d6; - if (vb <= 0 && d2 >= 0 && d6 <= 0) { - Scalar w = d2 / (d2 - d6); - return a + w * ac; - } - // Check if P in edge region of BC, if so return projection of P onto BC - Scalar va = d3*d6 - d5*d4; - if (va <= 0 && (d4 - d3) >= 0 && (d5 - d6) >= 0) { - Scalar w = (d4 - d3) / ((d4 - d3) + (d5 - d6)); - return b + w * (c - b); - } - // P inside face region. Compute Q through its barycentric coordinates (u,v,w) - Scalar denom = Scalar(1.0) / (va + vb + vc); - Scalar v = vb * denom; - Scalar w = vc * denom; - return a + ab * v + ac * w; // = u*a + v*b + w*c, u = va * denom = 1.0-v-w - }; - - template - static inline Scalar squared_distance_to_indexed_triangle_set_recursive( - IndexedTriangleSetDistancerType &distancer, + template + static inline Scalar squared_distance_to_indexed_primitives_recursive( + IndexedPrimitivesDistancerType &distancer, size_t node_idx, Scalar low_sqr_d, Scalar up_sqr_d, size_t &i, - Eigen::PlainObjectBase &c) + Eigen::PlainObjectBase &c) { - using Vector = typename IndexedTriangleSetDistancerType::VectorType; + using Vector = typename IndexedPrimitivesDistancerType::VectorType; if (low_sqr_d > up_sqr_d) return low_sqr_d; @@ -538,13 +551,9 @@ namespace detail { assert(node.is_valid()); if (node.is_leaf()) { - const auto &triangle = distancer.faces[node.idx]; - Vector c_candidate = closest_point_to_triangle( - distancer.origin, - distancer.vertices[triangle(0)].template cast(), - distancer.vertices[triangle(1)].template cast(), - distancer.vertices[triangle(2)].template cast()); - set_min((c_candidate - distancer.origin).squaredNorm(), node.idx, c_candidate); + Scalar sqr_dist; + Vector c_candidate = distancer.closest_point_to_origin(node.idx, sqr_dist); + set_min(sqr_dist, node.idx, c_candidate); } else { @@ -561,7 +570,7 @@ namespace detail { { size_t i_left; Vector c_left = c; - Scalar sqr_d_left = squared_distance_to_indexed_triangle_set_recursive(distancer, left_node_idx, low_sqr_d, up_sqr_d, i_left, c_left); + Scalar sqr_d_left = squared_distance_to_indexed_primitives_recursive(distancer, left_node_idx, low_sqr_d, up_sqr_d, i_left, c_left); set_min(sqr_d_left, i_left, c_left); looked_left = true; }; @@ -569,13 +578,13 @@ namespace detail { { size_t i_right; Vector c_right = c; - Scalar sqr_d_right = squared_distance_to_indexed_triangle_set_recursive(distancer, right_node_idx, low_sqr_d, up_sqr_d, i_right, c_right); + Scalar sqr_d_right = squared_distance_to_indexed_primitives_recursive(distancer, right_node_idx, low_sqr_d, up_sqr_d, i_right, c_right); set_min(sqr_d_right, i_right, c_right); looked_right = true; }; // must look left or right if in box - using BBoxScalar = typename IndexedTriangleSetDistancerType::TreeType::BoundingBox::Scalar; + using BBoxScalar = typename IndexedPrimitivesDistancerType::TreeType::BoundingBox::Scalar; if (node_left.bbox.contains(distancer.origin.template cast())) look_left(); if (node_right.bbox.contains(distancer.origin.template cast())) @@ -747,7 +756,7 @@ inline typename VectorType::Scalar squared_distance_to_indexed_triangle_set( auto distancer = detail::IndexedTriangleSetDistancer { vertices, faces, tree, point }; return tree.empty() ? Scalar(-1) : - detail::squared_distance_to_indexed_triangle_set_recursive(distancer, size_t(0), Scalar(0), std::numeric_limits::infinity(), hit_idx_out, hit_point_out); + detail::squared_distance_to_indexed_primitives_recursive(distancer, size_t(0), Scalar(0), std::numeric_limits::infinity(), hit_idx_out, hit_point_out); } // Decides if exists some triangle in defined radius on a 3D indexed triangle set using a pre-built AABBTreeIndirect::Tree. @@ -779,7 +788,7 @@ inline bool is_any_triangle_in_radius( return false; } - detail::squared_distance_to_indexed_triangle_set_recursive(distancer, size_t(0), Scalar(0), max_distance_squared, hit_idx, hit_point); + detail::squared_distance_to_indexed_primitives_recursive(distancer, size_t(0), Scalar(0), max_distance_squared, hit_idx, hit_point); return hit_point.allFinite(); } diff --git a/src/libslic3r/AABBTreeLines.hpp b/src/libslic3r/AABBTreeLines.hpp new file mode 100644 index 000000000..7b9595419 --- /dev/null +++ b/src/libslic3r/AABBTreeLines.hpp @@ -0,0 +1,112 @@ +#ifndef SRC_LIBSLIC3R_AABBTREELINES_HPP_ +#define SRC_LIBSLIC3R_AABBTREELINES_HPP_ + +#include "libslic3r/Point.hpp" +#include "libslic3r/EdgeGrid.hpp" +#include "libslic3r/AABBTreeIndirect.hpp" +#include "libslic3r/Line.hpp" + +namespace Slic3r { + +namespace AABBTreeLines { + +namespace detail { + +template +struct IndexedLinesDistancer { + using LineType = ALineType; + using TreeType = ATreeType; + using VectorType = AVectorType; + using ScalarType = typename VectorType::Scalar; + + const std::vector &lines; + const TreeType &tree; + + const VectorType origin; + + inline VectorType closest_point_to_origin(size_t primitive_index, + ScalarType &squared_distance) { + VectorType nearest_point; + const LineType &line = lines[primitive_index]; + squared_distance = line_alg::distance_to_squared(line, origin, &nearest_point); + return nearest_point; + } +}; + +} + +// Build a balanced AABB Tree over a vector of float lines, balancing the tree +// on centroids of the lines. +// Epsilon is applied to the bounding boxes of the AABB Tree to cope with numeric inaccuracies +// during tree traversal. +template +inline AABBTreeIndirect::Tree<2, typename LineType::Scalar> build_aabb_tree_over_indexed_lines( + const std::vector &lines, + //FIXME do we want to apply an epsilon? + const float eps = 0) + { + using TreeType = AABBTreeIndirect::Tree<2, typename LineType::Scalar>; +// using CoordType = typename TreeType::CoordType; + using VectorType = typename TreeType::VectorType; + using BoundingBox = typename TreeType::BoundingBox; + + struct InputType { + size_t idx() const { + return m_idx; + } + const BoundingBox& bbox() const { + return m_bbox; + } + const VectorType& centroid() const { + return m_centroid; + } + + size_t m_idx; + BoundingBox m_bbox; + VectorType m_centroid; + }; + + std::vector input; + input.reserve(lines.size()); + const VectorType veps(eps, eps); + for (size_t i = 0; i < lines.size(); ++i) { + const LineType &line = lines[i]; + InputType n; + n.m_idx = i; + n.m_centroid = (line.a + line.b) * 0.5; + n.m_bbox = BoundingBox(line.a, line.a); + n.m_bbox.extend(line.b); + n.m_bbox.min() -= veps; + n.m_bbox.max() += veps; + input.emplace_back(n); + } + + TreeType out; + out.build(std::move(input)); + return out; +} + +// Finding a closest line, its closest point and squared distance to the closest point +// Returns squared distance to the closest point or -1 if the input is empty. +template +inline typename VectorType::Scalar squared_distance_to_indexed_lines( + const std::vector &lines, + const TreeType &tree, + const VectorType &point, + size_t &hit_idx_out, + Eigen::PlainObjectBase &hit_point_out) + { + using Scalar = typename VectorType::Scalar; + auto distancer = detail::IndexedLinesDistancer + { lines, tree, point }; + return tree.empty() ? + Scalar(-1) : + AABBTreeIndirect::detail::squared_distance_to_indexed_primitives_recursive(distancer, size_t(0), Scalar(0), + std::numeric_limits::infinity(), hit_idx_out, hit_point_out); +} + +} + +} + +#endif /* SRC_LIBSLIC3R_AABBTREELINES_HPP_ */ diff --git a/src/libslic3r/AStar.hpp b/src/libslic3r/AStar.hpp index 052d0e814..b35b6a4af 100644 --- a/src/libslic3r/AStar.hpp +++ b/src/libslic3r/AStar.hpp @@ -1,11 +1,12 @@ #ifndef ASTAR_HPP #define ASTAR_HPP +#include // std::isinf() is here +#include + #include "libslic3r/Point.hpp" #include "libslic3r/MutablePriorityQueue.hpp" -#include - namespace Slic3r { namespace astar { // Input interface for the Astar algorithm. Specialize this struct for a @@ -34,6 +35,8 @@ template struct TracerTraits_ // Get the estimated distance heuristic from node 'n' to the destination. // This is referred to as the h value in AStar context. // If node 'n' is the goal, this function should return a negative value. + // Note that this heuristic should be admissible (never bigger than the real + // cost) in order for Astar to work. static float goal_heuristic(const T &tracer, const Node &n) { return tracer.goal_heuristic(n); @@ -50,131 +53,136 @@ template struct TracerTraits_ template using TracerNodeT = typename TracerTraits_>::Node; -namespace detail { -// Helper functions dispatching calls through the TracerTraits_ interface +constexpr auto Unassigned = std::numeric_limits::max(); -template using TracerTraits = TracerTraits_>; - -template -void foreach_reachable(const T &tracer, const TracerNodeT &from, Fn &&fn) +template +struct QNode // Queue node. Keeps track of scores g, and h { - TracerTraits::foreach_reachable(tracer, from, fn); -} + TracerNodeT node; // The actual node itself + size_t queue_id; // Position in the open queue or Unassigned if closed + size_t parent; // unique id of the parent or Unassigned -template -float trace_distance(const T &tracer, const TracerNodeT &a, const TracerNodeT &b) -{ - return TracerTraits::distance(tracer, a, b); -} + float g, h; + float f() const { return g + h; } -template -float goal_heuristic(const T &tracer, const TracerNodeT &n) -{ - return TracerTraits::goal_heuristic(tracer, n); -} - -template -size_t unique_id(const T &tracer, const TracerNodeT &n) -{ - return TracerTraits::unique_id(tracer, n); -} - -} // namespace astar_detail + QNode(TracerNodeT n = {}, + size_t p = Unassigned, + float gval = std::numeric_limits::infinity(), + float hval = 0.f) + : node{std::move(n)} + , parent{p} + , queue_id{InvalidQueueID} + , g{gval} + , h{hval} + {} +}; // Run the AStar algorithm on a tracer implementation. // The 'tracer' argument encapsulates the domain (grid, point cloud, etc...) // The 'source' argument is the starting node. // The 'out' argument is the output iterator into which the output nodes are -// written. -// Note that no destination node is given. The tracer's goal_heuristic() method -// should return a negative value if a node is a destination node. -template -bool search_route(const Tracer &tracer, const TracerNodeT &source, It out) +// written. For performance reasons, the order is reverse, from the destination +// to the source -- (destination included, source is not). +// The 'cached_nodes' argument is an optional associative container to hold a +// QNode entry for each visited node. Any compatible container can be used +// (like std::map or maps with different allocators, even a sufficiently large +// std::vector). +// +// Note that no destination node is given in the signature. The tracer's +// goal_heuristic() method should return a negative value if a node is a +// destination node. +template>> +bool search_route(const Tracer &tracer, + const TracerNodeT &source, + It out, + NodeMap &&cached_nodes = {}) { - using namespace detail; + using Node = TracerNodeT; + using QNode = QNode; + using TracerTraits = TracerTraits_>; - using Node = TracerNodeT; - enum class QueueType { Open, Closed, None }; - - struct QNode // Queue node. Keeps track of scores g, and h - { - Node node; // The actual node itself - QueueType qtype = QueueType::None; // Which queue holds this node - - float g = 0.f, h = 0.f; - float f() const { return g + h; } - }; - - // TODO: apply a linear memory allocator - using QMap = std::unordered_map; - - // The traversed nodes are stored here encapsulated in QNodes - QMap cached_nodes; - - struct LessPred { // Comparison functor needed by MutablePriorityQueue - QMap &m; + struct LessPred { // Comparison functor needed by the priority queue + NodeMap &m; bool operator ()(size_t node_a, size_t node_b) { - auto ait = m.find(node_a); - auto bit = m.find(node_b); - assert (ait != m.end() && bit != m.end()); - - return ait->second.f() < bit->second.f(); + return m[node_a].f() < m[node_b].f(); } }; - auto qopen = - make_mutable_priority_queue([](size_t, size_t){}, - LessPred{cached_nodes}); + auto qopen = make_mutable_priority_queue( + [&cached_nodes](size_t el, size_t qidx) { + cached_nodes[el].queue_id = qidx; + }, + LessPred{cached_nodes}); - auto qclosed = - make_mutable_priority_queue([](size_t, size_t){}, - LessPred{cached_nodes}); + QNode initial{source, /*parent = */ Unassigned, /*g = */0.f}; + size_t source_id = TracerTraits::unique_id(tracer, source); + cached_nodes[source_id] = initial; + qopen.push(source_id); - QNode initial{source, QueueType::Open}; - cached_nodes.insert({unique_id(tracer, source), initial}); - qopen.push(unique_id(tracer, source)); + size_t goal_id = TracerTraits::goal_heuristic(tracer, source) < 0.f ? + source_id : + Unassigned; - bool goal_reached = false; - - while (!goal_reached && !qopen.empty()) { + while (goal_id == Unassigned && !qopen.empty()) { size_t q_id = qopen.top(); qopen.pop(); - QNode q = cached_nodes.at(q_id); + QNode &q = cached_nodes[q_id]; - foreach_reachable(tracer, q.node, [&](const Node &nd) { - if (goal_reached) return goal_reached; + // This should absolutely be initialized in the cache already + assert(!std::isinf(q.g)); + + TracerTraits::foreach_reachable(tracer, q.node, [&](const Node &succ_nd) { + if (goal_id != Unassigned) + return true; + + float h = TracerTraits::goal_heuristic(tracer, succ_nd); + float dst = TracerTraits::distance(tracer, q.node, succ_nd); + size_t succ_id = TracerTraits::unique_id(tracer, succ_nd); + QNode qsucc_nd{succ_nd, q_id, q.g + dst, h}; - float h = goal_heuristic(tracer, nd); if (h < 0.f) { - goal_reached = true; + goal_id = succ_id; + cached_nodes[succ_id] = qsucc_nd; } else { - float dst = trace_distance(tracer, q.node, nd); - QNode qnd{nd, QueueType::None, q.g + dst, h}; - size_t qnd_id = unique_id(tracer, nd); + // If succ_id is not in cache, it gets created with g = infinity + QNode &prev_nd = cached_nodes[succ_id]; - auto it = cached_nodes.find(qnd_id); + if (qsucc_nd.g < prev_nd.g) { + // new route is better, apply it: - if (it == cached_nodes.end() || - (it->second.qtype != QueueType::None && qnd.f() < it->second.f())) { - qnd.qtype = QueueType::Open; - cached_nodes.insert_or_assign(qnd_id, qnd); - qopen.push(qnd_id); + // Save the old queue id, it would be lost after the next line + size_t queue_id = prev_nd.queue_id; + + // The cache needs to be updated either way + prev_nd = qsucc_nd; + + if (queue_id == InvalidQueueID) + // was in closed or unqueued, rescheduling + qopen.push(succ_id); + else // was in open, updating + qopen.update(queue_id); } } - return goal_reached; + return goal_id != Unassigned; }); - - q.qtype = QueueType::Closed; - cached_nodes.insert_or_assign(q_id, q); - qclosed.push(q_id); - - // write the output - *out = q.node; - ++out; } - return goal_reached; + // Write the output, do not reverse. Clients can do so if they need to. + if (goal_id != Unassigned) { + const QNode *q = &cached_nodes[goal_id]; + while (q->parent != Unassigned) { + assert(!std::isinf(q->g)); // Uninitialized nodes are NOT allowed + + *out = q->node; + ++out; + q = &cached_nodes[q->parent]; + } + } + + return goal_id != Unassigned; } }} // namespace Slic3r::astar diff --git a/src/libslic3r/AppConfig.cpp b/src/libslic3r/AppConfig.cpp index 928febffd..82c0a72a6 100644 --- a/src/libslic3r/AppConfig.cpp +++ b/src/libslic3r/AppConfig.cpp @@ -230,8 +230,13 @@ static std::string appconfig_md5_hash_line(const std::string_view data) return "# MD5 checksum " + md5_digest_str + "\n"; }; +struct ConfigFileInfo { + bool correct_checksum {false}; + bool contains_null {false}; +}; + // Assume that the last line with the comment inside the config file contains a checksum and that the user didn't modify the config file. -static bool verify_config_file_checksum(boost::nowide::ifstream &ifs) +static ConfigFileInfo check_config_file_and_verify_checksum(boost::nowide::ifstream &ifs) { auto read_whole_config_file = [&ifs]() -> std::string { std::stringstream ss; @@ -240,7 +245,8 @@ static bool verify_config_file_checksum(boost::nowide::ifstream &ifs) }; ifs.seekg(0, boost::nowide::ifstream::beg); - std::string whole_config = read_whole_config_file(); + const std::string whole_config = read_whole_config_file(); + const bool contains_null = whole_config.find_first_of('\0') != std::string::npos; // The checksum should be on the last line in the config file. if (size_t last_comment_pos = whole_config.find_last_of('#'); last_comment_pos != std::string::npos) { @@ -249,9 +255,9 @@ static bool verify_config_file_checksum(boost::nowide::ifstream &ifs) // When the checksum isn't found, the checksum was not saved correctly, it was removed or it is an older config file without the checksum. // If the checksum is incorrect, then the file was either not saved correctly or modified. if (std::string_view(whole_config.c_str() + last_comment_pos, whole_config.size() - last_comment_pos) == appconfig_md5_hash_line({ whole_config.data(), last_comment_pos })) - return true; + return {true, contains_null}; } - return false; + return {false, contains_null}; } #endif @@ -269,14 +275,25 @@ std::string AppConfig::load(const std::string &path) ifs.open(path); #ifdef WIN32 // Verify the checksum of the config file without taking just for debugging purpose. - if (!verify_config_file_checksum(ifs)) - BOOST_LOG_TRIVIAL(info) << "The configuration file " << path << - " has a wrong MD5 checksum or the checksum is missing. This may indicate a file corruption or a harmless user edit."; + const ConfigFileInfo config_file_info = check_config_file_and_verify_checksum(ifs); + if (!config_file_info.correct_checksum) + BOOST_LOG_TRIVIAL(info) + << "The configuration file " << path + << " has a wrong MD5 checksum or the checksum is missing. This may indicate a file corruption or a harmless user edit."; + + if (!config_file_info.correct_checksum && config_file_info.contains_null) { + BOOST_LOG_TRIVIAL(info) << "The configuration file " + path + " is corrupted, because it is contains null characters."; + throw Slic3r::CriticalException("The configuration file contains null characters."); + } ifs.seekg(0, boost::nowide::ifstream::beg); #endif - pt::read_ini(ifs, tree); - } catch (pt::ptree_error& ex) { + try { + pt::read_ini(ifs, tree); + } catch (pt::ptree_error &ex) { + throw Slic3r::CriticalException(ex.what()); + } + } catch (Slic3r::CriticalException &ex) { #ifdef WIN32 // The configuration file is corrupted, try replacing it with the backup configuration. ifs.close(); @@ -284,29 +301,29 @@ std::string AppConfig::load(const std::string &path) if (boost::filesystem::exists(backup_path)) { // Compute checksum of the configuration backup file and try to load configuration from it when the checksum is correct. boost::nowide::ifstream backup_ifs(backup_path); - if (!verify_config_file_checksum(backup_ifs)) { - BOOST_LOG_TRIVIAL(error) << format("Both \"%1%\" and \"%2%\" are corrupted. It isn't possible to restore configuration from the backup.", path, backup_path); + if (const ConfigFileInfo config_file_info = check_config_file_and_verify_checksum(backup_ifs); !config_file_info.correct_checksum || config_file_info.contains_null) { + BOOST_LOG_TRIVIAL(error) << format(R"(Both "%1%" and "%2%" are corrupted. It isn't possible to restore configuration from the backup.)", path, backup_path); backup_ifs.close(); boost::filesystem::remove(backup_path); } else if (std::string error_message; copy_file(backup_path, path, error_message, false) != SUCCESS) { - BOOST_LOG_TRIVIAL(error) << format("Configuration file \"%1%\" is corrupted. Failed to restore from backup \"%2%\": %3%", path, backup_path, error_message); + BOOST_LOG_TRIVIAL(error) << format(R"(Configuration file "%1%" is corrupted. Failed to restore from backup "%2%": %3%)", path, backup_path, error_message); backup_ifs.close(); boost::filesystem::remove(backup_path); } else { - BOOST_LOG_TRIVIAL(info) << format("Configuration file \"%1%\" was corrupted. It has been succesfully restored from the backup \"%2%\".", path, backup_path); + BOOST_LOG_TRIVIAL(info) << format(R"(Configuration file "%1%" was corrupted. It has been successfully restored from the backup "%2%".)", path, backup_path); // Try parse configuration file after restore from backup. try { ifs.open(path); pt::read_ini(ifs, tree); recovered = true; } catch (pt::ptree_error& ex) { - BOOST_LOG_TRIVIAL(info) << format("Failed to parse configuration file \"%1%\" after it has been restored from backup: %2%", path, ex.what()); + BOOST_LOG_TRIVIAL(info) << format(R"(Failed to parse configuration file "%1%" after it has been restored from backup: %2%)", path, ex.what()); } } } else #endif // WIN32 - BOOST_LOG_TRIVIAL(info) << format("Failed to parse configuration file \"%1%\": %2%", path, ex.what()); - if (! recovered) { + BOOST_LOG_TRIVIAL(info) << format(R"(Failed to parse configuration file "%1%": %2%)", path, ex.what()); + if (!recovered) { // Report the initial error of parsing PrusaSlicer.ini. // Error while parsing config file. We'll customize the error message and rethrow to be displayed. // ! But to avoid the use of _utf8 (related to use of wxWidgets) diff --git a/src/libslic3r/BuildVolume.cpp b/src/libslic3r/BuildVolume.cpp index c580e6f87..899055355 100644 --- a/src/libslic3r/BuildVolume.cpp +++ b/src/libslic3r/BuildVolume.cpp @@ -238,7 +238,7 @@ BuildVolume::ObjectState object_state_templ(const indexed_triangle_set &its, con const stl_vertex p2 = trafo * its.vertices[tri(iedge)]; assert(sign(p1) == s[iprev]); assert(sign(p2) == s[iedge]); - assert(p1.z() * p2.z() < 0); + assert((p1.z() - world_min_z) * (p2.z() - world_min_z) < 0); // Edge crosses the z plane. Calculate intersection point with the plane. const float t = (world_min_z - p1.z()) / (p2.z() - p1.z()); (is_inside(Vec3f(p1.x() + (p2.x() - p1.x()) * t, p1.y() + (p2.y() - p1.y()) * t, world_min_z)) ? inside : outside) = true; diff --git a/src/libslic3r/Fill/FillLightning.cpp b/src/libslic3r/Fill/FillLightning.cpp index 447fd8057..dd2189e6b 100644 --- a/src/libslic3r/Fill/FillLightning.cpp +++ b/src/libslic3r/Fill/FillLightning.cpp @@ -1,29 +1,34 @@ #include "../Print.hpp" +#include "../ShortestPath.hpp" #include "FillLightning.hpp" #include "Lightning/Generator.hpp" -#include "../Surface.hpp" - -#include -#include -#include -#include namespace Slic3r::FillLightning { -Polylines Filler::fill_surface(const Surface *surface, const FillParams ¶ms) +void Filler::_fill_surface_single( + const FillParams ¶ms, + unsigned int thickness_layers, + const std::pair &direction, + ExPolygon expolygon, + Polylines &polylines_out) { - const Layer &layer = generator->getTreesForLayer(this->layer_id); - return layer.convertToLines(to_polygons(surface->expolygon), generator->infilll_extrusion_width()); + const Layer &layer = generator->getTreesForLayer(this->layer_id); + Polylines fill_lines = layer.convertToLines(to_polygons(expolygon), scaled(0.5 * this->spacing - this->overlap)); + + if (params.dont_connect() || fill_lines.size() <= 1) { + append(polylines_out, chain_polylines(std::move(fill_lines))); + } else + connect_infill(std::move(fill_lines), expolygon, polylines_out, this->spacing, params); } void GeneratorDeleter::operator()(Generator *p) { delete p; } -GeneratorPtr build_generator(const PrintObject &print_object) +GeneratorPtr build_generator(const PrintObject &print_object, const std::function &throw_on_cancel_callback) { - return GeneratorPtr(new Generator(print_object)); + return GeneratorPtr(new Generator(print_object, throw_on_cancel_callback)); } } // namespace Slic3r::FillAdaptive diff --git a/src/libslic3r/Fill/FillLightning.hpp b/src/libslic3r/Fill/FillLightning.hpp index b4e7e35f1..6e672783a 100644 --- a/src/libslic3r/Fill/FillLightning.hpp +++ b/src/libslic3r/Fill/FillLightning.hpp @@ -14,7 +14,7 @@ class Generator; struct GeneratorDeleter { void operator()(Generator *p); }; using GeneratorPtr = std::unique_ptr; -GeneratorPtr build_generator(const PrintObject &print_object); +GeneratorPtr build_generator(const PrintObject &print_object, const std::function &throw_on_cancel_callback); class Filler : public Slic3r::Fill { @@ -24,8 +24,13 @@ public: Generator *generator { nullptr }; protected: Fill* clone() const override { return new Filler(*this); } - // Perform the fill. - Polylines fill_surface(const Surface *surface, const FillParams ¶ms) override; + + void _fill_surface_single(const FillParams ¶ms, + unsigned int thickness_layers, + const std::pair &direction, + ExPolygon expolygon, + Polylines &polylines_out) override; + // Let the G-code export reoder the infill lines. bool no_sort() const override { return false; } }; diff --git a/src/libslic3r/Fill/FillRectilinear.cpp b/src/libslic3r/Fill/FillRectilinear.cpp index 264ae8a59..ba7461c5f 100644 --- a/src/libslic3r/Fill/FillRectilinear.cpp +++ b/src/libslic3r/Fill/FillRectilinear.cpp @@ -3043,14 +3043,18 @@ Polylines FillSupportBase::fill_surface(const Surface *surface, const FillParams return polylines_out; } -Points sample_grid_pattern(const ExPolygon &expolygon, coord_t spacing) +// Lightning infill assumes that the distance between any two sampled points is always +// at least equal to the value of spacing. To meet this assumption, we need to use +// BoundingBox for whole layers instead of bounding box just around processing ExPolygon. +// Using just BoundingBox around processing ExPolygon could produce two points closer +// than spacing (in cases where two ExPolygon are closer than spacing). +Points sample_grid_pattern(const ExPolygon &expolygon, coord_t spacing, const BoundingBox &global_bounding_box) { ExPolygonWithOffset poly_with_offset(expolygon, 0, 0, 0); - BoundingBox bounding_box = poly_with_offset.bounding_box_src(); std::vector segs = slice_region_by_vertical_lines( poly_with_offset, - (bounding_box.max.x() - bounding_box.min.x() + spacing - 1) / spacing, - bounding_box.min.x(), + (global_bounding_box.max.x() - global_bounding_box.min.x() + spacing - 1) / spacing, + global_bounding_box.min.x(), spacing); Points out; @@ -3066,17 +3070,17 @@ Points sample_grid_pattern(const ExPolygon &expolygon, coord_t spacing) return out; } -Points sample_grid_pattern(const ExPolygons &expolygons, coord_t spacing) +Points sample_grid_pattern(const ExPolygons &expolygons, coord_t spacing, const BoundingBox &global_bounding_box) { Points out; for (const ExPolygon &expoly : expolygons) - append(out, sample_grid_pattern(expoly, spacing)); + append(out, sample_grid_pattern(expoly, spacing, global_bounding_box)); return out; } -Points sample_grid_pattern(const Polygons &polygons, coord_t spacing) +Points sample_grid_pattern(const Polygons &polygons, coord_t spacing, const BoundingBox &global_bounding_box) { - return sample_grid_pattern(union_ex(polygons), spacing); + return sample_grid_pattern(union_ex(polygons), spacing, global_bounding_box); } } // namespace Slic3r diff --git a/src/libslic3r/Fill/FillRectilinear.hpp b/src/libslic3r/Fill/FillRectilinear.hpp index ba735dd02..0a6c976ad 100644 --- a/src/libslic3r/Fill/FillRectilinear.hpp +++ b/src/libslic3r/Fill/FillRectilinear.hpp @@ -109,9 +109,9 @@ protected: float _layer_angle(size_t idx) const override { return 0.f; } }; -Points sample_grid_pattern(const ExPolygon &expolygon, coord_t spacing); -Points sample_grid_pattern(const ExPolygons &expolygons, coord_t spacing); -Points sample_grid_pattern(const Polygons &polygons, coord_t spacing); +Points sample_grid_pattern(const ExPolygon &expolygon, coord_t spacing, const BoundingBox &global_bounding_box); +Points sample_grid_pattern(const ExPolygons &expolygons, coord_t spacing, const BoundingBox &global_bounding_box); +Points sample_grid_pattern(const Polygons &polygons, coord_t spacing, const BoundingBox &global_bounding_box); } // namespace Slic3r diff --git a/src/libslic3r/Fill/Lightning/DistanceField.cpp b/src/libslic3r/Fill/Lightning/DistanceField.cpp index 308ca41c6..0cbfa9af5 100644 --- a/src/libslic3r/Fill/Lightning/DistanceField.cpp +++ b/src/libslic3r/Fill/Lightning/DistanceField.cpp @@ -5,11 +5,36 @@ #include "../FillRectilinear.hpp" #include "../../ClipperUtils.hpp" +#include + +#ifdef LIGHTNING_DISTANCE_FIELD_DEBUG_OUTPUT +#include "../../SVG.hpp" +#endif + namespace Slic3r::FillLightning { constexpr coord_t radius_per_cell_size = 6; // The cell-size should be small compared to the radius, but not so small as to be inefficient. +#ifdef LIGHTNING_DISTANCE_FIELD_DEBUG_OUTPUT +void export_distance_field_to_svg(const std::string &path, const Polygons &outline, const Polygons &overhang, const std::list &unsupported_points, const Points &points = {}) +{ + coordf_t stroke_width = scaled(0.01); + BoundingBox bbox = get_extents(outline); + + bbox.offset(SCALED_EPSILON); + SVG svg(path, bbox); + svg.draw_outline(outline, "green", stroke_width); + svg.draw_outline(overhang, "blue", stroke_width); + + for (const DistanceField::UnsupportedCell &cell : unsupported_points) + svg.draw(cell.loc, "cyan", coord_t(stroke_width)); + + for (const Point &pt : points) + svg.draw(pt, "red", coord_t(stroke_width)); +} +#endif + DistanceField::DistanceField(const coord_t& radius, const Polygons& current_outline, const BoundingBox& current_outlines_bbox, const Polygons& current_overhang) : m_cell_size(radius / radius_per_cell_size), m_supporting_radius(radius), @@ -17,37 +42,54 @@ DistanceField::DistanceField(const coord_t& radius, const Polygons& current_outl { m_supporting_radius2 = Slic3r::sqr(int64_t(radius)); // Sample source polygons with a regular grid sampling pattern. + const BoundingBox overhang_bbox = get_extents(current_overhang); for (const ExPolygon &expoly : union_ex(current_overhang)) { - for (const Point &p : sample_grid_pattern(expoly, m_cell_size)) { - // Find a squared distance to the source expolygon boundary. - double d2 = std::numeric_limits::max(); - for (size_t icontour = 0; icontour <= expoly.holes.size(); ++icontour) { - const Polygon &contour = icontour == 0 ? expoly.contour : expoly.holes[icontour - 1]; - if (contour.size() > 2) { - Point prev = contour.points.back(); - for (const Point &p2 : contour.points) { - d2 = std::min(d2, Line::distance_to_squared(p, prev, p2)); - prev = p2; + const Points sampled_points = sample_grid_pattern(expoly, m_cell_size, overhang_bbox); + const size_t unsupported_points_prev_size = m_unsupported_points.size(); + m_unsupported_points.resize(unsupported_points_prev_size + sampled_points.size()); + + tbb::parallel_for(tbb::blocked_range(0, sampled_points.size()), [&self = *this, &expoly = std::as_const(expoly), &sampled_points = std::as_const(sampled_points), &unsupported_points_prev_size = std::as_const(unsupported_points_prev_size)](const tbb::blocked_range &range) -> void { + for (size_t sp_idx = range.begin(); sp_idx < range.end(); ++sp_idx) { + const Point &sp = sampled_points[sp_idx]; + // Find a squared distance to the source expolygon boundary. + double d2 = std::numeric_limits::max(); + for (size_t icontour = 0; icontour <= expoly.holes.size(); ++icontour) { + const Polygon &contour = icontour == 0 ? expoly.contour : expoly.holes[icontour - 1]; + if (contour.size() > 2) { + Point prev = contour.points.back(); + for (const Point &p2 : contour.points) { + d2 = std::min(d2, Line::distance_to_squared(sp, prev, p2)); + prev = p2; + } } } + self.m_unsupported_points[unsupported_points_prev_size + sp_idx] = {sp, coord_t(std::sqrt(d2))}; + assert(self.m_unsupported_points_bbox.contains(sp)); } - m_unsupported_points.emplace_back(p, sqrt(d2)); - assert(m_unsupported_points_bbox.contains(p)); - } + }); // end of parallel_for } - m_unsupported_points.sort([&radius](const UnsupportedCell &a, const UnsupportedCell &b) { + std::stable_sort(m_unsupported_points.begin(), m_unsupported_points.end(), [&radius](const UnsupportedCell &a, const UnsupportedCell &b) { constexpr coord_t prime_for_hash = 191; return std::abs(b.dist_to_boundary - a.dist_to_boundary) > radius ? a.dist_to_boundary < b.dist_to_boundary : (PointHash{}(a.loc) % prime_for_hash) < (PointHash{}(b.loc) % prime_for_hash); }); - for (auto it = m_unsupported_points.begin(); it != m_unsupported_points.end(); ++it) { - UnsupportedCell& cell = *it; - m_unsupported_points_grid.emplace(this->to_grid_point(cell.loc), it); - } + + m_unsupported_points_erased.resize(m_unsupported_points.size()); + std::fill(m_unsupported_points_erased.begin(), m_unsupported_points_erased.end(), false); + + m_unsupported_points_grid.initialize(m_unsupported_points, [&self = std::as_const(*this)](const Point &p) -> Point { return self.to_grid_point(p); }); + // Because the distance between two points is at least one axis equal to m_cell_size, every cell // in m_unsupported_points_grid contains exactly one point. assert(m_unsupported_points.size() == m_unsupported_points_grid.size()); + +#ifdef LIGHTNING_DISTANCE_FIELD_DEBUG_OUTPUT + { + static int iRun = 0; + export_distance_field_to_svg(debug_out_path("FillLightning-DistanceField-%d.svg", iRun++), current_outline, current_overhang, m_unsupported_points); + } +#endif } void DistanceField::update(const Point& to_node, const Point& added_leaf) @@ -96,12 +138,11 @@ void DistanceField::update(const Point& to_node, const Point& added_leaf) } // Inside a circle at the end of the new leaf, or inside a rotated rectangle. // Remove unsupported leafs at this grid location. - if (auto it = m_unsupported_points_grid.find(grid_addr); it != m_unsupported_points_grid.end()) { - std::list::iterator& list_it = it->second; - UnsupportedCell& cell = *list_it; + if (const size_t cell_idx = m_unsupported_points_grid.find_cell_idx(grid_addr); cell_idx != std::numeric_limits::max()) { + const UnsupportedCell &cell = m_unsupported_points[cell_idx]; if ((cell.loc - added_leaf).cast().squaredNorm() <= m_supporting_radius2) { - m_unsupported_points.erase(list_it); - m_unsupported_points_grid.erase(it); + m_unsupported_points_erased[cell_idx] = true; + m_unsupported_points_grid.mark_erased(grid_addr); } } } diff --git a/src/libslic3r/Fill/Lightning/DistanceField.hpp b/src/libslic3r/Fill/Lightning/DistanceField.hpp index beb46c5c5..007ac235e 100644 --- a/src/libslic3r/Fill/Lightning/DistanceField.hpp +++ b/src/libslic3r/Fill/Lightning/DistanceField.hpp @@ -8,6 +8,8 @@ #include "../../Point.hpp" #include "../../Polygon.hpp" +//#define LIGHTNING_DISTANCE_FIELD_DEBUG_OUTPUT + namespace Slic3r::FillLightning { @@ -38,11 +40,17 @@ public: * \return ``true`` if successful, or ``false`` if there are no more points * to consider. */ - bool tryGetNextPoint(Point* p) const { - if (m_unsupported_points.empty()) - return false; - *p = m_unsupported_points.front().loc; - return true; + bool tryGetNextPoint(Point *out_unsupported_location, size_t *out_unsupported_cell_idx, const size_t start_idx = 0) const + { + for (size_t point_idx = start_idx; point_idx < m_unsupported_points.size(); ++point_idx) { + if (!m_unsupported_points_erased[point_idx]) { + *out_unsupported_cell_idx = point_idx; + *out_unsupported_location = m_unsupported_points[point_idx].loc; + return true; + } + } + + return false; } /*! @@ -77,7 +85,6 @@ protected: */ struct UnsupportedCell { - UnsupportedCell(const Point &loc, coord_t dist_to_boundary) : loc(loc), dist_to_boundary(dist_to_boundary) {} // The position of the center of this cell. Point loc; // How far this cell is removed from the ``current_outline`` polygon, the edge of the infill area. @@ -87,7 +94,8 @@ protected: /*! * Cells which still need to be supported at some point. */ - std::list m_unsupported_points; + std::vector m_unsupported_points; + std::vector m_unsupported_points_erased; /*! * BoundingBox of all points in m_unsupported_points. Used for mapping of sign integer numbers to positive integer numbers. @@ -98,7 +106,84 @@ protected: * Links the unsupported points to a grid point, so that we can quickly look * up the cell belonging to a certain position in the grid. */ - std::unordered_map::iterator, PointHash> m_unsupported_points_grid; + + class UnsupportedPointsGrid + { + public: + UnsupportedPointsGrid() = default; + void initialize(const std::vector &unsupported_points, const std::function &map_cell_to_grid) + { + if (unsupported_points.empty()) + return; + + BoundingBox unsupported_points_bbox; + for (const UnsupportedCell &cell : unsupported_points) + unsupported_points_bbox.merge(cell.loc); + + m_size = unsupported_points.size(); + m_grid_range = BoundingBox(map_cell_to_grid(unsupported_points_bbox.min), map_cell_to_grid(unsupported_points_bbox.max)); + m_grid_size = m_grid_range.size() + Point::Ones(); + + m_data.assign(m_grid_size.y() * m_grid_size.x(), std::numeric_limits::max()); + m_data_erased.assign(m_grid_size.y() * m_grid_size.x(), true); + + for (size_t cell_idx = 0; cell_idx < unsupported_points.size(); ++cell_idx) { + const size_t flat_idx = map_to_flat_array(map_cell_to_grid(unsupported_points[cell_idx].loc)); + assert(m_data[flat_idx] == std::numeric_limits::max()); + m_data[flat_idx] = cell_idx; + m_data_erased[flat_idx] = false; + } + } + + size_t size() const { return m_size; } + + size_t find_cell_idx(const Point &grid_addr) + { + if (!m_grid_range.contains(grid_addr)) + return std::numeric_limits::max(); + + if (const size_t flat_idx = map_to_flat_array(grid_addr); !m_data_erased[flat_idx]) { + assert(m_data[flat_idx] != std::numeric_limits::max()); + return m_data[flat_idx]; + } + + return std::numeric_limits::max(); + } + + void mark_erased(const Point &grid_addr) + { + assert(m_grid_range.contains(grid_addr)); + if (!m_grid_range.contains(grid_addr)) + return; + + const size_t flat_idx = map_to_flat_array(grid_addr); + assert(!m_data_erased[flat_idx] && m_data[flat_idx] != std::numeric_limits::max()); + assert(m_size != 0); + + m_data_erased[flat_idx] = true; + --m_size; + } + + private: + size_t m_size = 0; + + BoundingBox m_grid_range; + Point m_grid_size; + + std::vector m_data; + std::vector m_data_erased; + + inline size_t map_to_flat_array(const Point &loc) const + { + const Point offset_loc = loc - m_grid_range.min; + const size_t flat_idx = m_grid_size.x() * offset_loc.y() + offset_loc.x(); + assert(offset_loc.x() >= 0 && offset_loc.y() >= 0); + assert(flat_idx < size_t(m_grid_size.y() * m_grid_size.x())); + return flat_idx; + } + }; + + UnsupportedPointsGrid m_unsupported_points_grid; /*! * Maps the point to the grid coordinates. @@ -113,6 +198,10 @@ protected: Point from_grid_point(const Point &point) const { return point * m_cell_size + m_unsupported_points_bbox.min; } + +#ifdef LIGHTNING_DISTANCE_FIELD_DEBUG_OUTPUT + friend void export_distance_field_to_svg(const std::string &path, const Polygons &outline, const Polygons &overhang, const std::list &unsupported_points, const Points &points); +#endif }; } // namespace Slic3r::FillLightning diff --git a/src/libslic3r/Fill/Lightning/Generator.cpp b/src/libslic3r/Fill/Lightning/Generator.cpp index 0bdd1c7e8..75ca5583e 100644 --- a/src/libslic3r/Fill/Lightning/Generator.cpp +++ b/src/libslic3r/Fill/Lightning/Generator.cpp @@ -7,7 +7,6 @@ #include "../../ClipperUtils.hpp" #include "../../Layer.hpp" #include "../../Print.hpp" -#include "../../Surface.hpp" /* Possible future tasks/optimizations,etc.: * - Improve connecting heuristic to favor connecting to shorter trees @@ -25,7 +24,7 @@ namespace Slic3r::FillLightning { -Generator::Generator(const PrintObject &print_object) +Generator::Generator(const PrintObject &print_object, const std::function &throw_on_cancel_callback) { const PrintConfig &print_config = print_object.print()->config(); const PrintObjectConfig &object_config = print_object.config(); @@ -47,24 +46,23 @@ Generator::Generator(const PrintObject &print_object) m_prune_length = coord_t(layer_thickness * std::tan(lightning_infill_prune_angle)); m_straightening_max_distance = coord_t(layer_thickness * std::tan(lightning_infill_straightening_angle)); - generateInitialInternalOverhangs(print_object); - generateTrees(print_object); + generateInitialInternalOverhangs(print_object, throw_on_cancel_callback); + generateTrees(print_object, throw_on_cancel_callback); } -void Generator::generateInitialInternalOverhangs(const PrintObject &print_object) +void Generator::generateInitialInternalOverhangs(const PrintObject &print_object, const std::function &throw_on_cancel_callback) { m_overhang_per_layer.resize(print_object.layers().size()); - // FIXME: It can be adjusted to improve bonding between infill and perimeters. - const float infill_wall_offset = 0;// m_infill_extrusion_width; Polygons infill_area_above; //Iterate from top to bottom, to subtract the overhang areas above from the overhang areas on the layer below, to get only overhang in the top layer where it is overhanging. - for (int layer_nr = int(print_object.layers().size()) - 1; layer_nr >= 0; layer_nr--) { + for (int layer_nr = int(print_object.layers().size()) - 1; layer_nr >= 0; --layer_nr) { + throw_on_cancel_callback(); Polygons infill_area_here; for (const LayerRegion* layerm : print_object.get_layer(layer_nr)->regions()) for (const Surface& surface : layerm->fill_surfaces.surfaces) if (surface.surface_type == stInternal || surface.surface_type == stInternalVoid) - append(infill_area_here, infill_wall_offset == 0 ? surface.expolygon : offset(surface.expolygon, infill_wall_offset)); + append(infill_area_here, to_polygons(surface.expolygon)); //Remove the part of the infill area that is already supported by the walls. Polygons overhang = diff(offset(infill_area_here, -float(m_wall_supporting_radius)), infill_area_above); @@ -80,20 +78,20 @@ const Layer& Generator::getTreesForLayer(const size_t& layer_id) const return m_lightning_layers[layer_id]; } -void Generator::generateTrees(const PrintObject &print_object) +void Generator::generateTrees(const PrintObject &print_object, const std::function &throw_on_cancel_callback) { m_lightning_layers.resize(print_object.layers().size()); - // FIXME: It can be adjusted to improve bonding between infill and perimeters. - const coord_t infill_wall_offset = 0;// m_infill_extrusion_width; std::vector infill_outlines(print_object.layers().size(), Polygons()); // For-each layer from top to bottom: - for (int layer_id = int(print_object.layers().size()) - 1; layer_id >= 0; layer_id--) + for (int layer_id = int(print_object.layers().size()) - 1; layer_id >= 0; layer_id--) { + throw_on_cancel_callback(); for (const LayerRegion *layerm : print_object.get_layer(layer_id)->regions()) for (const Surface &surface : layerm->fill_surfaces.surfaces) if (surface.surface_type == stInternal || surface.surface_type == stInternalVoid) - append(infill_outlines[layer_id], infill_wall_offset == 0 ? surface.expolygon : offset(surface.expolygon, infill_wall_offset)); + append(infill_outlines[layer_id], to_polygons(surface.expolygon)); + } // For various operations its beneficial to quickly locate nearby features on the polygon: const size_t top_layer_id = print_object.layers().size() - 1; @@ -102,6 +100,7 @@ void Generator::generateTrees(const PrintObject &print_object) // For-each layer from top to bottom: for (int layer_id = int(top_layer_id); layer_id >= 0; layer_id--) { + throw_on_cancel_callback(); Layer ¤t_lightning_layer = m_lightning_layers[layer_id]; const Polygons ¤t_outlines = infill_outlines[layer_id]; const BoundingBox ¤t_outlines_bbox = get_extents(current_outlines); @@ -109,7 +108,7 @@ void Generator::generateTrees(const PrintObject &print_object) // register all trees propagated from the previous layer as to-be-reconnected std::vector to_be_reconnected_tree_roots = current_lightning_layer.tree_roots; - current_lightning_layer.generateNewTrees(m_overhang_per_layer[layer_id], current_outlines, current_outlines_bbox, outlines_locator, m_supporting_radius, m_wall_supporting_radius); + current_lightning_layer.generateNewTrees(m_overhang_per_layer[layer_id], current_outlines, current_outlines_bbox, outlines_locator, m_supporting_radius, m_wall_supporting_radius, throw_on_cancel_callback); current_lightning_layer.reconnectRoots(to_be_reconnected_tree_roots, current_outlines, current_outlines_bbox, outlines_locator, m_supporting_radius, m_wall_supporting_radius); // Initialize trees for next lower layer from the current one. @@ -121,6 +120,9 @@ void Generator::generateTrees(const PrintObject &print_object) if (const BoundingBox &outlines_locator_bbox = outlines_locator.bbox(); outlines_locator_bbox.defined) below_outlines_bbox.merge(outlines_locator_bbox); + if (!current_lightning_layer.tree_roots.empty()) + below_outlines_bbox.merge(get_extents(current_lightning_layer.tree_roots).inflated(SCALED_EPSILON)); + outlines_locator.set_bbox(below_outlines_bbox); outlines_locator.create(below_outlines, locator_cell_size); diff --git a/src/libslic3r/Fill/Lightning/Generator.hpp b/src/libslic3r/Fill/Lightning/Generator.hpp index b538c4dbe..d7e5f63f9 100644 --- a/src/libslic3r/Fill/Lightning/Generator.hpp +++ b/src/libslic3r/Fill/Lightning/Generator.hpp @@ -43,9 +43,8 @@ public: * This generator will pre-compute things in preparation of generating * Lightning Infill for the infill areas in that mesh. The infill areas must * already be calculated at this point. - * \param mesh The mesh to generate infill for. */ - explicit Generator(const PrintObject &print_object); + explicit Generator(const PrintObject &print_object, const std::function &throw_on_cancel_callback); /*! * Get a tree of paths generated for a certain layer of the mesh. @@ -69,12 +68,12 @@ protected: * only when support is generated. For this pattern, we also need to * generate overhang areas for the inside of the model. */ - void generateInitialInternalOverhangs(const PrintObject &print_object); + void generateInitialInternalOverhangs(const PrintObject &print_object, const std::function &throw_on_cancel_callback); /*! * Calculate the tree structure of all layers. */ - void generateTrees(const PrintObject &print_object); + void generateTrees(const PrintObject &print_object, const std::function &throw_on_cancel_callback); float m_infill_extrusion_width; diff --git a/src/libslic3r/Fill/Lightning/Layer.cpp b/src/libslic3r/Fill/Lightning/Layer.cpp index c996b9b7b..354623e51 100644 --- a/src/libslic3r/Fill/Lightning/Layer.cpp +++ b/src/libslic3r/Fill/Lightning/Layer.cpp @@ -10,6 +10,10 @@ #include "../../Geometry.hpp" #include "Utils.hpp" +#include +#include +#include + namespace Slic3r::FillLightning { coord_t Layer::getWeightedDistance(const Point& boundary_loc, const Point& unsupported_location) @@ -44,18 +48,22 @@ void Layer::generateNewTrees const BoundingBox& current_outlines_bbox, const EdgeGrid::Grid& outlines_locator, const coord_t supporting_radius, - const coord_t wall_supporting_radius + const coord_t wall_supporting_radius, + const std::function &throw_on_cancel_callback ) { DistanceField distance_field(supporting_radius, current_outlines, current_outlines_bbox, current_overhang); + throw_on_cancel_callback(); SparseNodeGrid tree_node_locator; fillLocator(tree_node_locator, current_outlines_bbox); // Until no more points need to be added to support all: // Determine next point from tree/outline areas via distance-field - Point unsupported_location; - while (distance_field.tryGetNextPoint(&unsupported_location)) { + size_t unsupported_cell_idx = 0; + Point unsupported_location; + while (distance_field.tryGetNextPoint(&unsupported_location, &unsupported_cell_idx, unsupported_cell_idx)) { + throw_on_cancel_callback(); GroundingLocation grounding_loc = getBestGroundingLocation( unsupported_location, current_outlines, current_outlines_bbox, outlines_locator, supporting_radius, wall_supporting_radius, tree_node_locator); @@ -126,9 +134,10 @@ GroundingLocation Layer::getBestGroundingLocation if (contour.size() > 2) { Point prev = contour.points.back(); for (const Point &p2 : contour.points) { - if (double d = Line::distance_to_squared(unsupported_location, prev, p2); d < d2) { + Point closest_point; + if (double d = line_alg::distance_to_squared(Line{prev, p2}, unsupported_location, &closest_point); d < d2) { d2 = d; - node_location = Geometry::foot_pt({ prev, p2 }, unsupported_location).cast(); + node_location = closest_point; } prev = p2; } @@ -137,30 +146,52 @@ GroundingLocation Layer::getBestGroundingLocation const auto within_dist = coord_t((node_location - unsupported_location).cast().norm()); - NodeSPtr sub_tree{ nullptr }; - coord_t current_dist = getWeightedDistance(node_location, unsupported_location); + NodeSPtr sub_tree{nullptr}; + coord_t current_dist = getWeightedDistance(node_location, unsupported_location); if (current_dist >= wall_supporting_radius) { // Only reconnect tree roots to other trees if they are not already close to the outlines. const coord_t search_radius = std::min(current_dist, within_dist); BoundingBox region(unsupported_location - Point(search_radius, search_radius), unsupported_location + Point(search_radius + locator_cell_size, search_radius + locator_cell_size)); region.min = to_grid_point(region.min, current_outlines_bbox); region.max = to_grid_point(region.max, current_outlines_bbox); - Point grid_addr; - for (grid_addr.y() = region.min.y(); grid_addr.y() < region.max.y(); ++ grid_addr.y()) - for (grid_addr.x() = region.min.x(); grid_addr.x() < region.max.x(); ++ grid_addr.x()) { - auto it_range = tree_node_locator.equal_range(grid_addr); - for (auto it = it_range.first; it != it_range.second; ++ it) { - auto candidate_sub_tree = it->second.lock(); - if ((candidate_sub_tree && candidate_sub_tree != exclude_tree) && - !(exclude_tree && exclude_tree->hasOffspring(candidate_sub_tree)) && - !polygonCollidesWithLineSegment(unsupported_location, candidate_sub_tree->getLocation(), outline_locator)) { - const coord_t candidate_dist = candidate_sub_tree->getWeightedDistance(unsupported_location, supporting_radius); - if (candidate_dist < current_dist) { - current_dist = candidate_dist; - sub_tree = candidate_sub_tree; + + Point current_dist_grid_addr{std::numeric_limits::lowest(), std::numeric_limits::lowest()}; + std::mutex current_dist_mutex; + tbb::parallel_for(tbb::blocked_range2d(region.min.y(), region.max.y(), region.min.x(), region.max.x()), [¤t_dist, current_dist_copy = current_dist, ¤t_dist_mutex, &sub_tree, ¤t_dist_grid_addr, &exclude_tree = std::as_const(exclude_tree), &outline_locator = std::as_const(outline_locator), &supporting_radius = std::as_const(supporting_radius), &tree_node_locator = std::as_const(tree_node_locator), &unsupported_location = std::as_const(unsupported_location)](const tbb::blocked_range2d &range) -> void { + for (coord_t grid_addr_y = range.rows().begin(); grid_addr_y < range.rows().end(); ++grid_addr_y) + for (coord_t grid_addr_x = range.cols().begin(); grid_addr_x < range.cols().end(); ++grid_addr_x) { + const Point local_grid_addr{grid_addr_x, grid_addr_y}; + NodeSPtr local_sub_tree{nullptr}; + coord_t local_current_dist = current_dist_copy; + const auto it_range = tree_node_locator.equal_range(local_grid_addr); + for (auto it = it_range.first; it != it_range.second; ++it) { + const NodeSPtr candidate_sub_tree = it->second.lock(); + if ((candidate_sub_tree && candidate_sub_tree != exclude_tree) && + !(exclude_tree && exclude_tree->hasOffspring(candidate_sub_tree)) && + !polygonCollidesWithLineSegment(unsupported_location, candidate_sub_tree->getLocation(), outline_locator)) { + if (const coord_t candidate_dist = candidate_sub_tree->getWeightedDistance(unsupported_location, supporting_radius); candidate_dist < local_current_dist) { + local_current_dist = candidate_dist; + local_sub_tree = candidate_sub_tree; + } + } + } + // To always get the same result in a parallel version as in a non-parallel version, + // we need to preserve that for the same current_dist, we select the same sub_tree + // as in the non-parallel version. For this purpose, inside the variable + // current_dist_grid_addr is stored from with 2D grid position assigned sub_tree comes. + // And when there are two sub_tree with the same current_dist, one which will be found + // the first in the non-parallel version is selected. + { + std::lock_guard lock(current_dist_mutex); + if (local_current_dist < current_dist || + (local_current_dist == current_dist && (grid_addr_y < current_dist_grid_addr.y() || + (grid_addr_y == current_dist_grid_addr.y() && grid_addr_x < current_dist_grid_addr.x())))) { + current_dist = local_current_dist; + sub_tree = local_sub_tree; + current_dist_grid_addr = local_grid_addr; } } } - } + }); // end of parallel_for } return ! sub_tree ? @@ -402,15 +433,14 @@ static unsigned int moveInside(const Polygons& polygons, Point& from, int distan } #endif -// Returns 'added someting'. -Polylines Layer::convertToLines(const Polygons& limit_to_outline, const coord_t line_width) const +Polylines Layer::convertToLines(const Polygons& limit_to_outline, const coord_t line_overlap) const { if (tree_roots.empty()) return {}; Polylines result_lines; for (const auto &tree : tree_roots) - tree->convertToPolylines(result_lines, line_width); + tree->convertToPolylines(result_lines, line_overlap); return intersection_pl(result_lines, limit_to_outline); } diff --git a/src/libslic3r/Fill/Lightning/Layer.hpp b/src/libslic3r/Fill/Lightning/Layer.hpp index cc79c15b5..e8c0a38b4 100644 --- a/src/libslic3r/Fill/Lightning/Layer.hpp +++ b/src/libslic3r/Fill/Lightning/Layer.hpp @@ -44,7 +44,8 @@ public: const BoundingBox& current_outlines_bbox, const EdgeGrid::Grid& outline_locator, coord_t supporting_radius, - coord_t wall_supporting_radius + coord_t wall_supporting_radius, + const std::function &throw_on_cancel_callback ); /*! Determine & connect to connection point in tree/outline. @@ -79,7 +80,7 @@ public: coord_t wall_supporting_radius ); - Polylines convertToLines(const Polygons& limit_to_outline, coord_t line_width) const; + Polylines convertToLines(const Polygons& limit_to_outline, coord_t line_overlap) const; coord_t getWeightedDistance(const Point& boundary_loc, const Point& unsupported_location); diff --git a/src/libslic3r/Fill/Lightning/TreeNode.cpp b/src/libslic3r/Fill/Lightning/TreeNode.cpp index 822550fc4..982d47b10 100644 --- a/src/libslic3r/Fill/Lightning/TreeNode.cpp +++ b/src/libslic3r/Fill/Lightning/TreeNode.cpp @@ -180,7 +180,11 @@ bool lineSegmentPolygonsIntersection(const Point& a, const Point& b, const EdgeG } visitor { outline_locator, a.cast(), b.cast() }; outline_locator.visit_cells_intersecting_line(a, b, visitor); - return visitor.d2min < double(within_max_dist) * double(within_max_dist); + if (visitor.d2min < double(within_max_dist) * double(within_max_dist)) { + result = Point(visitor.intersection_pt); + return true; + } + return false; } bool Node::realign(const Polygons& outlines, const EdgeGrid::Grid& outline_locator, std::vector& rerooted_parts) @@ -343,12 +347,12 @@ coord_t Node::prune(const coord_t& pruning_distance) return max_distance_pruned; } -void Node::convertToPolylines(Polylines &output, const coord_t line_width) const +void Node::convertToPolylines(Polylines &output, const coord_t line_overlap) const { Polylines result; result.emplace_back(); convertToPolylines(0, result); - removeJunctionOverlap(result, line_width); + removeJunctionOverlap(result, line_overlap); append(output, std::move(result)); } @@ -372,10 +376,10 @@ void Node::convertToPolylines(size_t long_line_idx, Polylines &output) const } } -void Node::removeJunctionOverlap(Polylines &result_lines, const coord_t line_width) const +void Node::removeJunctionOverlap(Polylines &result_lines, const coord_t line_overlap) const { - const coord_t reduction = line_width / 2; // TODO make configurable? - size_t res_line_idx = 0; + const coord_t reduction = line_overlap; + size_t res_line_idx = 0; while (res_line_idx < result_lines.size()) { Polyline &polyline = result_lines[res_line_idx]; if (polyline.size() <= 1) { diff --git a/src/libslic3r/Fill/Lightning/TreeNode.hpp b/src/libslic3r/Fill/Lightning/TreeNode.hpp index fdb80d2e6..8791b4331 100644 --- a/src/libslic3r/Fill/Lightning/TreeNode.hpp +++ b/src/libslic3r/Fill/Lightning/TreeNode.hpp @@ -46,7 +46,7 @@ public: { struct EnableMakeShared : public Node { - EnableMakeShared(Arg&&...arg) : Node(std::forward(arg)...) {} + explicit EnableMakeShared(Arg&&...arg) : Node(std::forward(arg)...) {} }; return std::make_shared(std::forward(arg)...); } @@ -179,16 +179,16 @@ public: */ bool hasOffspring(const NodeSPtr& to_be_checked) const; -protected: Node() = delete; // Don't allow empty contruction +protected: /*! * Construct a new node, either for insertion in a tree or as root. * \param p The physical location in the 2D layer that this node represents. * Connecting other nodes to this node indicates that a line segment should * be drawn between those two physical positions. */ - Node(const Point& p, const std::optional& last_grounding_location = std::nullopt); + explicit Node(const Point& p, const std::optional& last_grounding_location = std::nullopt); /*! * Copy this node and its entire sub-tree. @@ -239,7 +239,7 @@ public: * * \param output all branches in this tree connected into polylines */ - void convertToPolylines(Polylines &output, coord_t line_width) const; + void convertToPolylines(Polylines &output, coord_t line_overlap) const; /*! If this was ever a direct child of the root, it'll have a previous grounding location. * @@ -260,7 +260,7 @@ protected: */ void convertToPolylines(size_t long_line_idx, Polylines &output) const; - void removeJunctionOverlap(Polylines &polylines, coord_t line_width) const; + void removeJunctionOverlap(Polylines &polylines, coord_t line_overlap) const; bool m_is_root; Point m_p; @@ -269,6 +269,9 @@ protected: std::optional m_last_grounding_location; // &tree_roots); + #ifdef LIGHTNING_TREE_NODE_DEBUG_OUTPUT friend void export_to_svg(const NodeSPtr &root_node, Slic3r::SVG &svg); friend void export_to_svg(const std::string &path, const Polygons &contour, const std::vector &root_nodes); @@ -278,6 +281,23 @@ protected: bool inside(const Polygons &polygons, const Point &p); bool lineSegmentPolygonsIntersection(const Point& a, const Point& b, const EdgeGrid::Grid& outline_locator, Point& result, coord_t within_max_dist); +inline BoundingBox get_extents(const NodeSPtr &root_node) +{ + BoundingBox bbox; + for (const NodeSPtr &children : root_node->m_children) + bbox.merge(get_extents(children)); + bbox.merge(root_node->getLocation()); + return bbox; +} + +inline BoundingBox get_extents(const std::vector &tree_roots) +{ + BoundingBox bbox; + for (const NodeSPtr &root_node : tree_roots) + bbox.merge(get_extents(root_node)); + return bbox; +} + #ifdef LIGHTNING_TREE_NODE_DEBUG_OUTPUT void export_to_svg(const NodeSPtr &root_node, SVG &svg); void export_to_svg(const std::string &path, const Polygons &contour, const std::vector &root_nodes); diff --git a/src/libslic3r/Format/3mf.cpp b/src/libslic3r/Format/3mf.cpp index 4259562aa..9a71c8978 100644 --- a/src/libslic3r/Format/3mf.cpp +++ b/src/libslic3r/Format/3mf.cpp @@ -1,3198 +1,3222 @@ -#include "../libslic3r.h" -#include "../Exception.hpp" -#include "../Model.hpp" -#include "../Utils.hpp" -#include "../LocalesUtils.hpp" -#include "../GCode.hpp" -#include "../Geometry.hpp" -#include "../GCode/ThumbnailData.hpp" -#include "../Semver.hpp" -#include "../Time.hpp" - -#include "../I18N.hpp" - -#include "3mf.hpp" - -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -namespace pt = boost::property_tree; - -#include -#include -#include "miniz_extension.hpp" - -#include - -// Slightly faster than sprintf("%.9g"), but there is an issue with the karma floating point formatter, -// https://github.com/boostorg/spirit/pull/586 -// where the exported string is one digit shorter than it should be to guarantee lossless round trip. -// The code is left here for the ocasion boost guys improve. -#define EXPORT_3MF_USE_SPIRIT_KARMA_FP 0 - -// VERSION NUMBERS -// 0 : .3mf, files saved by older slic3r or other applications. No version definition in them. -// 1 : Introduction of 3mf versioning. No other change in data saved into 3mf files. -// 2 : Volumes' matrices and source data added to Metadata/Slic3r_PE_model.config file, meshes transformed back to their coordinate system on loading. -// WARNING !! -> the version number has been rolled back to 1 -// the next change should use 3 -const unsigned int VERSION_3MF = 1; -// Allow loading version 2 file as well. -const unsigned int VERSION_3MF_COMPATIBLE = 2; -const char* SLIC3RPE_3MF_VERSION = "slic3rpe:Version3mf"; // definition of the metadata name saved into .model file - -// Painting gizmos data version numbers -// 0 : 3MF files saved by older PrusaSlicer or the painting gizmo wasn't used. No version definition in them. -// 1 : Introduction of painting gizmos data versioning. No other changes in painting gizmos data. -const unsigned int FDM_SUPPORTS_PAINTING_VERSION = 1; -const unsigned int SEAM_PAINTING_VERSION = 1; -const unsigned int MM_PAINTING_VERSION = 1; - -const std::string SLIC3RPE_FDM_SUPPORTS_PAINTING_VERSION = "slic3rpe:FdmSupportsPaintingVersion"; -const std::string SLIC3RPE_SEAM_PAINTING_VERSION = "slic3rpe:SeamPaintingVersion"; -const std::string SLIC3RPE_MM_PAINTING_VERSION = "slic3rpe:MmPaintingVersion"; - -const std::string MODEL_FOLDER = "3D/"; -const std::string MODEL_EXTENSION = ".model"; -const std::string MODEL_FILE = "3D/3dmodel.model"; // << this is the only format of the string which works with CURA -const std::string CONTENT_TYPES_FILE = "[Content_Types].xml"; -const std::string RELATIONSHIPS_FILE = "_rels/.rels"; -const std::string THUMBNAIL_FILE = "Metadata/thumbnail.png"; -const std::string PRINT_CONFIG_FILE = "Metadata/Slic3r_PE.config"; -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"; - -static constexpr const char* MODEL_TAG = "model"; -static constexpr const char* RESOURCES_TAG = "resources"; -static constexpr const char* OBJECT_TAG = "object"; -static constexpr const char* MESH_TAG = "mesh"; -static constexpr const char* VERTICES_TAG = "vertices"; -static constexpr const char* VERTEX_TAG = "vertex"; -static constexpr const char* TRIANGLES_TAG = "triangles"; -static constexpr const char* TRIANGLE_TAG = "triangle"; -static constexpr const char* COMPONENTS_TAG = "components"; -static constexpr const char* COMPONENT_TAG = "component"; -static constexpr const char* BUILD_TAG = "build"; -static constexpr const char* ITEM_TAG = "item"; -static constexpr const char* METADATA_TAG = "metadata"; - -static constexpr const char* CONFIG_TAG = "config"; -static constexpr const char* VOLUME_TAG = "volume"; - -static constexpr const char* UNIT_ATTR = "unit"; -static constexpr const char* NAME_ATTR = "name"; -static constexpr const char* TYPE_ATTR = "type"; -static constexpr const char* ID_ATTR = "id"; -static constexpr const char* X_ATTR = "x"; -static constexpr const char* Y_ATTR = "y"; -static constexpr const char* Z_ATTR = "z"; -static constexpr const char* V1_ATTR = "v1"; -static constexpr const char* V2_ATTR = "v2"; -static constexpr const char* V3_ATTR = "v3"; -static constexpr const char* OBJECTID_ATTR = "objectid"; -static constexpr const char* TRANSFORM_ATTR = "transform"; -static constexpr const char* PRINTABLE_ATTR = "printable"; -static constexpr const char* INSTANCESCOUNT_ATTR = "instances_count"; -static constexpr const char* CUSTOM_SUPPORTS_ATTR = "slic3rpe:custom_supports"; -static constexpr const char* CUSTOM_SEAM_ATTR = "slic3rpe:custom_seam"; -static constexpr const char* MMU_SEGMENTATION_ATTR = "slic3rpe:mmu_segmentation"; - -static constexpr const char* KEY_ATTR = "key"; -static constexpr const char* VALUE_ATTR = "value"; -static constexpr const char* FIRST_TRIANGLE_ID_ATTR = "firstid"; -static constexpr const char* LAST_TRIANGLE_ID_ATTR = "lastid"; - -static constexpr const char* OBJECT_TYPE = "object"; -static constexpr const char* VOLUME_TYPE = "volume"; - -static constexpr const char* NAME_KEY = "name"; -static constexpr const char* MODIFIER_KEY = "modifier"; -static constexpr const char* VOLUME_TYPE_KEY = "volume_type"; -static constexpr const char* MATRIX_KEY = "matrix"; -static constexpr const char* SOURCE_FILE_KEY = "source_file"; -static constexpr const char* SOURCE_OBJECT_ID_KEY = "source_object_id"; -static constexpr const char* SOURCE_VOLUME_ID_KEY = "source_volume_id"; -static constexpr const char* SOURCE_OFFSET_X_KEY = "source_offset_x"; -static constexpr const char* SOURCE_OFFSET_Y_KEY = "source_offset_y"; -static constexpr const char* SOURCE_OFFSET_Z_KEY = "source_offset_z"; -static constexpr const char* SOURCE_IN_INCHES_KEY = "source_in_inches"; -static constexpr const char* SOURCE_IN_METERS_KEY = "source_in_meters"; -#if ENABLE_RELOAD_FROM_DISK_REWORK -static constexpr const char* SOURCE_IS_BUILTIN_VOLUME_KEY = "source_is_builtin_volume"; -#endif // ENABLE_RELOAD_FROM_DISK_REWORK - -static constexpr const char* MESH_STAT_EDGES_FIXED = "edges_fixed"; -static constexpr const char* MESH_STAT_DEGENERATED_FACETS = "degenerate_facets"; -static constexpr const char* MESH_STAT_FACETS_REMOVED = "facets_removed"; -static constexpr const char* MESH_STAT_FACETS_RESERVED = "facets_reversed"; -static constexpr const char* MESH_STAT_BACKWARDS_EDGES = "backwards_edges"; - - -const unsigned int VALID_OBJECT_TYPES_COUNT = 1; -const char* VALID_OBJECT_TYPES[] = -{ - "model" -}; - -const char* INVALID_OBJECT_TYPES[] = -{ - "solidsupport", - "support", - "surface", - "other" -}; - -class version_error : public Slic3r::FileIOError -{ -public: - version_error(const std::string& what_arg) : Slic3r::FileIOError(what_arg) {} - version_error(const char* what_arg) : Slic3r::FileIOError(what_arg) {} -}; - -const char* get_attribute_value_charptr(const char** attributes, unsigned int attributes_size, const char* attribute_key) -{ - if ((attributes == nullptr) || (attributes_size == 0) || (attributes_size % 2 != 0) || (attribute_key == nullptr)) - return nullptr; - - for (unsigned int a = 0; a < attributes_size; a += 2) { - if (::strcmp(attributes[a], attribute_key) == 0) - return attributes[a + 1]; - } - - return nullptr; -} - -std::string get_attribute_value_string(const char** attributes, unsigned int attributes_size, const char* attribute_key) -{ - const char* text = get_attribute_value_charptr(attributes, attributes_size, attribute_key); - return (text != nullptr) ? text : ""; -} - -float get_attribute_value_float(const char** attributes, unsigned int attributes_size, const char* attribute_key) -{ - float value = 0.0f; - if (const char *text = get_attribute_value_charptr(attributes, attributes_size, attribute_key); text != nullptr) - fast_float::from_chars(text, text + strlen(text), value); - return value; -} - -int get_attribute_value_int(const char** attributes, unsigned int attributes_size, const char* attribute_key) -{ - int value = 0; - if (const char *text = get_attribute_value_charptr(attributes, attributes_size, attribute_key); text != nullptr) - boost::spirit::qi::parse(text, text + strlen(text), boost::spirit::qi::int_, value); - return value; -} - -bool get_attribute_value_bool(const char** attributes, unsigned int attributes_size, const char* attribute_key) -{ - const char* text = get_attribute_value_charptr(attributes, attributes_size, attribute_key); - return (text != nullptr) ? (bool)::atoi(text) : true; -} - -Slic3r::Transform3d get_transform_from_3mf_specs_string(const std::string& mat_str) -{ - // check: https://3mf.io/3d-manufacturing-format/ or https://github.com/3MFConsortium/spec_core/blob/master/3MF%20Core%20Specification.md - // to see how matrices are stored inside 3mf according to specifications - Slic3r::Transform3d ret = Slic3r::Transform3d::Identity(); - - if (mat_str.empty()) - // empty string means default identity matrix - return ret; - - std::vector mat_elements_str; - boost::split(mat_elements_str, mat_str, boost::is_any_of(" "), boost::token_compress_on); - - unsigned int size = (unsigned int)mat_elements_str.size(); - if (size != 12) - // invalid data, return identity matrix - return ret; - - unsigned int i = 0; - // matrices are stored into 3mf files as 4x3 - // we need to transpose them - for (unsigned int c = 0; c < 4; ++c) { - for (unsigned int r = 0; r < 3; ++r) { - ret(r, c) = ::atof(mat_elements_str[i++].c_str()); - } - } - return ret; -} - -float get_unit_factor(const std::string& unit) -{ - const char* text = unit.c_str(); - - if (::strcmp(text, "micron") == 0) - return 0.001f; - else if (::strcmp(text, "centimeter") == 0) - return 10.0f; - else if (::strcmp(text, "inch") == 0) - return 25.4f; - else if (::strcmp(text, "foot") == 0) - return 304.8f; - else if (::strcmp(text, "meter") == 0) - return 1000.0f; - else - // default "millimeters" (see specification) - return 1.0f; -} - -bool is_valid_object_type(const std::string& type) -{ - // if the type is empty defaults to "model" (see specification) - if (type.empty()) - return true; - - for (unsigned int i = 0; i < VALID_OBJECT_TYPES_COUNT; ++i) { - if (::strcmp(type.c_str(), VALID_OBJECT_TYPES[i]) == 0) - return true; - } - - return false; -} - -namespace Slic3r { - -//! macro used to mark string used at localization, -//! return same string -#define L(s) (s) -#define _(s) Slic3r::I18N::translate(s) - - // Base class with error messages management - class _3MF_Base - { - std::vector m_errors; - - protected: - void add_error(const std::string& error) { m_errors.push_back(error); } - void clear_errors() { m_errors.clear(); } - - public: - void log_errors() - { - for (const std::string& error : m_errors) - BOOST_LOG_TRIVIAL(error) << error; - } - }; - - class _3MF_Importer : public _3MF_Base - { - struct Component - { - int object_id; - Transform3d transform; - - explicit Component(int object_id) - : object_id(object_id) - , transform(Transform3d::Identity()) - { - } - - Component(int object_id, const Transform3d& transform) - : object_id(object_id) - , transform(transform) - { - } - }; - - typedef std::vector ComponentsList; - - struct Geometry - { - std::vector vertices; - std::vector triangles; - std::vector custom_supports; - std::vector custom_seam; - std::vector mmu_segmentation; - - bool empty() { return vertices.empty() || triangles.empty(); } - - void reset() { - vertices.clear(); - triangles.clear(); - custom_supports.clear(); - custom_seam.clear(); - mmu_segmentation.clear(); - } - }; - - struct CurrentObject - { - // ID of the object inside the 3MF file, 1 based. - int id; - // Index of the ModelObject in its respective Model, zero based. - int model_object_idx; - Geometry geometry; - ModelObject* object; - ComponentsList components; - - CurrentObject() { reset(); } - - void reset() { - id = -1; - model_object_idx = -1; - geometry.reset(); - object = nullptr; - components.clear(); - } - }; - - struct CurrentConfig - { - int object_id; - int volume_id; - }; - - struct Instance - { - ModelInstance* instance; - Transform3d transform; - - Instance(ModelInstance* instance, const Transform3d& transform) - : instance(instance) - , transform(transform) - { - } - }; - - struct Metadata - { - std::string key; - std::string value; - - Metadata(const std::string& key, const std::string& value) - : key(key) - , value(value) - { - } - }; - - typedef std::vector MetadataList; - - struct ObjectMetadata - { - struct VolumeMetadata - { - unsigned int first_triangle_id; - unsigned int last_triangle_id; - MetadataList metadata; - RepairedMeshErrors mesh_stats; - - VolumeMetadata(unsigned int first_triangle_id, unsigned int last_triangle_id) - : first_triangle_id(first_triangle_id) - , last_triangle_id(last_triangle_id) - { - } - }; - - typedef std::vector VolumeMetadataList; - - MetadataList metadata; - VolumeMetadataList volumes; - }; - - // Map from a 1 based 3MF object ID to a 0 based ModelObject index inside m_model->objects. - typedef std::map IdToModelObjectMap; - typedef std::map IdToAliasesMap; - typedef std::vector InstancesList; - typedef std::map IdToMetadataMap; - typedef std::map IdToGeometryMap; - 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; - bool m_check_version; - - // Semantic version of PrusaSlicer, that generated this 3MF. - boost::optional m_prusaslicer_generator_version; - unsigned int m_fdm_supports_painting_version = 0; - unsigned int m_seam_painting_version = 0; - unsigned int m_mm_painting_version = 0; - - XML_Parser m_xml_parser; - // Error code returned by the application side of the parser. In that case the expat may not reliably deliver the error state - // after returning from XML_Parse() function, thus we keep the error state here. - bool m_parse_error { false }; - std::string m_parse_error_message; - Model* m_model; - float m_unit_factor; - CurrentObject m_curr_object; - IdToModelObjectMap m_objects; - IdToAliasesMap m_objects_aliases; - InstancesList m_instances; - IdToGeometryMap m_geometries; - CurrentConfig m_curr_config; - IdToMetadataMap m_objects_metadata; - 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; - - public: - _3MF_Importer(); - ~_3MF_Importer(); - - bool load_model_from_file(const std::string& filename, Model& model, DynamicPrintConfig& config, ConfigSubstitutionContext& config_substitutions, bool check_version); - unsigned int version() const { return m_version; } - - private: - void _destroy_xml_parser(); - void _stop_xml_parser(const std::string& msg = std::string()); - - bool parse_error() const { return m_parse_error; } - const char* parse_error_message() const { - return m_parse_error ? - // The error was signalled by the user code, not the expat parser. - (m_parse_error_message.empty() ? "Invalid 3MF format" : m_parse_error_message.c_str()) : - // The error was signalled by the expat parser. - XML_ErrorString(XML_GetErrorCode(m_xml_parser)); - } - - bool _load_model_from_file(const std::string& filename, Model& model, DynamicPrintConfig& config, ConfigSubstitutionContext& config_substitutions); - bool _extract_model_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat); - 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, ConfigSubstitutionContext& config_substitutions); - 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); - - void _extract_print_config_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat, DynamicPrintConfig& config, ConfigSubstitutionContext& subs_context, const std::string& archive_filename); - bool _extract_model_config_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat, Model& model); - - // handlers to parse the .model file - void _handle_start_model_xml_element(const char* name, const char** attributes); - void _handle_end_model_xml_element(const char* name); - void _handle_model_xml_characters(const XML_Char* s, int len); - - // handlers to parse the MODEL_CONFIG_FILE file - void _handle_start_config_xml_element(const char* name, const char** attributes); - void _handle_end_config_xml_element(const char* name); - - bool _handle_start_model(const char** attributes, unsigned int num_attributes); - bool _handle_end_model(); - - bool _handle_start_resources(const char** attributes, unsigned int num_attributes); - bool _handle_end_resources(); - - bool _handle_start_object(const char** attributes, unsigned int num_attributes); - bool _handle_end_object(); - - bool _handle_start_mesh(const char** attributes, unsigned int num_attributes); - bool _handle_end_mesh(); - - bool _handle_start_vertices(const char** attributes, unsigned int num_attributes); - bool _handle_end_vertices(); - - bool _handle_start_vertex(const char** attributes, unsigned int num_attributes); - bool _handle_end_vertex(); - - bool _handle_start_triangles(const char** attributes, unsigned int num_attributes); - bool _handle_end_triangles(); - - bool _handle_start_triangle(const char** attributes, unsigned int num_attributes); - bool _handle_end_triangle(); - - bool _handle_start_components(const char** attributes, unsigned int num_attributes); - bool _handle_end_components(); - - bool _handle_start_component(const char** attributes, unsigned int num_attributes); - bool _handle_end_component(); - - bool _handle_start_build(const char** attributes, unsigned int num_attributes); - bool _handle_end_build(); - - bool _handle_start_item(const char** attributes, unsigned int num_attributes); - bool _handle_end_item(); - - bool _handle_start_metadata(const char** attributes, unsigned int num_attributes); - bool _handle_end_metadata(); - - bool _create_object_instance(int object_id, const Transform3d& transform, const bool printable, unsigned int recur_counter); - - void _apply_transform(ModelInstance& instance, const Transform3d& transform); - - bool _handle_start_config(const char** attributes, unsigned int num_attributes); - bool _handle_end_config(); - - bool _handle_start_config_object(const char** attributes, unsigned int num_attributes); - bool _handle_end_config_object(); - - bool _handle_start_config_volume(const char** attributes, unsigned int num_attributes); - bool _handle_start_config_volume_mesh(const char** attributes, unsigned int num_attributes); - bool _handle_end_config_volume(); - bool _handle_end_config_volume_mesh(); - - bool _handle_start_config_metadata(const char** attributes, unsigned int num_attributes); - bool _handle_end_config_metadata(); - - bool _generate_volumes(ModelObject& object, const Geometry& geometry, const ObjectMetadata::VolumeMetadataList& volumes, ConfigSubstitutionContext& config_substitutions); - - // callbacks to parse the .model file - static void XMLCALL _handle_start_model_xml_element(void* userData, const char* name, const char** attributes); - static void XMLCALL _handle_end_model_xml_element(void* userData, const char* name); - static void XMLCALL _handle_model_xml_characters(void* userData, const XML_Char* s, int len); - - // callbacks to parse the MODEL_CONFIG_FILE file - static void XMLCALL _handle_start_config_xml_element(void* userData, const char* name, const char** attributes); - static void XMLCALL _handle_end_config_xml_element(void* userData, const char* name); - }; - - _3MF_Importer::_3MF_Importer() - : m_version(0) - , m_check_version(false) - , m_xml_parser(nullptr) - , m_model(nullptr) - , m_unit_factor(1.0f) - , m_curr_metadata_name("") - , m_curr_characters("") - , m_name("") - { - } - - _3MF_Importer::~_3MF_Importer() - { - _destroy_xml_parser(); - } - - bool _3MF_Importer::load_model_from_file(const std::string& filename, Model& model, DynamicPrintConfig& config, ConfigSubstitutionContext& config_substitutions, bool check_version) - { - m_version = 0; - m_fdm_supports_painting_version = 0; - m_seam_painting_version = 0; - m_mm_painting_version = 0; - m_check_version = check_version; - m_model = &model; - m_unit_factor = 1.0f; - m_curr_object.reset(); - m_objects.clear(); - m_objects_aliases.clear(); - m_instances.clear(); - m_geometries.clear(); - m_curr_config.object_id = -1; - m_curr_config.volume_id = -1; - m_objects_metadata.clear(); - m_layer_heights_profiles.clear(); - m_layer_config_ranges.clear(); - m_sla_support_points.clear(); - m_curr_metadata_name.clear(); - m_curr_characters.clear(); - clear_errors(); - - return _load_model_from_file(filename, model, config, config_substitutions); - } - - void _3MF_Importer::_destroy_xml_parser() - { - if (m_xml_parser != nullptr) { - XML_ParserFree(m_xml_parser); - m_xml_parser = nullptr; - } - } - - void _3MF_Importer::_stop_xml_parser(const std::string &msg) - { - assert(! m_parse_error); - assert(m_parse_error_message.empty()); - assert(m_xml_parser != nullptr); - m_parse_error = true; - m_parse_error_message = msg; - XML_StopParser(m_xml_parser, false); - } - - bool _3MF_Importer::_load_model_from_file(const std::string& filename, Model& model, DynamicPrintConfig& config, ConfigSubstitutionContext& config_substitutions) - { - mz_zip_archive archive; - mz_zip_zero_struct(&archive); - - if (!open_zip_reader(&archive, filename)) { - add_error("Unable to open the file"); - return false; - } - - mz_uint num_entries = mz_zip_reader_get_num_files(&archive); - - mz_zip_archive_file_stat stat; - - m_name = boost::filesystem::path(filename).stem().string(); - - // we first loop the entries to read from the archive the .model file only, in order to extract the version from it - for (mz_uint i = 0; i < num_entries; ++i) { - if (mz_zip_reader_file_stat(&archive, i, &stat)) { - std::string name(stat.m_filename); - std::replace(name.begin(), name.end(), '\\', '/'); - - if (boost::algorithm::istarts_with(name, MODEL_FOLDER) && boost::algorithm::iends_with(name, MODEL_EXTENSION)) { - try - { - // valid model name -> extract model - if (!_extract_model_from_archive(archive, stat)) { - close_zip_reader(&archive); - add_error("Archive does not contain a valid model"); - return false; - } - } - catch (const std::exception& e) - { - // ensure the zip archive is closed and rethrow the exception - close_zip_reader(&archive); - throw Slic3r::FileIOError(e.what()); - } - } - } - } - - // we then loop again the entries to read other files stored in the archive - for (mz_uint i = 0; i < num_entries; ++i) { - if (mz_zip_reader_file_stat(&archive, i, &stat)) { - std::string name(stat.m_filename); - std::replace(name.begin(), name.end(), '\\', '/'); - - if (boost::algorithm::iequals(name, LAYER_HEIGHTS_PROFILE_FILE)) { - // extract slic3r layer heights profile file - _extract_layer_heights_profile_config_from_archive(archive, stat); - } - else if (boost::algorithm::iequals(name, LAYER_CONFIG_RANGES_FILE)) { - // extract slic3r layer config ranges file - _extract_layer_config_ranges_from_archive(archive, stat, config_substitutions); - } - else if (boost::algorithm::iequals(name, SLA_SUPPORT_POINTS_FILE)) { - // 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 - _extract_print_config_from_archive(archive, stat, config, config_substitutions, filename); - } - else if (boost::algorithm::iequals(name, CUSTOM_GCODE_PER_PRINT_Z_FILE)) { - // extract slic3r layer config ranges file - _extract_custom_gcode_per_print_z_from_archive(archive, stat); - } - else if (boost::algorithm::iequals(name, MODEL_CONFIG_FILE)) { - // extract slic3r model config file - if (!_extract_model_config_from_archive(archive, stat, model)) { - close_zip_reader(&archive); - add_error("Archive does not contain a valid model config"); - return false; - } - } - } - } - - close_zip_reader(&archive); - - if (m_version == 0) { - // if the 3mf was not produced by PrusaSlicer and there is more than one instance, - // split the object in as many objects as instances - size_t curr_models_count = m_model->objects.size(); - size_t i = 0; - while (i < curr_models_count) { - ModelObject* model_object = m_model->objects[i]; - if (model_object->instances.size() > 1) { - // select the geometry associated with the original model object - const Geometry* geometry = nullptr; - for (const IdToModelObjectMap::value_type& object : m_objects) { - if (object.second == int(i)) { - IdToGeometryMap::const_iterator obj_geometry = m_geometries.find(object.first); - if (obj_geometry == m_geometries.end()) { - add_error("Unable to find object geometry"); - return false; - } - geometry = &obj_geometry->second; - break; - } - } - - if (geometry == nullptr) { - add_error("Unable to find object geometry"); - return false; - } - - // use the geometry to create the volumes in the new model objects - ObjectMetadata::VolumeMetadataList volumes(1, { 0, (unsigned int)geometry->triangles.size() - 1 }); - - // for each instance after the 1st, create a new model object containing only that instance - // and copy into it the geometry - while (model_object->instances.size() > 1) { - ModelObject* new_model_object = m_model->add_object(*model_object); - new_model_object->clear_instances(); - new_model_object->add_instance(*model_object->instances.back()); - model_object->delete_last_instance(); - if (!_generate_volumes(*new_model_object, *geometry, volumes, config_substitutions)) - return false; - } - } - ++i; - } - } - - for (const IdToModelObjectMap::value_type& object : m_objects) { - if (object.second >= int(m_model->objects.size())) { - add_error("Unable to find object"); - return false; - } - ModelObject* model_object = m_model->objects[object.second]; - IdToGeometryMap::const_iterator obj_geometry = m_geometries.find(object.first); - if (obj_geometry == m_geometries.end()) { - add_error("Unable to find object geometry"); - return false; - } - - // m_layer_heights_profiles are indexed by a 1 based model object index. - IdToLayerHeightsProfileMap::iterator obj_layer_heights_profile = m_layer_heights_profiles.find(object.second + 1); - if (obj_layer_heights_profile != m_layer_heights_profiles.end()) - model_object->layer_height_profile.set(std::move(obj_layer_heights_profile->second)); - - // m_layer_config_ranges are indexed by a 1 based model object index. - IdToLayerConfigRangesMap::iterator obj_layer_config_ranges = m_layer_config_ranges.find(object.second + 1); - if (obj_layer_config_ranges != m_layer_config_ranges.end()) - model_object->layer_config_ranges = std::move(obj_layer_config_ranges->second); - - // m_sla_support_points are indexed by a 1 based model object index. - IdToSlaSupportPointsMap::iterator obj_sla_support_points = m_sla_support_points.find(object.second + 1); - if (obj_sla_support_points != m_sla_support_points.end() && !obj_sla_support_points->second.empty()) { - model_object->sla_support_points = std::move(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 = std::move(obj_drain_holes->second); - } - - ObjectMetadata::VolumeMetadataList volumes; - ObjectMetadata::VolumeMetadataList* volumes_ptr = nullptr; - - IdToMetadataMap::iterator obj_metadata = m_objects_metadata.find(object.first); - if (obj_metadata != m_objects_metadata.end()) { - // config data has been found, this model was saved using slic3r pe - - // apply object's name and config data - for (const Metadata& metadata : obj_metadata->second.metadata) { - if (metadata.key == "name") - model_object->name = metadata.value; - else - model_object->config.set_deserialize(metadata.key, metadata.value, config_substitutions); - } - - // select object's detected volumes - volumes_ptr = &obj_metadata->second.volumes; - } - else { - // config data not found, this model was not saved using slic3r pe - - // add the entire geometry as the single volume to generate - volumes.emplace_back(0, (int)obj_geometry->second.triangles.size() - 1); - - // select as volumes - volumes_ptr = &volumes; - } - - if (!_generate_volumes(*model_object, obj_geometry->second, *volumes_ptr, config_substitutions)) - return false; - } - -#if ENABLE_RELOAD_FROM_DISK_REWORK - for (int obj_id = 0; obj_id < int(model.objects.size()); ++obj_id) { - ModelObject* o = model.objects[obj_id]; - for (int vol_id = 0; vol_id < int(o->volumes.size()); ++vol_id) { - ModelVolume* v = o->volumes[vol_id]; - if (v->source.input_file.empty()) - v->source.input_file = v->name.empty() ? filename : v->name; - if (v->source.volume_idx == -1) - v->source.volume_idx = vol_id; - if (v->source.object_idx == -1) - v->source.object_idx = obj_id; - } - } -#else - int object_idx = 0; - for (ModelObject* o : model.objects) { - int volume_idx = 0; - for (ModelVolume* v : o->volumes) { - if (v->source.input_file.empty() && v->type() == ModelVolumeType::MODEL_PART) { - v->source.input_file = filename; - if (v->source.volume_idx == -1) - v->source.volume_idx = volume_idx; - if (v->source.object_idx == -1) - v->source.object_idx = object_idx; - } - ++volume_idx; - } - ++object_idx; - } -#endif // ENABLE_RELOAD_FROM_DISK_REWORK - -// // fixes the min z of the model if negative -// model.adjust_min_z(); - - return true; - } - - bool _3MF_Importer::_extract_model_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat) - { - if (stat.m_uncomp_size == 0) { - add_error("Found invalid size"); - return false; - } - - _destroy_xml_parser(); - - m_xml_parser = XML_ParserCreate(nullptr); - if (m_xml_parser == nullptr) { - add_error("Unable to create parser"); - return false; - } - - XML_SetUserData(m_xml_parser, (void*)this); - XML_SetElementHandler(m_xml_parser, _3MF_Importer::_handle_start_model_xml_element, _3MF_Importer::_handle_end_model_xml_element); - XML_SetCharacterDataHandler(m_xml_parser, _3MF_Importer::_handle_model_xml_characters); - - struct CallbackData - { - XML_Parser& parser; - _3MF_Importer& importer; - const mz_zip_archive_file_stat& stat; - - CallbackData(XML_Parser& parser, _3MF_Importer& importer, const mz_zip_archive_file_stat& stat) : parser(parser), importer(importer), stat(stat) {} - }; - - CallbackData data(m_xml_parser, *this, stat); - - mz_bool res = 0; - - try - { - res = mz_zip_reader_extract_file_to_callback(&archive, stat.m_filename, [](void* pOpaque, mz_uint64 file_ofs, const void* pBuf, size_t n)->size_t { - CallbackData* data = (CallbackData*)pOpaque; - if (!XML_Parse(data->parser, (const char*)pBuf, (int)n, (file_ofs + n == data->stat.m_uncomp_size) ? 1 : 0) || data->importer.parse_error()) { - char error_buf[1024]; - ::sprintf(error_buf, "Error (%s) while parsing '%s' at line %d", data->importer.parse_error_message(), data->stat.m_filename, (int)XML_GetCurrentLineNumber(data->parser)); - throw Slic3r::FileIOError(error_buf); - } - - return n; - }, &data, 0); - } - catch (const version_error& e) - { - // rethrow the exception - throw Slic3r::FileIOError(e.what()); - } - catch (std::exception& e) - { - add_error(e.what()); - return false; - } - - if (res == 0) { - add_error("Error while extracting model data from zip archive"); - return false; - } - - return true; - } - - void _3MF_Importer::_extract_print_config_from_archive( - mz_zip_archive& archive, const mz_zip_archive_file_stat& stat, - DynamicPrintConfig& config, ConfigSubstitutionContext& config_substitutions, - const std::string& archive_filename) - { - 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 config data to buffer"); - return; - } - //FIXME Loading a "will be one day a legacy format" of configuration in a form of a G-code comment. - // Each config line is prefixed with a semicolon (G-code comment), that is ugly. - - // Replacing the legacy function with load_from_ini_string_commented leads to issues when - // parsing 3MFs from before PrusaSlicer 2.0.0 (which can have duplicated entries in the INI. - // See https://github.com/prusa3d/PrusaSlicer/issues/7155. We'll revert it for now. - //config_substitutions.substitutions = config.load_from_ini_string_commented(std::move(buffer), config_substitutions.rule); - ConfigBase::load_from_gcode_string_legacy(config, buffer.data(), config_substitutions); - } - } - - void _3MF_Importer::_extract_layer_heights_profile_config_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 layer heights profile 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); - - 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; - } - - IdToLayerHeightsProfileMap::iterator object_item = m_layer_heights_profiles.find(object_id); - if (object_item != m_layer_heights_profiles.end()) { - add_error("Found duplicated layer heights profile"); - continue; - } - - std::vector object_data_profile; - boost::split(object_data_profile, object_data[1], boost::is_any_of(";"), boost::token_compress_off); - if (object_data_profile.size() <= 4 || object_data_profile.size() % 2 != 0) { - add_error("Found invalid layer heights profile"); - continue; - } - - std::vector profile; - profile.reserve(object_data_profile.size()); - - for (const std::string& value : object_data_profile) { - profile.push_back((coordf_t)std::atof(value.c_str())); - } - - m_layer_heights_profiles.insert({ object_id, profile }); - } - } - } - - void _3MF_Importer::_extract_layer_config_ranges_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat, ConfigSubstitutionContext& config_substitutions) - { - 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 layer config ranges data to buffer"); - return; - } - - std::istringstream iss(buffer); // wrap returned xml to istringstream - pt::ptree objects_tree; - pt::read_xml(iss, objects_tree); - - for (const auto& object : objects_tree.get_child("objects")) { - pt::ptree object_tree = object.second; - int obj_idx = object_tree.get(".id", -1); - if (obj_idx <= 0) { - add_error("Found invalid object id"); - continue; - } - - IdToLayerConfigRangesMap::iterator object_item = m_layer_config_ranges.find(obj_idx); - if (object_item != m_layer_config_ranges.end()) { - add_error("Found duplicated layer config range"); - continue; - } - - t_layer_config_ranges config_ranges; - - for (const auto& range : object_tree) { - if (range.first != "range") - continue; - pt::ptree range_tree = range.second; - double min_z = range_tree.get(".min_z"); - double max_z = range_tree.get(".max_z"); - - // get Z range information - DynamicPrintConfig config; - - for (const auto& option : range_tree) { - if (option.first != "option") - continue; - std::string opt_key = option.second.get(".opt_key"); - std::string value = option.second.data(); - config.set_deserialize(opt_key, value, config_substitutions); - } - - config_ranges[{ min_z, max_z }].assign_config(std::move(config)); - } - - if (!config_ranges.empty()) - m_layer_config_ranges.insert({ obj_idx, std::move(config_ranges) }); - } - } - } - - void _3MF_Importer::_extract_sla_support_points_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("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 - } - - 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; - } - - IdToSlaSupportPointsMap::iterator object_item = m_sla_support_points.find(object_id); - if (object_item != m_sla_support_points.end()) { - add_error("Found duplicated SLA support points"); - continue; - } - - std::vector object_data_points; - boost::split(object_data_points, object_data[1], boost::is_any_of(" "), boost::token_compress_off); - - std::vector sla_support_points; - - if (version == 0) { - for (unsigned int i=0; i 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; i 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 custom Gcodes per height data to buffer"); - return; - } - - std::istringstream iss(buffer); // wrap returned xml to istringstream - pt::ptree main_tree; - pt::read_xml(iss, main_tree); - - if (main_tree.front().first != "custom_gcodes_per_print_z") - return; - pt::ptree code_tree = main_tree.front().second; - - m_model->custom_gcode_per_print_z.gcodes.clear(); - - for (const auto& code : code_tree) { - if (code.first == "mode") { - pt::ptree tree = code.second; - std::string mode = tree.get(".value"); - m_model->custom_gcode_per_print_z.mode = mode == CustomGCode::SingleExtruderMode ? CustomGCode::Mode::SingleExtruder : - mode == CustomGCode::MultiAsSingleMode ? CustomGCode::Mode::MultiAsSingle : - CustomGCode::Mode::MultiExtruder; - } - if (code.first != "code") - continue; - - pt::ptree tree = code.second; - double print_z = tree.get (".print_z" ); - int extruder = tree.get (".extruder"); - std::string color = tree.get (".color" ); - - CustomGCode::Type type; - std::string extra; - pt::ptree attr_tree = tree.find("")->second; - if (attr_tree.find("type") == attr_tree.not_found()) { - // It means that data was saved in old version (2.2.0 and older) of PrusaSlicer - // read old data ... - std::string gcode = tree.get (".gcode"); - // ... and interpret them to the new data - type = gcode == "M600" ? CustomGCode::ColorChange : - gcode == "M601" ? CustomGCode::PausePrint : - gcode == "tool_change" ? CustomGCode::ToolChange : CustomGCode::Custom; - extra = type == CustomGCode::PausePrint ? color : - type == CustomGCode::Custom ? gcode : ""; - } - else { - type = static_cast(tree.get(".type")); - extra = tree.get(".extra"); - } - m_model->custom_gcode_per_print_z.gcodes.push_back(CustomGCode::Item{print_z, type, extruder, color, extra}) ; - } - } - } - - void _3MF_Importer::_handle_start_model_xml_element(const char* name, const char** attributes) - { - if (m_xml_parser == nullptr) - return; - - bool res = true; - unsigned int num_attributes = (unsigned int)XML_GetSpecifiedAttributeCount(m_xml_parser); - - if (::strcmp(MODEL_TAG, name) == 0) - res = _handle_start_model(attributes, num_attributes); - else if (::strcmp(RESOURCES_TAG, name) == 0) - res = _handle_start_resources(attributes, num_attributes); - else if (::strcmp(OBJECT_TAG, name) == 0) - res = _handle_start_object(attributes, num_attributes); - else if (::strcmp(MESH_TAG, name) == 0) - res = _handle_start_mesh(attributes, num_attributes); - else if (::strcmp(VERTICES_TAG, name) == 0) - res = _handle_start_vertices(attributes, num_attributes); - else if (::strcmp(VERTEX_TAG, name) == 0) - res = _handle_start_vertex(attributes, num_attributes); - else if (::strcmp(TRIANGLES_TAG, name) == 0) - res = _handle_start_triangles(attributes, num_attributes); - else if (::strcmp(TRIANGLE_TAG, name) == 0) - res = _handle_start_triangle(attributes, num_attributes); - else if (::strcmp(COMPONENTS_TAG, name) == 0) - res = _handle_start_components(attributes, num_attributes); - else if (::strcmp(COMPONENT_TAG, name) == 0) - res = _handle_start_component(attributes, num_attributes); - else if (::strcmp(BUILD_TAG, name) == 0) - res = _handle_start_build(attributes, num_attributes); - else if (::strcmp(ITEM_TAG, name) == 0) - res = _handle_start_item(attributes, num_attributes); - else if (::strcmp(METADATA_TAG, name) == 0) - res = _handle_start_metadata(attributes, num_attributes); - - if (!res) - _stop_xml_parser(); - } - - void _3MF_Importer::_handle_end_model_xml_element(const char* name) - { - if (m_xml_parser == nullptr) - return; - - bool res = true; - - if (::strcmp(MODEL_TAG, name) == 0) - res = _handle_end_model(); - else if (::strcmp(RESOURCES_TAG, name) == 0) - res = _handle_end_resources(); - else if (::strcmp(OBJECT_TAG, name) == 0) - res = _handle_end_object(); - else if (::strcmp(MESH_TAG, name) == 0) - res = _handle_end_mesh(); - else if (::strcmp(VERTICES_TAG, name) == 0) - res = _handle_end_vertices(); - else if (::strcmp(VERTEX_TAG, name) == 0) - res = _handle_end_vertex(); - else if (::strcmp(TRIANGLES_TAG, name) == 0) - res = _handle_end_triangles(); - else if (::strcmp(TRIANGLE_TAG, name) == 0) - res = _handle_end_triangle(); - else if (::strcmp(COMPONENTS_TAG, name) == 0) - res = _handle_end_components(); - else if (::strcmp(COMPONENT_TAG, name) == 0) - res = _handle_end_component(); - else if (::strcmp(BUILD_TAG, name) == 0) - res = _handle_end_build(); - else if (::strcmp(ITEM_TAG, name) == 0) - res = _handle_end_item(); - else if (::strcmp(METADATA_TAG, name) == 0) - res = _handle_end_metadata(); - - if (!res) - _stop_xml_parser(); - } - - void _3MF_Importer::_handle_model_xml_characters(const XML_Char* s, int len) - { - m_curr_characters.append(s, len); - } - - void _3MF_Importer::_handle_start_config_xml_element(const char* name, const char** attributes) - { - if (m_xml_parser == nullptr) - return; - - bool res = true; - unsigned int num_attributes = (unsigned int)XML_GetSpecifiedAttributeCount(m_xml_parser); - - if (::strcmp(CONFIG_TAG, name) == 0) - res = _handle_start_config(attributes, num_attributes); - else if (::strcmp(OBJECT_TAG, name) == 0) - res = _handle_start_config_object(attributes, num_attributes); - else if (::strcmp(VOLUME_TAG, name) == 0) - res = _handle_start_config_volume(attributes, num_attributes); - else if (::strcmp(MESH_TAG, name) == 0) - res = _handle_start_config_volume_mesh(attributes, num_attributes); - else if (::strcmp(METADATA_TAG, name) == 0) - res = _handle_start_config_metadata(attributes, num_attributes); - - if (!res) - _stop_xml_parser(); - } - - void _3MF_Importer::_handle_end_config_xml_element(const char* name) - { - if (m_xml_parser == nullptr) - return; - - bool res = true; - - if (::strcmp(CONFIG_TAG, name) == 0) - res = _handle_end_config(); - else if (::strcmp(OBJECT_TAG, name) == 0) - res = _handle_end_config_object(); - else if (::strcmp(VOLUME_TAG, name) == 0) - res = _handle_end_config_volume(); - else if (::strcmp(MESH_TAG, name) == 0) - res = _handle_end_config_volume_mesh(); - else if (::strcmp(METADATA_TAG, name) == 0) - res = _handle_end_config_metadata(); - - if (!res) - _stop_xml_parser(); - } - - bool _3MF_Importer::_handle_start_model(const char** attributes, unsigned int num_attributes) - { - m_unit_factor = get_unit_factor(get_attribute_value_string(attributes, num_attributes, UNIT_ATTR)); - return true; - } - - bool _3MF_Importer::_handle_end_model() - { - // deletes all non-built or non-instanced objects - for (const IdToModelObjectMap::value_type& object : m_objects) { - if (object.second >= int(m_model->objects.size())) { - add_error("Unable to find object"); - return false; - } - ModelObject *model_object = m_model->objects[object.second]; - if (model_object != nullptr && model_object->instances.size() == 0) - m_model->delete_object(model_object); - } - - if (m_version == 0) { - // if the 3mf was not produced by PrusaSlicer and there is only one object, - // set the object name to match the filename - if (m_model->objects.size() == 1) - m_model->objects.front()->name = m_name; - } - - // applies instances' matrices - for (Instance& instance : m_instances) { - if (instance.instance != nullptr && instance.instance->get_object() != nullptr) - // apply the transform to the instance - _apply_transform(*instance.instance, instance.transform); - } - - return true; - } - - bool _3MF_Importer::_handle_start_resources(const char** attributes, unsigned int num_attributes) - { - // do nothing - return true; - } - - bool _3MF_Importer::_handle_end_resources() - { - // do nothing - return true; - } - - bool _3MF_Importer::_handle_start_object(const char** attributes, unsigned int num_attributes) - { - // reset current data - m_curr_object.reset(); - - if (is_valid_object_type(get_attribute_value_string(attributes, num_attributes, TYPE_ATTR))) { - // create new object (it may be removed later if no instances are generated from it) - m_curr_object.model_object_idx = (int)m_model->objects.size(); - m_curr_object.object = m_model->add_object(); - if (m_curr_object.object == nullptr) { - add_error("Unable to create object"); - return false; - } - - // set object data - m_curr_object.object->name = get_attribute_value_string(attributes, num_attributes, NAME_ATTR); - if (m_curr_object.object->name.empty()) - m_curr_object.object->name = m_name + "_" + std::to_string(m_model->objects.size()); - - m_curr_object.id = get_attribute_value_int(attributes, num_attributes, ID_ATTR); - } - - return true; - } - - bool _3MF_Importer::_handle_end_object() - { - if (m_curr_object.object != nullptr) { - if (m_curr_object.geometry.empty()) { - // no geometry defined - // remove the object from the model - m_model->delete_object(m_curr_object.object); - - if (m_curr_object.components.empty()) { - // no components defined -> invalid object, delete it - IdToModelObjectMap::iterator object_item = m_objects.find(m_curr_object.id); - if (object_item != m_objects.end()) - m_objects.erase(object_item); - - IdToAliasesMap::iterator alias_item = m_objects_aliases.find(m_curr_object.id); - if (alias_item != m_objects_aliases.end()) - m_objects_aliases.erase(alias_item); - } - else - // adds components to aliases - m_objects_aliases.insert({ m_curr_object.id, m_curr_object.components }); - } - else { - // geometry defined, store it for later use - m_geometries.insert({ m_curr_object.id, std::move(m_curr_object.geometry) }); - - // stores the object for later use - if (m_objects.find(m_curr_object.id) == m_objects.end()) { - m_objects.insert({ m_curr_object.id, m_curr_object.model_object_idx }); - m_objects_aliases.insert({ m_curr_object.id, { 1, Component(m_curr_object.id) } }); // aliases itself - } - else { - add_error("Found object with duplicate id"); - return false; - } - } - } - - return true; - } - - bool _3MF_Importer::_handle_start_mesh(const char** attributes, unsigned int num_attributes) - { - // reset current geometry - m_curr_object.geometry.reset(); - return true; - } - - bool _3MF_Importer::_handle_end_mesh() - { - // do nothing - return true; - } - - bool _3MF_Importer::_handle_start_vertices(const char** attributes, unsigned int num_attributes) - { - // reset current vertices - m_curr_object.geometry.vertices.clear(); - return true; - } - - bool _3MF_Importer::_handle_end_vertices() - { - // do nothing - return true; - } - - bool _3MF_Importer::_handle_start_vertex(const char** attributes, unsigned int num_attributes) - { - // appends the vertex coordinates - // missing values are set equal to ZERO - m_curr_object.geometry.vertices.emplace_back( - m_unit_factor * get_attribute_value_float(attributes, num_attributes, X_ATTR), - m_unit_factor * get_attribute_value_float(attributes, num_attributes, Y_ATTR), - m_unit_factor * get_attribute_value_float(attributes, num_attributes, Z_ATTR)); - return true; - } - - bool _3MF_Importer::_handle_end_vertex() - { - // do nothing - return true; - } - - bool _3MF_Importer::_handle_start_triangles(const char** attributes, unsigned int num_attributes) - { - // reset current triangles - m_curr_object.geometry.triangles.clear(); - return true; - } - - bool _3MF_Importer::_handle_end_triangles() - { - // do nothing - return true; - } - - bool _3MF_Importer::_handle_start_triangle(const char** attributes, unsigned int num_attributes) - { - // we are ignoring the following attributes: - // p1 - // p2 - // p3 - // pid - // see specifications - - // appends the triangle's vertices indices - // missing values are set equal to ZERO - m_curr_object.geometry.triangles.emplace_back( - get_attribute_value_int(attributes, num_attributes, V1_ATTR), - get_attribute_value_int(attributes, num_attributes, V2_ATTR), - get_attribute_value_int(attributes, num_attributes, V3_ATTR)); - - m_curr_object.geometry.custom_supports.push_back(get_attribute_value_string(attributes, num_attributes, CUSTOM_SUPPORTS_ATTR)); - m_curr_object.geometry.custom_seam.push_back(get_attribute_value_string(attributes, num_attributes, CUSTOM_SEAM_ATTR)); - m_curr_object.geometry.mmu_segmentation.push_back(get_attribute_value_string(attributes, num_attributes, MMU_SEGMENTATION_ATTR)); - return true; - } - - bool _3MF_Importer::_handle_end_triangle() - { - // do nothing - return true; - } - - bool _3MF_Importer::_handle_start_components(const char** attributes, unsigned int num_attributes) - { - // reset current components - m_curr_object.components.clear(); - return true; - } - - bool _3MF_Importer::_handle_end_components() - { - // do nothing - return true; - } - - bool _3MF_Importer::_handle_start_component(const char** attributes, unsigned int num_attributes) - { - int object_id = get_attribute_value_int(attributes, num_attributes, OBJECTID_ATTR); - Transform3d transform = get_transform_from_3mf_specs_string(get_attribute_value_string(attributes, num_attributes, TRANSFORM_ATTR)); - - IdToModelObjectMap::iterator object_item = m_objects.find(object_id); - if (object_item == m_objects.end()) { - IdToAliasesMap::iterator alias_item = m_objects_aliases.find(object_id); - if (alias_item == m_objects_aliases.end()) { - add_error("Found component with invalid object id"); - return false; - } - } - - m_curr_object.components.emplace_back(object_id, transform); - - return true; - } - - bool _3MF_Importer::_handle_end_component() - { - // do nothing - return true; - } - - bool _3MF_Importer::_handle_start_build(const char** attributes, unsigned int num_attributes) - { - // do nothing - return true; - } - - bool _3MF_Importer::_handle_end_build() - { - // do nothing - return true; - } - - bool _3MF_Importer::_handle_start_item(const char** attributes, unsigned int num_attributes) - { - // we are ignoring the following attributes - // thumbnail - // partnumber - // pid - // pindex - // see specifications - - int object_id = get_attribute_value_int(attributes, num_attributes, OBJECTID_ATTR); - Transform3d transform = get_transform_from_3mf_specs_string(get_attribute_value_string(attributes, num_attributes, TRANSFORM_ATTR)); - int printable = get_attribute_value_bool(attributes, num_attributes, PRINTABLE_ATTR); - - return _create_object_instance(object_id, transform, printable, 1); - } - - bool _3MF_Importer::_handle_end_item() - { - // do nothing - return true; - } - - bool _3MF_Importer::_handle_start_metadata(const char** attributes, unsigned int num_attributes) - { - m_curr_characters.clear(); - - std::string name = get_attribute_value_string(attributes, num_attributes, NAME_ATTR); - if (!name.empty()) - m_curr_metadata_name = name; - - return true; - } - - inline static void check_painting_version(unsigned int loaded_version, unsigned int highest_supported_version, const std::string &error_msg) - { - if (loaded_version > highest_supported_version) - throw version_error(error_msg); - } - - bool _3MF_Importer::_handle_end_metadata() - { - if (m_curr_metadata_name == SLIC3RPE_3MF_VERSION) { - m_version = (unsigned int)atoi(m_curr_characters.c_str()); - if (m_check_version && (m_version > VERSION_3MF_COMPATIBLE)) { - // std::string msg = _(L("The selected 3mf file has been saved with a newer version of " + std::string(SLIC3R_APP_NAME) + " and is not compatible.")); - // throw version_error(msg.c_str()); - const std::string msg = (boost::format(_(L("The selected 3mf file has been saved with a newer version of %1% and is not compatible."))) % std::string(SLIC3R_APP_NAME)).str(); - throw version_error(msg); - } - } else if (m_curr_metadata_name == "Application") { - // Generator application of the 3MF. - // SLIC3R_APP_KEY - SLIC3R_VERSION - if (boost::starts_with(m_curr_characters, "PrusaSlicer-")) - m_prusaslicer_generator_version = Semver::parse(m_curr_characters.substr(12)); - } else if (m_curr_metadata_name == SLIC3RPE_FDM_SUPPORTS_PAINTING_VERSION) { - m_fdm_supports_painting_version = (unsigned int) atoi(m_curr_characters.c_str()); - check_painting_version(m_fdm_supports_painting_version, FDM_SUPPORTS_PAINTING_VERSION, - _(L("The selected 3MF contains FDM supports painted object using a newer version of PrusaSlicer and is not compatible."))); - } else if (m_curr_metadata_name == SLIC3RPE_SEAM_PAINTING_VERSION) { - m_seam_painting_version = (unsigned int) atoi(m_curr_characters.c_str()); - check_painting_version(m_seam_painting_version, SEAM_PAINTING_VERSION, - _(L("The selected 3MF contains seam painted object using a newer version of PrusaSlicer and is not compatible."))); - } else if (m_curr_metadata_name == SLIC3RPE_MM_PAINTING_VERSION) { - m_mm_painting_version = (unsigned int) atoi(m_curr_characters.c_str()); - check_painting_version(m_mm_painting_version, MM_PAINTING_VERSION, - _(L("The selected 3MF contains multi-material painted object using a newer version of PrusaSlicer and is not compatible."))); - } - - return true; - } - - bool _3MF_Importer::_create_object_instance(int object_id, const Transform3d& transform, const bool printable, unsigned int recur_counter) - { - static const unsigned int MAX_RECURSIONS = 10; - - // escape from circular aliasing - if (recur_counter > MAX_RECURSIONS) { - add_error("Too many recursions"); - return false; - } - - IdToAliasesMap::iterator it = m_objects_aliases.find(object_id); - if (it == m_objects_aliases.end()) { - add_error("Found item with invalid object id"); - return false; - } - - if (it->second.size() == 1 && it->second[0].object_id == object_id) { - // aliasing to itself - - IdToModelObjectMap::iterator object_item = m_objects.find(object_id); - if (object_item == m_objects.end() || object_item->second == -1) { - add_error("Found invalid object"); - return false; - } - else { - ModelInstance* instance = m_model->objects[object_item->second]->add_instance(); - if (instance == nullptr) { - add_error("Unable to add object instance"); - return false; - } - instance->printable = printable; - - m_instances.emplace_back(instance, transform); - } - } - else { - // recursively process nested components - for (const Component& component : it->second) { - if (!_create_object_instance(component.object_id, transform * component.transform, printable, recur_counter + 1)) - return false; - } - } - - return true; - } - - void _3MF_Importer::_apply_transform(ModelInstance& instance, const Transform3d& transform) - { - Slic3r::Geometry::Transformation t(transform); - // invalid scale value, return - if (!t.get_scaling_factor().all()) - return; - - instance.set_transformation(t); - } - - bool _3MF_Importer::_handle_start_config(const char** attributes, unsigned int num_attributes) - { - // do nothing - return true; - } - - bool _3MF_Importer::_handle_end_config() - { - // do nothing - return true; - } - - bool _3MF_Importer::_handle_start_config_object(const char** attributes, unsigned int num_attributes) - { - int object_id = get_attribute_value_int(attributes, num_attributes, ID_ATTR); - IdToMetadataMap::iterator object_item = m_objects_metadata.find(object_id); - if (object_item != m_objects_metadata.end()) { - add_error("Found duplicated object id"); - return false; - } - - // Added because of github #3435, currently not used by PrusaSlicer - // int instances_count_id = get_attribute_value_int(attributes, num_attributes, INSTANCESCOUNT_ATTR); - - m_objects_metadata.insert({ object_id, ObjectMetadata() }); - m_curr_config.object_id = object_id; - return true; - } - - bool _3MF_Importer::_handle_end_config_object() - { - // do nothing - return true; - } - - bool _3MF_Importer::_handle_start_config_volume(const char** attributes, unsigned int num_attributes) - { - IdToMetadataMap::iterator object = m_objects_metadata.find(m_curr_config.object_id); - if (object == m_objects_metadata.end()) { - add_error("Cannot assign volume to a valid object"); - return false; - } - - m_curr_config.volume_id = (int)object->second.volumes.size(); - - unsigned int first_triangle_id = (unsigned int)get_attribute_value_int(attributes, num_attributes, FIRST_TRIANGLE_ID_ATTR); - unsigned int last_triangle_id = (unsigned int)get_attribute_value_int(attributes, num_attributes, LAST_TRIANGLE_ID_ATTR); - - object->second.volumes.emplace_back(first_triangle_id, last_triangle_id); - return true; - } - - bool _3MF_Importer::_handle_start_config_volume_mesh(const char** attributes, unsigned int num_attributes) - { - IdToMetadataMap::iterator object = m_objects_metadata.find(m_curr_config.object_id); - if (object == m_objects_metadata.end()) { - add_error("Cannot assign volume mesh to a valid object"); - return false; - } - if (object->second.volumes.empty()) { - add_error("Cannot assign mesh to a valid olume"); - return false; - } - - ObjectMetadata::VolumeMetadata& volume = object->second.volumes.back(); - - int edges_fixed = get_attribute_value_int(attributes, num_attributes, MESH_STAT_EDGES_FIXED ); - int degenerate_facets = get_attribute_value_int(attributes, num_attributes, MESH_STAT_DEGENERATED_FACETS); - int facets_removed = get_attribute_value_int(attributes, num_attributes, MESH_STAT_FACETS_REMOVED ); - int facets_reversed = get_attribute_value_int(attributes, num_attributes, MESH_STAT_FACETS_RESERVED ); - int backwards_edges = get_attribute_value_int(attributes, num_attributes, MESH_STAT_BACKWARDS_EDGES ); - - volume.mesh_stats = { edges_fixed, degenerate_facets, facets_removed, facets_reversed, backwards_edges }; - - return true; - } - - bool _3MF_Importer::_handle_end_config_volume() - { - // do nothing - return true; - } - - bool _3MF_Importer::_handle_end_config_volume_mesh() - { - // do nothing - return true; - } - - bool _3MF_Importer::_handle_start_config_metadata(const char** attributes, unsigned int num_attributes) - { - IdToMetadataMap::iterator object = m_objects_metadata.find(m_curr_config.object_id); - if (object == m_objects_metadata.end()) { - add_error("Cannot assign metadata to valid object id"); - return false; - } - - std::string type = get_attribute_value_string(attributes, num_attributes, TYPE_ATTR); - std::string key = get_attribute_value_string(attributes, num_attributes, KEY_ATTR); - std::string value = get_attribute_value_string(attributes, num_attributes, VALUE_ATTR); - - if (type == OBJECT_TYPE) - object->second.metadata.emplace_back(key, value); - else if (type == VOLUME_TYPE) { - if (size_t(m_curr_config.volume_id) < object->second.volumes.size()) - object->second.volumes[m_curr_config.volume_id].metadata.emplace_back(key, value); - } - else { - add_error("Found invalid metadata type"); - return false; - } - - return true; - } - - bool _3MF_Importer::_handle_end_config_metadata() - { - // do nothing - return true; - } - - bool _3MF_Importer::_generate_volumes(ModelObject& object, const Geometry& geometry, const ObjectMetadata::VolumeMetadataList& volumes, ConfigSubstitutionContext& config_substitutions) - { - if (!object.volumes.empty()) { - add_error("Found invalid volumes count"); - return false; - } - - unsigned int geo_tri_count = (unsigned int)geometry.triangles.size(); - unsigned int renamed_volumes_count = 0; - - for (const ObjectMetadata::VolumeMetadata& volume_data : volumes) { - if (geo_tri_count <= volume_data.first_triangle_id || geo_tri_count <= volume_data.last_triangle_id || volume_data.last_triangle_id < volume_data.first_triangle_id) { - add_error("Found invalid triangle id"); - return false; - } - - Transform3d volume_matrix_to_object = Transform3d::Identity(); - bool has_transform = false; - // extract the volume transformation from the volume's metadata, if present - for (const Metadata& metadata : volume_data.metadata) { - if (metadata.key == MATRIX_KEY) { - volume_matrix_to_object = Slic3r::Geometry::transform3d_from_string(metadata.value); - has_transform = ! volume_matrix_to_object.isApprox(Transform3d::Identity(), 1e-10); - break; - } - } - - // splits volume out of imported geometry - indexed_triangle_set its; - its.indices.assign(geometry.triangles.begin() + volume_data.first_triangle_id, geometry.triangles.begin() + volume_data.last_triangle_id + 1); - const size_t triangles_count = its.indices.size(); - if (triangles_count == 0) { - add_error("An empty triangle mesh found"); - return false; - } - - { - int min_id = its.indices.front()[0]; - int max_id = min_id; - for (const Vec3i& face : its.indices) { - for (const int tri_id : face) { - if (tri_id < 0 || tri_id >= int(geometry.vertices.size())) { - add_error("Found invalid vertex id"); - return false; - } - min_id = std::min(min_id, tri_id); - max_id = std::max(max_id, tri_id); - } - } - its.vertices.assign(geometry.vertices.begin() + min_id, geometry.vertices.begin() + max_id + 1); - - // rebase indices to the current vertices list - for (Vec3i& face : its.indices) - for (int& tri_id : face) - tri_id -= min_id; - } - - if (m_prusaslicer_generator_version && - *m_prusaslicer_generator_version >= *Semver::parse("2.4.0-alpha1") && - *m_prusaslicer_generator_version < *Semver::parse("2.4.0-alpha3")) - // PrusaSlicer 2.4.0-alpha2 contained a bug, where all vertices of a single object were saved for each volume the object contained. - // Remove the vertices, that are not referenced by any face. - its_compactify_vertices(its, true); - - TriangleMesh triangle_mesh(std::move(its), volume_data.mesh_stats); - - if (m_version == 0) { - // if the 3mf was not produced by PrusaSlicer and there is only one instance, - // bake the transformation into the geometry to allow the reload from disk command - // to work properly - if (object.instances.size() == 1) { - triangle_mesh.transform(object.instances.front()->get_transformation().get_matrix(), false); - object.instances.front()->set_transformation(Slic3r::Geometry::Transformation()); - //FIXME do the mesh fixing? - } - } - if (triangle_mesh.volume() < 0) - triangle_mesh.flip_triangles(); - - ModelVolume* volume = object.add_volume(std::move(triangle_mesh)); - // stores the volume matrix taken from the metadata, if present - if (has_transform) - volume->source.transform = Slic3r::Geometry::Transformation(volume_matrix_to_object); - - // recreate custom supports, seam and mmu segmentation from previously loaded attribute - volume->supported_facets.reserve(triangles_count); - volume->seam_facets.reserve(triangles_count); - volume->mmu_segmentation_facets.reserve(triangles_count); - for (size_t i=0; isupported_facets.set_triangle_from_string(i, geometry.custom_supports[index]); - if (! geometry.custom_seam[index].empty()) - volume->seam_facets.set_triangle_from_string(i, geometry.custom_seam[index]); - if (! geometry.mmu_segmentation[index].empty()) - volume->mmu_segmentation_facets.set_triangle_from_string(i, geometry.mmu_segmentation[index]); - } - volume->supported_facets.shrink_to_fit(); - volume->seam_facets.shrink_to_fit(); - volume->mmu_segmentation_facets.shrink_to_fit(); - - // apply the remaining volume's metadata - for (const Metadata& metadata : volume_data.metadata) { - if (metadata.key == NAME_KEY) - volume->name = metadata.value; - else if ((metadata.key == MODIFIER_KEY) && (metadata.value == "1")) - volume->set_type(ModelVolumeType::PARAMETER_MODIFIER); - else if (metadata.key == VOLUME_TYPE_KEY) - volume->set_type(ModelVolume::type_from_string(metadata.value)); - else if (metadata.key == SOURCE_FILE_KEY) - volume->source.input_file = metadata.value; - else if (metadata.key == SOURCE_OBJECT_ID_KEY) - volume->source.object_idx = ::atoi(metadata.value.c_str()); - else if (metadata.key == SOURCE_VOLUME_ID_KEY) - volume->source.volume_idx = ::atoi(metadata.value.c_str()); - else if (metadata.key == SOURCE_OFFSET_X_KEY) - volume->source.mesh_offset.x() = ::atof(metadata.value.c_str()); - else if (metadata.key == SOURCE_OFFSET_Y_KEY) - volume->source.mesh_offset.y() = ::atof(metadata.value.c_str()); - else if (metadata.key == SOURCE_OFFSET_Z_KEY) - volume->source.mesh_offset.z() = ::atof(metadata.value.c_str()); - else if (metadata.key == SOURCE_IN_INCHES_KEY) - volume->source.is_converted_from_inches = metadata.value == "1"; - else if (metadata.key == SOURCE_IN_METERS_KEY) - volume->source.is_converted_from_meters = metadata.value == "1"; -#if ENABLE_RELOAD_FROM_DISK_REWORK - else if (metadata.key == SOURCE_IS_BUILTIN_VOLUME_KEY) - volume->source.is_from_builtin_objects = metadata.value == "1"; -#endif // ENABLE_RELOAD_FROM_DISK_REWORK - else - volume->config.set_deserialize(metadata.key, metadata.value, config_substitutions); - } - - // this may happen for 3mf saved by 3rd part softwares - if (volume->name.empty()) { - volume->name = object.name; - if (renamed_volumes_count > 0) - volume->name += "_" + std::to_string(renamed_volumes_count + 1); - ++renamed_volumes_count; - } - } - - return true; - } - - void XMLCALL _3MF_Importer::_handle_start_model_xml_element(void* userData, const char* name, const char** attributes) - { - _3MF_Importer* importer = (_3MF_Importer*)userData; - if (importer != nullptr) - importer->_handle_start_model_xml_element(name, attributes); - } - - void XMLCALL _3MF_Importer::_handle_end_model_xml_element(void* userData, const char* name) - { - _3MF_Importer* importer = (_3MF_Importer*)userData; - if (importer != nullptr) - importer->_handle_end_model_xml_element(name); - } - - void XMLCALL _3MF_Importer::_handle_model_xml_characters(void* userData, const XML_Char* s, int len) - { - _3MF_Importer* importer = (_3MF_Importer*)userData; - if (importer != nullptr) - importer->_handle_model_xml_characters(s, len); - } - - void XMLCALL _3MF_Importer::_handle_start_config_xml_element(void* userData, const char* name, const char** attributes) - { - _3MF_Importer* importer = (_3MF_Importer*)userData; - if (importer != nullptr) - importer->_handle_start_config_xml_element(name, attributes); - } - - void XMLCALL _3MF_Importer::_handle_end_config_xml_element(void* userData, const char* name) - { - _3MF_Importer* importer = (_3MF_Importer*)userData; - if (importer != nullptr) - importer->_handle_end_config_xml_element(name); - } - - class _3MF_Exporter : public _3MF_Base - { - struct BuildItem - { - unsigned int id; - Transform3d transform; - bool printable; - - BuildItem(unsigned int id, const Transform3d& transform, const bool printable) - : id(id) - , transform(transform) - , printable(printable) - { - } - }; - - struct Offsets - { - unsigned int first_vertex_id; - unsigned int first_triangle_id; - unsigned int last_triangle_id; - - Offsets(unsigned int first_vertex_id) - : first_vertex_id(first_vertex_id) - , first_triangle_id(-1) - , last_triangle_id(-1) - { - } - }; - - typedef std::map VolumeToOffsetsMap; - - struct ObjectData - { - ModelObject* object; - VolumeToOffsetsMap volumes_offsets; - - explicit ObjectData(ModelObject* object) - : object(object) - { - } - }; - - typedef std::vector BuildItemsList; - typedef std::map IdToObjectDataMap; - - bool m_fullpath_sources{ true }; - bool m_zip64 { true }; - - public: - bool save_model_to_file(const std::string& filename, Model& model, const DynamicPrintConfig* config, bool fullpath_sources, const ThumbnailData* thumbnail_data, bool zip64); - - private: - bool _save_model_to_file(const std::string& filename, Model& model, const DynamicPrintConfig* config, const ThumbnailData* thumbnail_data); - bool _add_content_types_file_to_archive(mz_zip_archive& archive); - bool _add_thumbnail_file_to_archive(mz_zip_archive& archive, const ThumbnailData& thumbnail_data); - bool _add_relationships_file_to_archive(mz_zip_archive& archive); - bool _add_model_file_to_archive(const std::string& filename, mz_zip_archive& archive, const Model& model, IdToObjectDataMap& objects_data); - bool _add_object_to_model_stream(mz_zip_writer_staged_context &context, unsigned int& object_id, ModelObject& object, BuildItemsList& build_items, VolumeToOffsetsMap& volumes_offsets); - bool _add_mesh_to_object_stream(mz_zip_writer_staged_context &context, ModelObject& object, VolumeToOffsetsMap& volumes_offsets); - bool _add_build_to_model_stream(std::stringstream& stream, const BuildItemsList& build_items); - bool _add_layer_height_profile_file_to_archive(mz_zip_archive& archive, Model& model); - bool _add_layer_config_ranges_file_to_archive(mz_zip_archive& archive, Model& model); - bool _add_sla_support_points_file_to_archive(mz_zip_archive& archive, Model& model); - bool _add_sla_drain_holes_file_to_archive(mz_zip_archive& archive, Model& model); - bool _add_print_config_file_to_archive(mz_zip_archive& archive, const DynamicPrintConfig &config); - bool _add_model_config_file_to_archive(mz_zip_archive& archive, const Model& model, const IdToObjectDataMap &objects_data); - bool _add_custom_gcode_per_print_z_file_to_archive(mz_zip_archive& archive, Model& model, const DynamicPrintConfig* config); - }; - - bool _3MF_Exporter::save_model_to_file(const std::string& filename, Model& model, const DynamicPrintConfig* config, bool fullpath_sources, const ThumbnailData* thumbnail_data, bool zip64) - { - clear_errors(); - m_fullpath_sources = fullpath_sources; - m_zip64 = zip64; - return _save_model_to_file(filename, model, config, thumbnail_data); - } - - bool _3MF_Exporter::_save_model_to_file(const std::string& filename, Model& model, const DynamicPrintConfig* config, const ThumbnailData* thumbnail_data) - { - mz_zip_archive archive; - mz_zip_zero_struct(&archive); - - if (!open_zip_writer(&archive, filename)) { - add_error("Unable to open the file"); - return false; - } - - // Adds content types file ("[Content_Types].xml";). - // The content of this file is the same for each PrusaSlicer 3mf. - if (!_add_content_types_file_to_archive(archive)) { - close_zip_writer(&archive); - boost::filesystem::remove(filename); - return false; - } - - if (thumbnail_data != nullptr && thumbnail_data->is_valid()) { - // Adds the file Metadata/thumbnail.png. - if (!_add_thumbnail_file_to_archive(archive, *thumbnail_data)) { - close_zip_writer(&archive); - boost::filesystem::remove(filename); - return false; - } - } - - // Adds relationships file ("_rels/.rels"). - // The content of this file is the same for each PrusaSlicer 3mf. - // The relationshis file contains a reference to the geometry file "3D/3dmodel.model", the name was chosen to be compatible with CURA. - if (!_add_relationships_file_to_archive(archive)) { - close_zip_writer(&archive); - boost::filesystem::remove(filename); - return false; - } - - // Adds model file ("3D/3dmodel.model"). - // This is the one and only file that contains all the geometry (vertices and triangles) of all ModelVolumes. - IdToObjectDataMap objects_data; - if (!_add_model_file_to_archive(filename, archive, model, objects_data)) { - close_zip_writer(&archive); - boost::filesystem::remove(filename); - return false; - } - - // Adds layer height profile file ("Metadata/Slic3r_PE_layer_heights_profile.txt"). - // All layer height profiles of all ModelObjects are stored here, indexed by 1 based index of the ModelObject in Model. - // The index differes from the index of an object ID of an object instance of a 3MF file! - if (!_add_layer_height_profile_file_to_archive(archive, model)) { - close_zip_writer(&archive); - boost::filesystem::remove(filename); - return false; - } - - // Adds layer config ranges file ("Metadata/Slic3r_PE_layer_config_ranges.txt"). - // All layer height profiles of all ModelObjects are stored here, indexed by 1 based index of the ModelObject in Model. - // The index differes from the index of an object ID of an object instance of a 3MF file! - if (!_add_layer_config_ranges_file_to_archive(archive, model)) { - close_zip_writer(&archive); - boost::filesystem::remove(filename); - return false; - } - - // Adds sla support points file ("Metadata/Slic3r_PE_sla_support_points.txt"). - // All sla support points of all ModelObjects are stored here, indexed by 1 based index of the ModelObject in Model. - // The index differes from the index of an object ID of an object instance of a 3MF file! - if (!_add_sla_support_points_file_to_archive(archive, model)) { - close_zip_writer(&archive); - boost::filesystem::remove(filename); - return false; - } - - if (!_add_sla_drain_holes_file_to_archive(archive, model)) { - close_zip_writer(&archive); - boost::filesystem::remove(filename); - return false; - } - - - // Adds custom gcode per height file ("Metadata/Prusa_Slicer_custom_gcode_per_print_z.xml"). - // All custom gcode per height of whole Model are stored here - if (!_add_custom_gcode_per_print_z_file_to_archive(archive, model, config)) { - close_zip_writer(&archive); - boost::filesystem::remove(filename); - return false; - } - - // Adds slic3r print config file ("Metadata/Slic3r_PE.config"). - // This file contains the content of FullPrintConfing / SLAFullPrintConfig. - if (config != nullptr) { - if (!_add_print_config_file_to_archive(archive, *config)) { - close_zip_writer(&archive); - boost::filesystem::remove(filename); - return false; - } - } - - // Adds slic3r model config file ("Metadata/Slic3r_PE_model.config"). - // This file contains all the attributes of all ModelObjects and their ModelVolumes (names, parameter overrides). - // As there is just a single Indexed Triangle Set data stored per ModelObject, offsets of volumes into their respective Indexed Triangle Set data - // is stored here as well. - if (!_add_model_config_file_to_archive(archive, model, objects_data)) { - close_zip_writer(&archive); - boost::filesystem::remove(filename); - return false; - } - - if (!mz_zip_writer_finalize_archive(&archive)) { - close_zip_writer(&archive); - boost::filesystem::remove(filename); - add_error("Unable to finalize the archive"); - return false; - } - - close_zip_writer(&archive); - - return true; - } - - bool _3MF_Exporter::_add_content_types_file_to_archive(mz_zip_archive& archive) - { - std::stringstream stream; - stream << "\n"; - stream << "\n"; - stream << " \n"; - stream << " \n"; - stream << " \n"; - stream << ""; - - std::string out = stream.str(); - - if (!mz_zip_writer_add_mem(&archive, CONTENT_TYPES_FILE.c_str(), (const void*)out.data(), out.length(), MZ_DEFAULT_COMPRESSION)) { - add_error("Unable to add content types file to archive"); - return false; - } - - return true; - } - - bool _3MF_Exporter::_add_thumbnail_file_to_archive(mz_zip_archive& archive, const ThumbnailData& thumbnail_data) - { - bool res = false; - - size_t png_size = 0; - void* png_data = tdefl_write_image_to_png_file_in_memory_ex((const void*)thumbnail_data.pixels.data(), thumbnail_data.width, thumbnail_data.height, 4, &png_size, MZ_DEFAULT_LEVEL, 1); - if (png_data != nullptr) { - res = mz_zip_writer_add_mem(&archive, THUMBNAIL_FILE.c_str(), (const void*)png_data, png_size, MZ_DEFAULT_COMPRESSION); - mz_free(png_data); - } - - if (!res) - add_error("Unable to add thumbnail file to archive"); - - return res; - } - - bool _3MF_Exporter::_add_relationships_file_to_archive(mz_zip_archive& archive) - { - std::stringstream stream; - stream << "\n"; - stream << "\n"; - stream << " \n"; - stream << " \n"; - stream << ""; - - std::string out = stream.str(); - - if (!mz_zip_writer_add_mem(&archive, RELATIONSHIPS_FILE.c_str(), (const void*)out.data(), out.length(), MZ_DEFAULT_COMPRESSION)) { - add_error("Unable to add relationships file to archive"); - return false; - } - - return true; - } - - static void reset_stream(std::stringstream &stream) - { - stream.str(""); - stream.clear(); - // https://en.cppreference.com/w/cpp/types/numeric_limits/max_digits10 - // Conversion of a floating-point value to text and back is exact as long as at least max_digits10 were used (9 for float, 17 for double). - // It is guaranteed to produce the same floating-point value, even though the intermediate text representation is not exact. - // The default value of std::stream precision is 6 digits only! - stream << std::setprecision(std::numeric_limits::max_digits10); - } - - bool _3MF_Exporter::_add_model_file_to_archive(const std::string& filename, mz_zip_archive& archive, const Model& model, IdToObjectDataMap& objects_data) - { - mz_zip_writer_staged_context context; - if (!mz_zip_writer_add_staged_open(&archive, &context, MODEL_FILE.c_str(), - m_zip64 ? - // Maximum expected and allowed 3MF file size is 16GiB. - // This switches the ZIP file to a 64bit mode, which adds a tiny bit of overhead to file records. - (uint64_t(1) << 30) * 16 : - // Maximum expected 3MF file size is 4GB-1. This is a workaround for interoperability with Windows 10 3D model fixing API, see - // GH issue #6193. - (uint64_t(1) << 32) - 1, - nullptr, nullptr, 0, MZ_DEFAULT_COMPRESSION, nullptr, 0, nullptr, 0)) { - add_error("Unable to add model file to archive"); - return false; - } - - { - std::stringstream stream; - reset_stream(stream); - stream << "\n"; - stream << "<" << MODEL_TAG << " unit=\"millimeter\" xml:lang=\"en-US\" xmlns=\"http://schemas.microsoft.com/3dmanufacturing/core/2015/02\" xmlns:slic3rpe=\"http://schemas.slic3r.org/3mf/2017/06\">\n"; - stream << " <" << METADATA_TAG << " name=\"" << SLIC3RPE_3MF_VERSION << "\">" << VERSION_3MF << "\n"; - - if (model.is_fdm_support_painted()) - stream << " <" << METADATA_TAG << " name=\"" << SLIC3RPE_FDM_SUPPORTS_PAINTING_VERSION << "\">" << FDM_SUPPORTS_PAINTING_VERSION << "\n"; - - if (model.is_seam_painted()) - stream << " <" << METADATA_TAG << " name=\"" << SLIC3RPE_SEAM_PAINTING_VERSION << "\">" << SEAM_PAINTING_VERSION << "\n"; - - if (model.is_mm_painted()) - stream << " <" << METADATA_TAG << " name=\"" << SLIC3RPE_MM_PAINTING_VERSION << "\">" << MM_PAINTING_VERSION << "\n"; - - std::string name = xml_escape(boost::filesystem::path(filename).stem().string()); - stream << " <" << METADATA_TAG << " name=\"Title\">" << name << "\n"; - stream << " <" << METADATA_TAG << " name=\"Designer\">" << "\n"; - stream << " <" << METADATA_TAG << " name=\"Description\">" << name << "\n"; - stream << " <" << METADATA_TAG << " name=\"Copyright\">" << "\n"; - stream << " <" << METADATA_TAG << " name=\"LicenseTerms\">" << "\n"; - stream << " <" << METADATA_TAG << " name=\"Rating\">" << "\n"; - std::string date = Slic3r::Utils::utc_timestamp(Slic3r::Utils::get_current_time_utc()); - // keep only the date part of the string - date = date.substr(0, 10); - stream << " <" << METADATA_TAG << " name=\"CreationDate\">" << date << "\n"; - stream << " <" << METADATA_TAG << " name=\"ModificationDate\">" << date << "\n"; - stream << " <" << METADATA_TAG << " name=\"Application\">" << SLIC3R_APP_KEY << "-" << SLIC3R_VERSION << "\n"; - stream << " <" << RESOURCES_TAG << ">\n"; - std::string buf = stream.str(); - if (! buf.empty() && ! mz_zip_writer_add_staged_data(&context, buf.data(), buf.size())) { - add_error("Unable to add model file to archive"); - return false; - } - } - - // Instance transformations, indexed by the 3MF object ID (which is a linear serialization of all instances of all ModelObjects). - BuildItemsList build_items; - - // The object_id here is a one based identifier of the first instance of a ModelObject in the 3MF file, where - // all the object instances of all ModelObjects are stored and indexed in a 1 based linear fashion. - // Therefore the list of object_ids here may not be continuous. - unsigned int object_id = 1; - for (ModelObject* obj : model.objects) { - if (obj == nullptr) - continue; - - // Index of an object in the 3MF file corresponding to the 1st instance of a ModelObject. - unsigned int curr_id = object_id; - IdToObjectDataMap::iterator object_it = objects_data.insert({ curr_id, ObjectData(obj) }).first; - // Store geometry of all ModelVolumes contained in a single ModelObject into a single 3MF indexed triangle set object. - // object_it->second.volumes_offsets will contain the offsets of the ModelVolumes in that single indexed triangle set. - // object_id will be increased to point to the 1st instance of the next ModelObject. - if (!_add_object_to_model_stream(context, object_id, *obj, build_items, object_it->second.volumes_offsets)) { - add_error("Unable to add object to archive"); - mz_zip_writer_add_staged_finish(&context); - return false; - } - } - - { - std::stringstream stream; - reset_stream(stream); - stream << " \n"; - - // Store the transformations of all the ModelInstances of all ModelObjects, indexed in a linear fashion. - if (!_add_build_to_model_stream(stream, build_items)) { - add_error("Unable to add build to archive"); - mz_zip_writer_add_staged_finish(&context); - return false; - } - - stream << "\n"; - - std::string buf = stream.str(); - - if ((! buf.empty() && ! mz_zip_writer_add_staged_data(&context, buf.data(), buf.size())) || - ! mz_zip_writer_add_staged_finish(&context)) { - add_error("Unable to add model file to archive"); - return false; - } - } - - return true; - } - - bool _3MF_Exporter::_add_object_to_model_stream(mz_zip_writer_staged_context &context, unsigned int& object_id, ModelObject& object, BuildItemsList& build_items, VolumeToOffsetsMap& volumes_offsets) - { - std::stringstream stream; - reset_stream(stream); - unsigned int id = 0; - for (const ModelInstance* instance : object.instances) { - assert(instance != nullptr); - if (instance == nullptr) - continue; - - unsigned int instance_id = object_id + id; - stream << " <" << OBJECT_TAG << " id=\"" << instance_id << "\" type=\"model\">\n"; - - if (id == 0) { - std::string buf = stream.str(); - reset_stream(stream); - if ((! buf.empty() && ! mz_zip_writer_add_staged_data(&context, buf.data(), buf.size())) || - ! _add_mesh_to_object_stream(context, object, volumes_offsets)) { - add_error("Unable to add mesh to archive"); - return false; - } - } - else { - stream << " <" << COMPONENTS_TAG << ">\n"; - stream << " <" << COMPONENT_TAG << " objectid=\"" << object_id << "\"/>\n"; - stream << " \n"; - } - - Transform3d t = instance->get_matrix(); - // instance_id is just a 1 indexed index in build_items. - assert(instance_id == build_items.size() + 1); - build_items.emplace_back(instance_id, t, instance->printable); - - stream << " \n"; - - ++id; - } - - object_id += id; - std::string buf = stream.str(); - return buf.empty() || mz_zip_writer_add_staged_data(&context, buf.data(), buf.size()); - } - -#if EXPORT_3MF_USE_SPIRIT_KARMA_FP - template - struct coordinate_policy_fixed : boost::spirit::karma::real_policies - { - static int floatfield(Num n) { return fmtflags::fixed; } - // Number of decimal digits to maintain float accuracy when storing into a text file and parsing back. - static unsigned precision(Num /* n */) { return std::numeric_limits::max_digits10 + 1; } - // No trailing zeros, thus for fmtflags::fixed usually much less than max_digits10 decimal numbers will be produced. - static bool trailing_zeros(Num /* n */) { return false; } - }; - template - struct coordinate_policy_scientific : coordinate_policy_fixed - { - static int floatfield(Num n) { return fmtflags::scientific; } - }; - // Define a new generator type based on the new coordinate policy. - using coordinate_type_fixed = boost::spirit::karma::real_generator>; - using coordinate_type_scientific = boost::spirit::karma::real_generator>; -#endif // EXPORT_3MF_USE_SPIRIT_KARMA_FP - - bool _3MF_Exporter::_add_mesh_to_object_stream(mz_zip_writer_staged_context &context, ModelObject& object, VolumeToOffsetsMap& volumes_offsets) - { - std::string output_buffer; - output_buffer += " <"; - output_buffer += MESH_TAG; - output_buffer += ">\n <"; - output_buffer += VERTICES_TAG; - output_buffer += ">\n"; - - auto flush = [this, &output_buffer, &context](bool force = false) { - if ((force && ! output_buffer.empty()) || output_buffer.size() >= 65536 * 16) { - if (! mz_zip_writer_add_staged_data(&context, output_buffer.data(), output_buffer.size())) { - add_error("Error during writing or compression"); - return false; - } - output_buffer.clear(); - } - return true; - }; - - auto format_coordinate = [](float f, char *buf) -> char* { - assert(is_decimal_separator_point()); -#if EXPORT_3MF_USE_SPIRIT_KARMA_FP - // Slightly faster than sprintf("%.9g"), but there is an issue with the karma floating point formatter, - // https://github.com/boostorg/spirit/pull/586 - // where the exported string is one digit shorter than it should be to guarantee lossless round trip. - // The code is left here for the ocasion boost guys improve. - coordinate_type_fixed const coordinate_fixed = coordinate_type_fixed(); - coordinate_type_scientific const coordinate_scientific = coordinate_type_scientific(); - // Format "f" in a fixed format. - char *ptr = buf; - boost::spirit::karma::generate(ptr, coordinate_fixed, f); - // Format "f" in a scientific format. - char *ptr2 = ptr; - boost::spirit::karma::generate(ptr2, coordinate_scientific, f); - // Return end of the shorter string. - auto len2 = ptr2 - ptr; - if (ptr - buf > len2) { - // Move the shorter scientific form to the front. - memcpy(buf, ptr, len2); - ptr = buf + len2; - } - // Return pointer to the end. - return ptr; -#else - // Round-trippable float, shortest possible. - return buf + sprintf(buf, "%.9g", f); -#endif - }; - - char buf[256]; - unsigned int vertices_count = 0; - for (ModelVolume* volume : object.volumes) { - if (volume == nullptr) - continue; - - volumes_offsets.insert({ volume, Offsets(vertices_count) }); - - const indexed_triangle_set &its = volume->mesh().its; - if (its.vertices.empty()) { - add_error("Found invalid mesh"); - return false; - } - - vertices_count += (int)its.vertices.size(); - - const Transform3d& matrix = volume->get_matrix(); - - for (size_t i = 0; i < its.vertices.size(); ++i) { - Vec3f v = (matrix * its.vertices[i].cast()).cast(); - char *ptr = buf; - boost::spirit::karma::generate(ptr, boost::spirit::lit(" <") << VERTEX_TAG << " x=\""); - ptr = format_coordinate(v.x(), ptr); - boost::spirit::karma::generate(ptr, "\" y=\""); - ptr = format_coordinate(v.y(), ptr); - boost::spirit::karma::generate(ptr, "\" z=\""); - ptr = format_coordinate(v.z(), ptr); - boost::spirit::karma::generate(ptr, "\"/>\n"); - *ptr = '\0'; - output_buffer += buf; - if (! flush()) - return false; - } - } - - output_buffer += " \n <"; - output_buffer += TRIANGLES_TAG; - output_buffer += ">\n"; - - unsigned int triangles_count = 0; - for (ModelVolume* volume : object.volumes) { - if (volume == nullptr) - continue; - - bool is_left_handed = volume->is_left_handed(); - VolumeToOffsetsMap::iterator volume_it = volumes_offsets.find(volume); - assert(volume_it != volumes_offsets.end()); - - const indexed_triangle_set &its = volume->mesh().its; - - // updates triangle offsets - volume_it->second.first_triangle_id = triangles_count; - triangles_count += (int)its.indices.size(); - volume_it->second.last_triangle_id = triangles_count - 1; - - for (int i = 0; i < int(its.indices.size()); ++ i) { - { - const Vec3i &idx = its.indices[i]; - char *ptr = buf; - boost::spirit::karma::generate(ptr, boost::spirit::lit(" <") << TRIANGLE_TAG << - " v1=\"" << boost::spirit::int_ << - "\" v2=\"" << boost::spirit::int_ << - "\" v3=\"" << boost::spirit::int_ << "\"", - idx[is_left_handed ? 2 : 0] + volume_it->second.first_vertex_id, - idx[1] + volume_it->second.first_vertex_id, - idx[is_left_handed ? 0 : 2] + volume_it->second.first_vertex_id); - *ptr = '\0'; - output_buffer += buf; - } - - std::string custom_supports_data_string = volume->supported_facets.get_triangle_as_string(i); - if (! custom_supports_data_string.empty()) { - output_buffer += " "; - output_buffer += CUSTOM_SUPPORTS_ATTR; - output_buffer += "=\""; - output_buffer += custom_supports_data_string; - output_buffer += "\""; - } - - std::string custom_seam_data_string = volume->seam_facets.get_triangle_as_string(i); - if (! custom_seam_data_string.empty()) { - output_buffer += " "; - output_buffer += CUSTOM_SEAM_ATTR; - output_buffer += "=\""; - output_buffer += custom_seam_data_string; - output_buffer += "\""; - } - - std::string mmu_painting_data_string = volume->mmu_segmentation_facets.get_triangle_as_string(i); - if (! mmu_painting_data_string.empty()) { - output_buffer += " "; - output_buffer += MMU_SEGMENTATION_ATTR; - output_buffer += "=\""; - output_buffer += mmu_painting_data_string; - output_buffer += "\""; - } - - output_buffer += "/>\n"; - - if (! flush()) - return false; - } - } - - output_buffer += " \n \n"; - - // Force flush. - return flush(true); - } - - bool _3MF_Exporter::_add_build_to_model_stream(std::stringstream& stream, const BuildItemsList& build_items) - { - // This happens for empty projects - if (build_items.size() == 0) { - add_error("No build item found"); - return true; - } - - stream << " <" << BUILD_TAG << ">\n"; - - for (const BuildItem& item : build_items) { - stream << " <" << ITEM_TAG << " " << OBJECTID_ATTR << "=\"" << item.id << "\" " << TRANSFORM_ATTR << "=\""; - for (unsigned c = 0; c < 4; ++c) { - for (unsigned r = 0; r < 3; ++r) { - stream << item.transform(r, c); - if (r != 2 || c != 3) - stream << " "; - } - } - stream << "\" " << PRINTABLE_ATTR << "=\"" << item.printable << "\"/>\n"; - } - - stream << " \n"; - - return true; - } - - bool _3MF_Exporter::_add_layer_height_profile_file_to_archive(mz_zip_archive& archive, Model& model) - { - assert(is_decimal_separator_point()); - std::string out = ""; - char buffer[1024]; - - unsigned int count = 0; - for (const ModelObject* object : model.objects) { - ++count; - const std::vector& layer_height_profile = object->layer_height_profile.get(); - if (layer_height_profile.size() >= 4 && layer_height_profile.size() % 2 == 0) { - sprintf(buffer, "object_id=%d|", count); - out += buffer; - - // Store the layer height profile as a single semicolon separated list. - for (size_t i = 0; i < layer_height_profile.size(); ++i) { - sprintf(buffer, (i == 0) ? "%f" : ";%f", layer_height_profile[i]); - out += buffer; - } - - out += "\n"; - } - } - - if (!out.empty()) { - if (!mz_zip_writer_add_mem(&archive, LAYER_HEIGHTS_PROFILE_FILE.c_str(), (const void*)out.data(), out.length(), MZ_DEFAULT_COMPRESSION)) { - add_error("Unable to add layer heights profile file to archive"); - return false; - } - } - - return true; - } - - bool _3MF_Exporter::_add_layer_config_ranges_file_to_archive(mz_zip_archive& archive, Model& model) - { - std::string out = ""; - pt::ptree tree; - - unsigned int object_cnt = 0; - for (const ModelObject* object : model.objects) { - object_cnt++; - const t_layer_config_ranges& ranges = object->layer_config_ranges; - if (!ranges.empty()) - { - pt::ptree& obj_tree = tree.add("objects.object",""); - - obj_tree.put(".id", object_cnt); - - // Store the layer config ranges. - for (const auto& range : ranges) { - pt::ptree& range_tree = obj_tree.add("range", ""); - - // store minX and maxZ - range_tree.put(".min_z", range.first.first); - range_tree.put(".max_z", range.first.second); - - // store range configuration - const ModelConfig& config = range.second; - for (const std::string& opt_key : config.keys()) { - pt::ptree& opt_tree = range_tree.add("option", config.opt_serialize(opt_key)); - opt_tree.put(".opt_key", opt_key); - } - } - } - } - - if (!tree.empty()) { - std::ostringstream oss; - pt::write_xml(oss, tree); - out = oss.str(); - - // Post processing("beautification") of the output string for a better preview - boost::replace_all(out, ">\n \n \n ", ">\n "); - boost::replace_all(out, ">", ">\n "); - // OR just - boost::replace_all(out, "><", ">\n<"); - } - - if (!out.empty()) { - if (!mz_zip_writer_add_mem(&archive, LAYER_CONFIG_RANGES_FILE.c_str(), (const void*)out.data(), out.length(), MZ_DEFAULT_COMPRESSION)) { - add_error("Unable to add layer heights profile file to archive"); - return false; - } - } - - return true; - } - - bool _3MF_Exporter::_add_sla_support_points_file_to_archive(mz_zip_archive& archive, Model& model) - { - assert(is_decimal_separator_point()); - std::string out = ""; - char buffer[1024]; - - unsigned int count = 0; - for (const ModelObject* object : model.objects) { - ++count; - const std::vector& sla_support_points = object->sla_support_points; - if (!sla_support_points.empty()) { - sprintf(buffer, "object_id=%d|", count); - out += buffer; - - // Store the layer height profile as a single space separated list. - for (size_t i = 0; i < sla_support_points.size(); ++i) { - sprintf(buffer, (i==0 ? "%f %f %f %f %f" : " %f %f %f %f %f"), sla_support_points[i].pos(0), sla_support_points[i].pos(1), sla_support_points[i].pos(2), sla_support_points[i].head_front_radius, (float)sla_support_points[i].is_new_island); - out += buffer; - } - out += "\n"; - } - } - - if (!out.empty()) { - // Adds version header at the beginning: - out = std::string("support_points_format_version=") + std::to_string(support_points_format_version) + std::string("\n") + out; - - if (!mz_zip_writer_add_mem(&archive, SLA_SUPPORT_POINTS_FILE.c_str(), (const void*)out.data(), out.length(), MZ_DEFAULT_COMPRESSION)) { - add_error("Unable to add sla support points file to archive"); - return false; - } - } - return true; - } - - bool _3MF_Exporter::_add_sla_drain_holes_file_to_archive(mz_zip_archive& archive, Model& model) - { - assert(is_decimal_separator_point()); - const char *const fmt = "object_id=%d|"; - std::string out; - - unsigned int count = 0; - for (const ModelObject* object : model.objects) { - ++count; - sla::DrainHoles drain_holes = object->sla_drain_holes; - - // The holes were placed 1mm above the mesh in the first implementation. - // This was a bad idea and the reference point was changed in 2.3 so - // to be on the mesh exactly. The elevated position is still saved - // in 3MFs for compatibility reasons. - for (sla::DrainHole& hole : drain_holes) { - hole.pos -= hole.normal.normalized(); - hole.height += 1.f; - } - - 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) - { - assert(is_decimal_separator_point()); - char buffer[1024]; - sprintf(buffer, "; %s\n\n", header_slic3r_generated().c_str()); - std::string out = buffer; - - for (const std::string &key : config.keys()) - if (key != "compatible_printers") - out += "; " + key + " = " + config.opt_serialize(key) + "\n"; - - if (!out.empty()) { - if (!mz_zip_writer_add_mem(&archive, PRINT_CONFIG_FILE.c_str(), (const void*)out.data(), out.length(), MZ_DEFAULT_COMPRESSION)) { - add_error("Unable to add print config file to archive"); - return false; - } - } - - return true; - } - - bool _3MF_Exporter::_add_model_config_file_to_archive(mz_zip_archive& archive, const Model& model, const IdToObjectDataMap &objects_data) - { - std::stringstream stream; - // Store mesh transformation in full precision, as the volumes are stored transformed and they need to be transformed back - // when loaded as accurately as possible. - stream << std::setprecision(std::numeric_limits::max_digits10); - stream << "\n"; - stream << "<" << CONFIG_TAG << ">\n"; - - for (const IdToObjectDataMap::value_type& obj_metadata : objects_data) { - const ModelObject* obj = obj_metadata.second.object; - if (obj != nullptr) { - // Output of instances count added because of github #3435, currently not used by PrusaSlicer - stream << " <" << OBJECT_TAG << " " << ID_ATTR << "=\"" << obj_metadata.first << "\" " << INSTANCESCOUNT_ATTR << "=\"" << obj->instances.size() << "\">\n"; - - // stores object's name - if (!obj->name.empty()) - stream << " <" << METADATA_TAG << " " << TYPE_ATTR << "=\"" << OBJECT_TYPE << "\" " << KEY_ATTR << "=\"name\" " << VALUE_ATTR << "=\"" << xml_escape(obj->name) << "\"/>\n"; - - // stores object's config data - for (const std::string& key : obj->config.keys()) { - stream << " <" << METADATA_TAG << " " << TYPE_ATTR << "=\"" << OBJECT_TYPE << "\" " << KEY_ATTR << "=\"" << key << "\" " << VALUE_ATTR << "=\"" << obj->config.opt_serialize(key) << "\"/>\n"; - } - - for (const ModelVolume* volume : obj_metadata.second.object->volumes) { - if (volume != nullptr) { - const VolumeToOffsetsMap& offsets = obj_metadata.second.volumes_offsets; - VolumeToOffsetsMap::const_iterator it = offsets.find(volume); - if (it != offsets.end()) { - // stores volume's offsets - stream << " <" << VOLUME_TAG << " "; - stream << FIRST_TRIANGLE_ID_ATTR << "=\"" << it->second.first_triangle_id << "\" "; - stream << LAST_TRIANGLE_ID_ATTR << "=\"" << it->second.last_triangle_id << "\">\n"; - - // stores volume's name - if (!volume->name.empty()) - stream << " <" << METADATA_TAG << " " << TYPE_ATTR << "=\"" << VOLUME_TYPE << "\" " << KEY_ATTR << "=\"" << NAME_KEY << "\" " << VALUE_ATTR << "=\"" << xml_escape(volume->name) << "\"/>\n"; - - // stores volume's modifier field (legacy, to support old slicers) - if (volume->is_modifier()) - stream << " <" << METADATA_TAG << " " << TYPE_ATTR << "=\"" << VOLUME_TYPE << "\" " << KEY_ATTR << "=\"" << MODIFIER_KEY << "\" " << VALUE_ATTR << "=\"1\"/>\n"; - // stores volume's type (overrides the modifier field above) - stream << " <" << METADATA_TAG << " " << TYPE_ATTR << "=\"" << VOLUME_TYPE << "\" " << KEY_ATTR << "=\"" << VOLUME_TYPE_KEY << "\" " << - VALUE_ATTR << "=\"" << ModelVolume::type_to_string(volume->type()) << "\"/>\n"; - - // stores volume's local matrix - stream << " <" << METADATA_TAG << " " << TYPE_ATTR << "=\"" << VOLUME_TYPE << "\" " << KEY_ATTR << "=\"" << MATRIX_KEY << "\" " << VALUE_ATTR << "=\""; - const Transform3d matrix = volume->get_matrix() * volume->source.transform.get_matrix(); - for (int r = 0; r < 4; ++r) { - for (int c = 0; c < 4; ++c) { - stream << matrix(r, c); - if (r != 3 || c != 3) - stream << " "; - } - } - stream << "\"/>\n"; - - // stores volume's source data - { - std::string input_file = xml_escape(m_fullpath_sources ? volume->source.input_file : boost::filesystem::path(volume->source.input_file).filename().string()); - std::string prefix = std::string(" <") + METADATA_TAG + " " + TYPE_ATTR + "=\"" + VOLUME_TYPE + "\" " + KEY_ATTR + "=\""; - if (! volume->source.input_file.empty()) { - stream << prefix << SOURCE_FILE_KEY << "\" " << VALUE_ATTR << "=\"" << input_file << "\"/>\n"; - stream << prefix << SOURCE_OBJECT_ID_KEY << "\" " << VALUE_ATTR << "=\"" << volume->source.object_idx << "\"/>\n"; - stream << prefix << SOURCE_VOLUME_ID_KEY << "\" " << VALUE_ATTR << "=\"" << volume->source.volume_idx << "\"/>\n"; - stream << prefix << SOURCE_OFFSET_X_KEY << "\" " << VALUE_ATTR << "=\"" << volume->source.mesh_offset(0) << "\"/>\n"; - stream << prefix << SOURCE_OFFSET_Y_KEY << "\" " << VALUE_ATTR << "=\"" << volume->source.mesh_offset(1) << "\"/>\n"; - stream << prefix << SOURCE_OFFSET_Z_KEY << "\" " << VALUE_ATTR << "=\"" << volume->source.mesh_offset(2) << "\"/>\n"; - } - assert(! volume->source.is_converted_from_inches || ! volume->source.is_converted_from_meters); - if (volume->source.is_converted_from_inches) - stream << prefix << SOURCE_IN_INCHES_KEY << "\" " << VALUE_ATTR << "=\"1\"/>\n"; - else if (volume->source.is_converted_from_meters) - stream << prefix << SOURCE_IN_METERS_KEY << "\" " << VALUE_ATTR << "=\"1\"/>\n"; -#if ENABLE_RELOAD_FROM_DISK_REWORK - if (volume->source.is_from_builtin_objects) - stream << prefix << SOURCE_IS_BUILTIN_VOLUME_KEY << "\" " << VALUE_ATTR << "=\"1\"/>\n"; -#endif // ENABLE_RELOAD_FROM_DISK_REWORK - } - - // stores volume's config data - for (const std::string& key : volume->config.keys()) { - stream << " <" << METADATA_TAG << " " << TYPE_ATTR << "=\"" << VOLUME_TYPE << "\" " << KEY_ATTR << "=\"" << key << "\" " << VALUE_ATTR << "=\"" << volume->config.opt_serialize(key) << "\"/>\n"; - } - - // stores mesh's statistics - const RepairedMeshErrors& stats = volume->mesh().stats().repaired_errors; - stream << " <" << MESH_TAG << " "; - stream << MESH_STAT_EDGES_FIXED << "=\"" << stats.edges_fixed << "\" "; - stream << MESH_STAT_DEGENERATED_FACETS << "=\"" << stats.degenerate_facets << "\" "; - stream << MESH_STAT_FACETS_REMOVED << "=\"" << stats.facets_removed << "\" "; - stream << MESH_STAT_FACETS_RESERVED << "=\"" << stats.facets_reversed << "\" "; - stream << MESH_STAT_BACKWARDS_EDGES << "=\"" << stats.backwards_edges << "\"/>\n"; - - stream << " \n"; - } - } - } - - stream << " \n"; - } - } - - stream << "\n"; - - std::string out = stream.str(); - - if (!mz_zip_writer_add_mem(&archive, MODEL_CONFIG_FILE.c_str(), (const void*)out.data(), out.length(), MZ_DEFAULT_COMPRESSION)) { - add_error("Unable to add model config file to archive"); - return false; - } - - return true; - } - -bool _3MF_Exporter::_add_custom_gcode_per_print_z_file_to_archive( mz_zip_archive& archive, Model& model, const DynamicPrintConfig* config) -{ - std::string out = ""; - - if (!model.custom_gcode_per_print_z.gcodes.empty()) { - pt::ptree tree; - pt::ptree& main_tree = tree.add("custom_gcodes_per_print_z", ""); - - for (const CustomGCode::Item& code : model.custom_gcode_per_print_z.gcodes) { - pt::ptree& code_tree = main_tree.add("code", ""); - - // store data of custom_gcode_per_print_z - code_tree.put(".print_z" , code.print_z ); - code_tree.put(".type" , static_cast(code.type)); - code_tree.put(".extruder" , code.extruder ); - code_tree.put(".color" , code.color ); - code_tree.put(".extra" , code.extra ); - - // add gcode field data for the old version of the PrusaSlicer - std::string gcode = code.type == CustomGCode::ColorChange ? config->opt_string("color_change_gcode") : - code.type == CustomGCode::PausePrint ? config->opt_string("pause_print_gcode") : - code.type == CustomGCode::Template ? config->opt_string("template_custom_gcode") : - code.type == CustomGCode::ToolChange ? "tool_change" : code.extra; - code_tree.put(".gcode" , gcode ); - } - - pt::ptree& mode_tree = main_tree.add("mode", ""); - // store mode of a custom_gcode_per_print_z - mode_tree.put(".value", model.custom_gcode_per_print_z.mode == CustomGCode::Mode::SingleExtruder ? CustomGCode::SingleExtruderMode : - model.custom_gcode_per_print_z.mode == CustomGCode::Mode::MultiAsSingle ? CustomGCode::MultiAsSingleMode : - CustomGCode::MultiExtruderMode); - - if (!tree.empty()) { - std::ostringstream oss; - boost::property_tree::write_xml(oss, tree); - out = oss.str(); - - // Post processing("beautification") of the output string - boost::replace_all(out, "><", ">\n<"); - } - } - - if (!out.empty()) { - if (!mz_zip_writer_add_mem(&archive, CUSTOM_GCODE_PER_PRINT_Z_FILE.c_str(), (const void*)out.data(), out.length(), MZ_DEFAULT_COMPRESSION)) { - add_error("Unable to add custom Gcodes per print_z file to archive"); - return false; - } - } - - return true; -} - -// Perform conversions based on the config values available. -//FIXME provide a version of PrusaSlicer that stored the project file (3MF). -static void handle_legacy_project_loaded(unsigned int version_project_file, DynamicPrintConfig& config) -{ - if (! config.has("brim_separation")) { - if (auto *opt_elephant_foot = config.option("elefant_foot_compensation", false); opt_elephant_foot) { - // Conversion from older PrusaSlicer which applied brim separation equal to elephant foot compensation. - auto *opt_brim_separation = config.option("brim_separation", true); - opt_brim_separation->value = opt_elephant_foot->value; - } - } -} - -bool is_project_3mf(const std::string& filename) -{ - mz_zip_archive archive; - mz_zip_zero_struct(&archive); - - if (!open_zip_reader(&archive, filename)) - return false; - - mz_uint num_entries = mz_zip_reader_get_num_files(&archive); - - // loop the entries to search for config - mz_zip_archive_file_stat stat; - bool config_found = false; - for (mz_uint i = 0; i < num_entries; ++i) { - if (mz_zip_reader_file_stat(&archive, i, &stat)) { - std::string name(stat.m_filename); - std::replace(name.begin(), name.end(), '\\', '/'); - - if (boost::algorithm::iequals(name, PRINT_CONFIG_FILE)) { - config_found = true; - break; - } - } - } - - close_zip_reader(&archive); - - return config_found; -} - -bool load_3mf(const char* path, DynamicPrintConfig& config, ConfigSubstitutionContext& config_substitutions, Model* model, bool check_version) -{ - if (path == nullptr || model == nullptr) - return false; - - // All import should use "C" locales for number formatting. - CNumericLocalesSetter locales_setter; - _3MF_Importer importer; - bool res = importer.load_model_from_file(path, *model, config, config_substitutions, check_version); - importer.log_errors(); - handle_legacy_project_loaded(importer.version(), config); - return res; -} - -bool store_3mf(const char* path, Model* model, const DynamicPrintConfig* config, bool fullpath_sources, const ThumbnailData* thumbnail_data, bool zip64) -{ - // All export should use "C" locales for number formatting. - CNumericLocalesSetter locales_setter; - - if (path == nullptr || model == nullptr) - return false; - - _3MF_Exporter exporter; - bool res = exporter.save_model_to_file(path, *model, config, fullpath_sources, thumbnail_data, zip64); - if (!res) - exporter.log_errors(); - - return res; -} -} // namespace Slic3r +#include "../libslic3r.h" +#include "../Exception.hpp" +#include "../Model.hpp" +#include "../Utils.hpp" +#include "../LocalesUtils.hpp" +#include "../GCode.hpp" +#include "../Geometry.hpp" +#include "../GCode/ThumbnailData.hpp" +#include "../Semver.hpp" +#include "../Time.hpp" + +#include "../I18N.hpp" + +#include "3mf.hpp" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +namespace pt = boost::property_tree; + +#include +#include +#include "miniz_extension.hpp" + +#include + +// Slightly faster than sprintf("%.9g"), but there is an issue with the karma floating point formatter, +// https://github.com/boostorg/spirit/pull/586 +// where the exported string is one digit shorter than it should be to guarantee lossless round trip. +// The code is left here for the ocasion boost guys improve. +#define EXPORT_3MF_USE_SPIRIT_KARMA_FP 0 + +// VERSION NUMBERS +// 0 : .3mf, files saved by older slic3r or other applications. No version definition in them. +// 1 : Introduction of 3mf versioning. No other change in data saved into 3mf files. +// 2 : Volumes' matrices and source data added to Metadata/Slic3r_PE_model.config file, meshes transformed back to their coordinate system on loading. +// WARNING !! -> the version number has been rolled back to 1 +// the next change should use 3 +const unsigned int VERSION_3MF = 1; +// Allow loading version 2 file as well. +const unsigned int VERSION_3MF_COMPATIBLE = 2; +const char* SLIC3RPE_3MF_VERSION = "slic3rpe:Version3mf"; // definition of the metadata name saved into .model file + +// Painting gizmos data version numbers +// 0 : 3MF files saved by older PrusaSlicer or the painting gizmo wasn't used. No version definition in them. +// 1 : Introduction of painting gizmos data versioning. No other changes in painting gizmos data. +const unsigned int FDM_SUPPORTS_PAINTING_VERSION = 1; +const unsigned int SEAM_PAINTING_VERSION = 1; +const unsigned int MM_PAINTING_VERSION = 1; + +const std::string SLIC3RPE_FDM_SUPPORTS_PAINTING_VERSION = "slic3rpe:FdmSupportsPaintingVersion"; +const std::string SLIC3RPE_SEAM_PAINTING_VERSION = "slic3rpe:SeamPaintingVersion"; +const std::string SLIC3RPE_MM_PAINTING_VERSION = "slic3rpe:MmPaintingVersion"; + +const std::string MODEL_FOLDER = "3D/"; +const std::string MODEL_EXTENSION = ".model"; +const std::string MODEL_FILE = "3D/3dmodel.model"; // << this is the only format of the string which works with CURA +const std::string CONTENT_TYPES_FILE = "[Content_Types].xml"; +const std::string RELATIONSHIPS_FILE = "_rels/.rels"; +const std::string THUMBNAIL_FILE = "Metadata/thumbnail.png"; +const std::string PRINT_CONFIG_FILE = "Metadata/Slic3r_PE.config"; +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"; + +static constexpr const char* MODEL_TAG = "model"; +static constexpr const char* RESOURCES_TAG = "resources"; +static constexpr const char* OBJECT_TAG = "object"; +static constexpr const char* MESH_TAG = "mesh"; +static constexpr const char* VERTICES_TAG = "vertices"; +static constexpr const char* VERTEX_TAG = "vertex"; +static constexpr const char* TRIANGLES_TAG = "triangles"; +static constexpr const char* TRIANGLE_TAG = "triangle"; +static constexpr const char* COMPONENTS_TAG = "components"; +static constexpr const char* COMPONENT_TAG = "component"; +static constexpr const char* BUILD_TAG = "build"; +static constexpr const char* ITEM_TAG = "item"; +static constexpr const char* METADATA_TAG = "metadata"; + +static constexpr const char* CONFIG_TAG = "config"; +static constexpr const char* VOLUME_TAG = "volume"; + +static constexpr const char* UNIT_ATTR = "unit"; +static constexpr const char* NAME_ATTR = "name"; +static constexpr const char* TYPE_ATTR = "type"; +static constexpr const char* ID_ATTR = "id"; +static constexpr const char* X_ATTR = "x"; +static constexpr const char* Y_ATTR = "y"; +static constexpr const char* Z_ATTR = "z"; +static constexpr const char* V1_ATTR = "v1"; +static constexpr const char* V2_ATTR = "v2"; +static constexpr const char* V3_ATTR = "v3"; +static constexpr const char* OBJECTID_ATTR = "objectid"; +static constexpr const char* TRANSFORM_ATTR = "transform"; +static constexpr const char* PRINTABLE_ATTR = "printable"; +static constexpr const char* INSTANCESCOUNT_ATTR = "instances_count"; +static constexpr const char* CUSTOM_SUPPORTS_ATTR = "slic3rpe:custom_supports"; +static constexpr const char* CUSTOM_SEAM_ATTR = "slic3rpe:custom_seam"; +static constexpr const char* MMU_SEGMENTATION_ATTR = "slic3rpe:mmu_segmentation"; + +static constexpr const char* KEY_ATTR = "key"; +static constexpr const char* VALUE_ATTR = "value"; +static constexpr const char* FIRST_TRIANGLE_ID_ATTR = "firstid"; +static constexpr const char* LAST_TRIANGLE_ID_ATTR = "lastid"; + +static constexpr const char* OBJECT_TYPE = "object"; +static constexpr const char* VOLUME_TYPE = "volume"; + +static constexpr const char* NAME_KEY = "name"; +static constexpr const char* MODIFIER_KEY = "modifier"; +static constexpr const char* VOLUME_TYPE_KEY = "volume_type"; +static constexpr const char* MATRIX_KEY = "matrix"; +static constexpr const char* SOURCE_FILE_KEY = "source_file"; +static constexpr const char* SOURCE_OBJECT_ID_KEY = "source_object_id"; +static constexpr const char* SOURCE_VOLUME_ID_KEY = "source_volume_id"; +static constexpr const char* SOURCE_OFFSET_X_KEY = "source_offset_x"; +static constexpr const char* SOURCE_OFFSET_Y_KEY = "source_offset_y"; +static constexpr const char* SOURCE_OFFSET_Z_KEY = "source_offset_z"; +static constexpr const char* SOURCE_IN_INCHES_KEY = "source_in_inches"; +static constexpr const char* SOURCE_IN_METERS_KEY = "source_in_meters"; +#if ENABLE_RELOAD_FROM_DISK_REWORK +static constexpr const char* SOURCE_IS_BUILTIN_VOLUME_KEY = "source_is_builtin_volume"; +#endif // ENABLE_RELOAD_FROM_DISK_REWORK + +static constexpr const char* MESH_STAT_EDGES_FIXED = "edges_fixed"; +static constexpr const char* MESH_STAT_DEGENERATED_FACETS = "degenerate_facets"; +static constexpr const char* MESH_STAT_FACETS_REMOVED = "facets_removed"; +static constexpr const char* MESH_STAT_FACETS_RESERVED = "facets_reversed"; +static constexpr const char* MESH_STAT_BACKWARDS_EDGES = "backwards_edges"; + + +const unsigned int VALID_OBJECT_TYPES_COUNT = 1; +const char* VALID_OBJECT_TYPES[] = +{ + "model" +}; + +const char* INVALID_OBJECT_TYPES[] = +{ + "solidsupport", + "support", + "surface", + "other" +}; + +class version_error : public Slic3r::FileIOError +{ +public: + version_error(const std::string& what_arg) : Slic3r::FileIOError(what_arg) {} + version_error(const char* what_arg) : Slic3r::FileIOError(what_arg) {} +}; + +const char* get_attribute_value_charptr(const char** attributes, unsigned int attributes_size, const char* attribute_key) +{ + if ((attributes == nullptr) || (attributes_size == 0) || (attributes_size % 2 != 0) || (attribute_key == nullptr)) + return nullptr; + + for (unsigned int a = 0; a < attributes_size; a += 2) { + if (::strcmp(attributes[a], attribute_key) == 0) + return attributes[a + 1]; + } + + return nullptr; +} + +std::string get_attribute_value_string(const char** attributes, unsigned int attributes_size, const char* attribute_key) +{ + const char* text = get_attribute_value_charptr(attributes, attributes_size, attribute_key); + return (text != nullptr) ? text : ""; +} + +float get_attribute_value_float(const char** attributes, unsigned int attributes_size, const char* attribute_key) +{ + float value = 0.0f; + if (const char *text = get_attribute_value_charptr(attributes, attributes_size, attribute_key); text != nullptr) + fast_float::from_chars(text, text + strlen(text), value); + return value; +} + +int get_attribute_value_int(const char** attributes, unsigned int attributes_size, const char* attribute_key) +{ + int value = 0; + if (const char *text = get_attribute_value_charptr(attributes, attributes_size, attribute_key); text != nullptr) + boost::spirit::qi::parse(text, text + strlen(text), boost::spirit::qi::int_, value); + return value; +} + +bool get_attribute_value_bool(const char** attributes, unsigned int attributes_size, const char* attribute_key) +{ + const char* text = get_attribute_value_charptr(attributes, attributes_size, attribute_key); + return (text != nullptr) ? (bool)::atoi(text) : true; +} + +Slic3r::Transform3d get_transform_from_3mf_specs_string(const std::string& mat_str) +{ + // check: https://3mf.io/3d-manufacturing-format/ or https://github.com/3MFConsortium/spec_core/blob/master/3MF%20Core%20Specification.md + // to see how matrices are stored inside 3mf according to specifications + Slic3r::Transform3d ret = Slic3r::Transform3d::Identity(); + + if (mat_str.empty()) + // empty string means default identity matrix + return ret; + + std::vector mat_elements_str; + boost::split(mat_elements_str, mat_str, boost::is_any_of(" "), boost::token_compress_on); + + unsigned int size = (unsigned int)mat_elements_str.size(); + if (size != 12) + // invalid data, return identity matrix + return ret; + + unsigned int i = 0; + // matrices are stored into 3mf files as 4x3 + // we need to transpose them + for (unsigned int c = 0; c < 4; ++c) { + for (unsigned int r = 0; r < 3; ++r) { + ret(r, c) = ::atof(mat_elements_str[i++].c_str()); + } + } + return ret; +} + +float get_unit_factor(const std::string& unit) +{ + const char* text = unit.c_str(); + + if (::strcmp(text, "micron") == 0) + return 0.001f; + else if (::strcmp(text, "centimeter") == 0) + return 10.0f; + else if (::strcmp(text, "inch") == 0) + return 25.4f; + else if (::strcmp(text, "foot") == 0) + return 304.8f; + else if (::strcmp(text, "meter") == 0) + return 1000.0f; + else + // default "millimeters" (see specification) + return 1.0f; +} + +bool is_valid_object_type(const std::string& type) +{ + // if the type is empty defaults to "model" (see specification) + if (type.empty()) + return true; + + for (unsigned int i = 0; i < VALID_OBJECT_TYPES_COUNT; ++i) { + if (::strcmp(type.c_str(), VALID_OBJECT_TYPES[i]) == 0) + return true; + } + + return false; +} + +namespace Slic3r { + +//! macro used to mark string used at localization, +//! return same string +#define L(s) (s) +#define _(s) Slic3r::I18N::translate(s) + + // Base class with error messages management + class _3MF_Base + { + std::vector m_errors; + + protected: + void add_error(const std::string& error) { m_errors.push_back(error); } + void clear_errors() { m_errors.clear(); } + + public: + void log_errors() + { + for (const std::string& error : m_errors) + BOOST_LOG_TRIVIAL(error) << error; + } + }; + + class _3MF_Importer : public _3MF_Base + { + struct Component + { + int object_id; + Transform3d transform; + + explicit Component(int object_id) + : object_id(object_id) + , transform(Transform3d::Identity()) + { + } + + Component(int object_id, const Transform3d& transform) + : object_id(object_id) + , transform(transform) + { + } + }; + + typedef std::vector ComponentsList; + + struct Geometry + { + std::vector vertices; + std::vector triangles; + std::vector custom_supports; + std::vector custom_seam; + std::vector mmu_segmentation; + + bool empty() { return vertices.empty() || triangles.empty(); } + + void reset() { + vertices.clear(); + triangles.clear(); + custom_supports.clear(); + custom_seam.clear(); + mmu_segmentation.clear(); + } + }; + + struct CurrentObject + { + // ID of the object inside the 3MF file, 1 based. + int id; + // Index of the ModelObject in its respective Model, zero based. + int model_object_idx; + Geometry geometry; + ModelObject* object; + ComponentsList components; + + CurrentObject() { reset(); } + + void reset() { + id = -1; + model_object_idx = -1; + geometry.reset(); + object = nullptr; + components.clear(); + } + }; + + struct CurrentConfig + { + int object_id; + int volume_id; + }; + + struct Instance + { + ModelInstance* instance; + Transform3d transform; + + Instance(ModelInstance* instance, const Transform3d& transform) + : instance(instance) + , transform(transform) + { + } + }; + + struct Metadata + { + std::string key; + std::string value; + + Metadata(const std::string& key, const std::string& value) + : key(key) + , value(value) + { + } + }; + + typedef std::vector MetadataList; + + struct ObjectMetadata + { + struct VolumeMetadata + { + unsigned int first_triangle_id; + unsigned int last_triangle_id; + MetadataList metadata; + RepairedMeshErrors mesh_stats; + + VolumeMetadata(unsigned int first_triangle_id, unsigned int last_triangle_id) + : first_triangle_id(first_triangle_id) + , last_triangle_id(last_triangle_id) + { + } + }; + + typedef std::vector VolumeMetadataList; + + MetadataList metadata; + VolumeMetadataList volumes; + }; + + // Map from a 1 based 3MF object ID to a 0 based ModelObject index inside m_model->objects. + typedef std::map IdToModelObjectMap; + typedef std::map IdToAliasesMap; + typedef std::vector InstancesList; + typedef std::map IdToMetadataMap; + typedef std::map IdToGeometryMap; + 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; + bool m_check_version; + + // Semantic version of PrusaSlicer, that generated this 3MF. + boost::optional m_prusaslicer_generator_version; + unsigned int m_fdm_supports_painting_version = 0; + unsigned int m_seam_painting_version = 0; + unsigned int m_mm_painting_version = 0; + + XML_Parser m_xml_parser; + // Error code returned by the application side of the parser. In that case the expat may not reliably deliver the error state + // after returning from XML_Parse() function, thus we keep the error state here. + bool m_parse_error { false }; + std::string m_parse_error_message; + Model* m_model; + float m_unit_factor; + CurrentObject m_curr_object; + IdToModelObjectMap m_objects; + IdToAliasesMap m_objects_aliases; + InstancesList m_instances; + IdToGeometryMap m_geometries; + CurrentConfig m_curr_config; + IdToMetadataMap m_objects_metadata; + 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; + + public: + _3MF_Importer(); + ~_3MF_Importer(); + + bool load_model_from_file(const std::string& filename, Model& model, DynamicPrintConfig& config, ConfigSubstitutionContext& config_substitutions, bool check_version); + unsigned int version() const { return m_version; } + + private: + void _destroy_xml_parser(); + void _stop_xml_parser(const std::string& msg = std::string()); + + bool parse_error() const { return m_parse_error; } + const char* parse_error_message() const { + return m_parse_error ? + // The error was signalled by the user code, not the expat parser. + (m_parse_error_message.empty() ? "Invalid 3MF format" : m_parse_error_message.c_str()) : + // The error was signalled by the expat parser. + XML_ErrorString(XML_GetErrorCode(m_xml_parser)); + } + + bool _load_model_from_file(const std::string& filename, Model& model, DynamicPrintConfig& config, ConfigSubstitutionContext& config_substitutions); + bool _extract_model_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat); + 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, ConfigSubstitutionContext& config_substitutions); + 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); + + void _extract_print_config_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat, DynamicPrintConfig& config, ConfigSubstitutionContext& subs_context, const std::string& archive_filename); + bool _extract_model_config_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat, Model& model); + + // handlers to parse the .model file + void _handle_start_model_xml_element(const char* name, const char** attributes); + void _handle_end_model_xml_element(const char* name); + void _handle_model_xml_characters(const XML_Char* s, int len); + + // handlers to parse the MODEL_CONFIG_FILE file + void _handle_start_config_xml_element(const char* name, const char** attributes); + void _handle_end_config_xml_element(const char* name); + + bool _handle_start_model(const char** attributes, unsigned int num_attributes); + bool _handle_end_model(); + + bool _handle_start_resources(const char** attributes, unsigned int num_attributes); + bool _handle_end_resources(); + + bool _handle_start_object(const char** attributes, unsigned int num_attributes); + bool _handle_end_object(); + + bool _handle_start_mesh(const char** attributes, unsigned int num_attributes); + bool _handle_end_mesh(); + + bool _handle_start_vertices(const char** attributes, unsigned int num_attributes); + bool _handle_end_vertices(); + + bool _handle_start_vertex(const char** attributes, unsigned int num_attributes); + bool _handle_end_vertex(); + + bool _handle_start_triangles(const char** attributes, unsigned int num_attributes); + bool _handle_end_triangles(); + + bool _handle_start_triangle(const char** attributes, unsigned int num_attributes); + bool _handle_end_triangle(); + + bool _handle_start_components(const char** attributes, unsigned int num_attributes); + bool _handle_end_components(); + + bool _handle_start_component(const char** attributes, unsigned int num_attributes); + bool _handle_end_component(); + + bool _handle_start_build(const char** attributes, unsigned int num_attributes); + bool _handle_end_build(); + + bool _handle_start_item(const char** attributes, unsigned int num_attributes); + bool _handle_end_item(); + + bool _handle_start_metadata(const char** attributes, unsigned int num_attributes); + bool _handle_end_metadata(); + + bool _create_object_instance(int object_id, const Transform3d& transform, const bool printable, unsigned int recur_counter); + + void _apply_transform(ModelInstance& instance, const Transform3d& transform); + + bool _handle_start_config(const char** attributes, unsigned int num_attributes); + bool _handle_end_config(); + + bool _handle_start_config_object(const char** attributes, unsigned int num_attributes); + bool _handle_end_config_object(); + + bool _handle_start_config_volume(const char** attributes, unsigned int num_attributes); + bool _handle_start_config_volume_mesh(const char** attributes, unsigned int num_attributes); + bool _handle_end_config_volume(); + bool _handle_end_config_volume_mesh(); + + bool _handle_start_config_metadata(const char** attributes, unsigned int num_attributes); + bool _handle_end_config_metadata(); + + bool _generate_volumes(ModelObject& object, const Geometry& geometry, const ObjectMetadata::VolumeMetadataList& volumes, ConfigSubstitutionContext& config_substitutions); + + // callbacks to parse the .model file + static void XMLCALL _handle_start_model_xml_element(void* userData, const char* name, const char** attributes); + static void XMLCALL _handle_end_model_xml_element(void* userData, const char* name); + static void XMLCALL _handle_model_xml_characters(void* userData, const XML_Char* s, int len); + + // callbacks to parse the MODEL_CONFIG_FILE file + static void XMLCALL _handle_start_config_xml_element(void* userData, const char* name, const char** attributes); + static void XMLCALL _handle_end_config_xml_element(void* userData, const char* name); + }; + + _3MF_Importer::_3MF_Importer() + : m_version(0) + , m_check_version(false) + , m_xml_parser(nullptr) + , m_model(nullptr) + , m_unit_factor(1.0f) + , m_curr_metadata_name("") + , m_curr_characters("") + , m_name("") + { + } + + _3MF_Importer::~_3MF_Importer() + { + _destroy_xml_parser(); + } + + bool _3MF_Importer::load_model_from_file(const std::string& filename, Model& model, DynamicPrintConfig& config, ConfigSubstitutionContext& config_substitutions, bool check_version) + { + m_version = 0; + m_fdm_supports_painting_version = 0; + m_seam_painting_version = 0; + m_mm_painting_version = 0; + m_check_version = check_version; + m_model = &model; + m_unit_factor = 1.0f; + m_curr_object.reset(); + m_objects.clear(); + m_objects_aliases.clear(); + m_instances.clear(); + m_geometries.clear(); + m_curr_config.object_id = -1; + m_curr_config.volume_id = -1; + m_objects_metadata.clear(); + m_layer_heights_profiles.clear(); + m_layer_config_ranges.clear(); + m_sla_support_points.clear(); + m_curr_metadata_name.clear(); + m_curr_characters.clear(); + clear_errors(); + + return _load_model_from_file(filename, model, config, config_substitutions); + } + + void _3MF_Importer::_destroy_xml_parser() + { + if (m_xml_parser != nullptr) { + XML_ParserFree(m_xml_parser); + m_xml_parser = nullptr; + } + } + + void _3MF_Importer::_stop_xml_parser(const std::string &msg) + { + assert(! m_parse_error); + assert(m_parse_error_message.empty()); + assert(m_xml_parser != nullptr); + m_parse_error = true; + m_parse_error_message = msg; + XML_StopParser(m_xml_parser, false); + } + + bool _3MF_Importer::_load_model_from_file(const std::string& filename, Model& model, DynamicPrintConfig& config, ConfigSubstitutionContext& config_substitutions) + { + mz_zip_archive archive; + mz_zip_zero_struct(&archive); + + if (!open_zip_reader(&archive, filename)) { + add_error("Unable to open the file"); + return false; + } + + mz_uint num_entries = mz_zip_reader_get_num_files(&archive); + + mz_zip_archive_file_stat stat; + + m_name = boost::filesystem::path(filename).stem().string(); + + // we first loop the entries to read from the archive the .model file only, in order to extract the version from it + for (mz_uint i = 0; i < num_entries; ++i) { + if (mz_zip_reader_file_stat(&archive, i, &stat)) { + std::string name(stat.m_filename); + std::replace(name.begin(), name.end(), '\\', '/'); + + if (boost::algorithm::istarts_with(name, MODEL_FOLDER) && boost::algorithm::iends_with(name, MODEL_EXTENSION)) { + try + { + // valid model name -> extract model + if (!_extract_model_from_archive(archive, stat)) { + close_zip_reader(&archive); + add_error("Archive does not contain a valid model"); + return false; + } + } + catch (const std::exception& e) + { + // ensure the zip archive is closed and rethrow the exception + close_zip_reader(&archive); + throw Slic3r::FileIOError(e.what()); + } + } + } + } + + // we then loop again the entries to read other files stored in the archive + for (mz_uint i = 0; i < num_entries; ++i) { + if (mz_zip_reader_file_stat(&archive, i, &stat)) { + std::string name(stat.m_filename); + std::replace(name.begin(), name.end(), '\\', '/'); + + if (boost::algorithm::iequals(name, LAYER_HEIGHTS_PROFILE_FILE)) { + // extract slic3r layer heights profile file + _extract_layer_heights_profile_config_from_archive(archive, stat); + } + else if (boost::algorithm::iequals(name, LAYER_CONFIG_RANGES_FILE)) { + // extract slic3r layer config ranges file + _extract_layer_config_ranges_from_archive(archive, stat, config_substitutions); + } + else if (boost::algorithm::iequals(name, SLA_SUPPORT_POINTS_FILE)) { + // 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 + _extract_print_config_from_archive(archive, stat, config, config_substitutions, filename); + } + else if (boost::algorithm::iequals(name, CUSTOM_GCODE_PER_PRINT_Z_FILE)) { + // extract slic3r layer config ranges file + _extract_custom_gcode_per_print_z_from_archive(archive, stat); + } + else if (boost::algorithm::iequals(name, MODEL_CONFIG_FILE)) { + // extract slic3r model config file + if (!_extract_model_config_from_archive(archive, stat, model)) { + close_zip_reader(&archive); + add_error("Archive does not contain a valid model config"); + return false; + } + } + } + } + + close_zip_reader(&archive); + + if (m_version == 0) { + // if the 3mf was not produced by PrusaSlicer and there is more than one instance, + // split the object in as many objects as instances + size_t curr_models_count = m_model->objects.size(); + size_t i = 0; + while (i < curr_models_count) { + ModelObject* model_object = m_model->objects[i]; + if (model_object->instances.size() > 1) { + // select the geometry associated with the original model object + const Geometry* geometry = nullptr; + for (const IdToModelObjectMap::value_type& object : m_objects) { + if (object.second == int(i)) { + IdToGeometryMap::const_iterator obj_geometry = m_geometries.find(object.first); + if (obj_geometry == m_geometries.end()) { + add_error("Unable to find object geometry"); + return false; + } + geometry = &obj_geometry->second; + break; + } + } + + if (geometry == nullptr) { + add_error("Unable to find object geometry"); + return false; + } + + // use the geometry to create the volumes in the new model objects + ObjectMetadata::VolumeMetadataList volumes(1, { 0, (unsigned int)geometry->triangles.size() - 1 }); + + // for each instance after the 1st, create a new model object containing only that instance + // and copy into it the geometry + while (model_object->instances.size() > 1) { + ModelObject* new_model_object = m_model->add_object(*model_object); + new_model_object->clear_instances(); + new_model_object->add_instance(*model_object->instances.back()); + model_object->delete_last_instance(); + if (!_generate_volumes(*new_model_object, *geometry, volumes, config_substitutions)) + return false; + } + } + ++i; + } + } + + for (const IdToModelObjectMap::value_type& object : m_objects) { + if (object.second >= int(m_model->objects.size())) { + add_error("Unable to find object"); + return false; + } + ModelObject* model_object = m_model->objects[object.second]; + IdToGeometryMap::const_iterator obj_geometry = m_geometries.find(object.first); + if (obj_geometry == m_geometries.end()) { + add_error("Unable to find object geometry"); + return false; + } + + // m_layer_heights_profiles are indexed by a 1 based model object index. + IdToLayerHeightsProfileMap::iterator obj_layer_heights_profile = m_layer_heights_profiles.find(object.second + 1); + if (obj_layer_heights_profile != m_layer_heights_profiles.end()) + model_object->layer_height_profile.set(std::move(obj_layer_heights_profile->second)); + + // m_layer_config_ranges are indexed by a 1 based model object index. + IdToLayerConfigRangesMap::iterator obj_layer_config_ranges = m_layer_config_ranges.find(object.second + 1); + if (obj_layer_config_ranges != m_layer_config_ranges.end()) + model_object->layer_config_ranges = std::move(obj_layer_config_ranges->second); + + // m_sla_support_points are indexed by a 1 based model object index. + IdToSlaSupportPointsMap::iterator obj_sla_support_points = m_sla_support_points.find(object.second + 1); + if (obj_sla_support_points != m_sla_support_points.end() && !obj_sla_support_points->second.empty()) { + model_object->sla_support_points = std::move(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 = std::move(obj_drain_holes->second); + } + + ObjectMetadata::VolumeMetadataList volumes; + ObjectMetadata::VolumeMetadataList* volumes_ptr = nullptr; + + IdToMetadataMap::iterator obj_metadata = m_objects_metadata.find(object.first); + if (obj_metadata != m_objects_metadata.end()) { + // config data has been found, this model was saved using slic3r pe + + // apply object's name and config data + for (const Metadata& metadata : obj_metadata->second.metadata) { + if (metadata.key == "name") + model_object->name = metadata.value; + else + model_object->config.set_deserialize(metadata.key, metadata.value, config_substitutions); + } + + // select object's detected volumes + volumes_ptr = &obj_metadata->second.volumes; + } + else { + // config data not found, this model was not saved using slic3r pe + + // add the entire geometry as the single volume to generate + volumes.emplace_back(0, (int)obj_geometry->second.triangles.size() - 1); + + // select as volumes + volumes_ptr = &volumes; + } + + if (!_generate_volumes(*model_object, obj_geometry->second, *volumes_ptr, config_substitutions)) + return false; + } + + // If instances contain a single volume, the volume offset should be 0,0,0 + // This equals to say that instance world position and volume world position should match + // Correct all instances/volumes for which this does not hold + for (int obj_id = 0; obj_id < int(model.objects.size()); ++obj_id) { + ModelObject* o = model.objects[obj_id]; + if (o->volumes.size() == 1) { + ModelVolume* v = o->volumes.front(); + const Slic3r::Geometry::Transformation& first_inst_trafo = o->instances.front()->get_transformation(); + const Vec3d world_vol_offset = (first_inst_trafo * v->get_transformation()).get_offset(); + const Vec3d world_inst_offset = first_inst_trafo.get_offset(); + + if (!world_vol_offset.isApprox(world_inst_offset)) { + const Slic3r::Geometry::Transformation& vol_trafo = v->get_transformation(); + for (int inst_id = 0; inst_id < int(o->instances.size()); ++inst_id) { + ModelInstance* i = o->instances[inst_id]; + const Slic3r::Geometry::Transformation& inst_trafo = i->get_transformation(); + i->set_offset((inst_trafo * vol_trafo).get_offset()); + } + v->set_offset(Vec3d::Zero()); + } + } + } + +#if ENABLE_RELOAD_FROM_DISK_REWORK + for (int obj_id = 0; obj_id < int(model.objects.size()); ++obj_id) { + ModelObject* o = model.objects[obj_id]; + for (int vol_id = 0; vol_id < int(o->volumes.size()); ++vol_id) { + ModelVolume* v = o->volumes[vol_id]; + if (v->source.input_file.empty()) + v->source.input_file = v->name.empty() ? filename : v->name; + if (v->source.volume_idx == -1) + v->source.volume_idx = vol_id; + if (v->source.object_idx == -1) + v->source.object_idx = obj_id; + } + } +#else + int object_idx = 0; + for (ModelObject* o : model.objects) { + int volume_idx = 0; + for (ModelVolume* v : o->volumes) { + if (v->source.input_file.empty() && v->type() == ModelVolumeType::MODEL_PART) { + v->source.input_file = filename; + if (v->source.volume_idx == -1) + v->source.volume_idx = volume_idx; + if (v->source.object_idx == -1) + v->source.object_idx = object_idx; + } + ++volume_idx; + } + ++object_idx; + } +#endif // ENABLE_RELOAD_FROM_DISK_REWORK + +// // fixes the min z of the model if negative +// model.adjust_min_z(); + + return true; + } + + bool _3MF_Importer::_extract_model_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat) + { + if (stat.m_uncomp_size == 0) { + add_error("Found invalid size"); + return false; + } + + _destroy_xml_parser(); + + m_xml_parser = XML_ParserCreate(nullptr); + if (m_xml_parser == nullptr) { + add_error("Unable to create parser"); + return false; + } + + XML_SetUserData(m_xml_parser, (void*)this); + XML_SetElementHandler(m_xml_parser, _3MF_Importer::_handle_start_model_xml_element, _3MF_Importer::_handle_end_model_xml_element); + XML_SetCharacterDataHandler(m_xml_parser, _3MF_Importer::_handle_model_xml_characters); + + struct CallbackData + { + XML_Parser& parser; + _3MF_Importer& importer; + const mz_zip_archive_file_stat& stat; + + CallbackData(XML_Parser& parser, _3MF_Importer& importer, const mz_zip_archive_file_stat& stat) : parser(parser), importer(importer), stat(stat) {} + }; + + CallbackData data(m_xml_parser, *this, stat); + + mz_bool res = 0; + + try + { + res = mz_zip_reader_extract_file_to_callback(&archive, stat.m_filename, [](void* pOpaque, mz_uint64 file_ofs, const void* pBuf, size_t n)->size_t { + CallbackData* data = (CallbackData*)pOpaque; + if (!XML_Parse(data->parser, (const char*)pBuf, (int)n, (file_ofs + n == data->stat.m_uncomp_size) ? 1 : 0) || data->importer.parse_error()) { + char error_buf[1024]; + ::sprintf(error_buf, "Error (%s) while parsing '%s' at line %d", data->importer.parse_error_message(), data->stat.m_filename, (int)XML_GetCurrentLineNumber(data->parser)); + throw Slic3r::FileIOError(error_buf); + } + + return n; + }, &data, 0); + } + catch (const version_error& e) + { + // rethrow the exception + throw Slic3r::FileIOError(e.what()); + } + catch (std::exception& e) + { + add_error(e.what()); + return false; + } + + if (res == 0) { + add_error("Error while extracting model data from zip archive"); + return false; + } + + return true; + } + + void _3MF_Importer::_extract_print_config_from_archive( + mz_zip_archive& archive, const mz_zip_archive_file_stat& stat, + DynamicPrintConfig& config, ConfigSubstitutionContext& config_substitutions, + const std::string& archive_filename) + { + 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 config data to buffer"); + return; + } + //FIXME Loading a "will be one day a legacy format" of configuration in a form of a G-code comment. + // Each config line is prefixed with a semicolon (G-code comment), that is ugly. + + // Replacing the legacy function with load_from_ini_string_commented leads to issues when + // parsing 3MFs from before PrusaSlicer 2.0.0 (which can have duplicated entries in the INI. + // See https://github.com/prusa3d/PrusaSlicer/issues/7155. We'll revert it for now. + //config_substitutions.substitutions = config.load_from_ini_string_commented(std::move(buffer), config_substitutions.rule); + ConfigBase::load_from_gcode_string_legacy(config, buffer.data(), config_substitutions); + } + } + + void _3MF_Importer::_extract_layer_heights_profile_config_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 layer heights profile 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); + + 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; + } + + IdToLayerHeightsProfileMap::iterator object_item = m_layer_heights_profiles.find(object_id); + if (object_item != m_layer_heights_profiles.end()) { + add_error("Found duplicated layer heights profile"); + continue; + } + + std::vector object_data_profile; + boost::split(object_data_profile, object_data[1], boost::is_any_of(";"), boost::token_compress_off); + if (object_data_profile.size() <= 4 || object_data_profile.size() % 2 != 0) { + add_error("Found invalid layer heights profile"); + continue; + } + + std::vector profile; + profile.reserve(object_data_profile.size()); + + for (const std::string& value : object_data_profile) { + profile.push_back((coordf_t)std::atof(value.c_str())); + } + + m_layer_heights_profiles.insert({ object_id, profile }); + } + } + } + + void _3MF_Importer::_extract_layer_config_ranges_from_archive(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat, ConfigSubstitutionContext& config_substitutions) + { + 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 layer config ranges data to buffer"); + return; + } + + std::istringstream iss(buffer); // wrap returned xml to istringstream + pt::ptree objects_tree; + pt::read_xml(iss, objects_tree); + + for (const auto& object : objects_tree.get_child("objects")) { + pt::ptree object_tree = object.second; + int obj_idx = object_tree.get(".id", -1); + if (obj_idx <= 0) { + add_error("Found invalid object id"); + continue; + } + + IdToLayerConfigRangesMap::iterator object_item = m_layer_config_ranges.find(obj_idx); + if (object_item != m_layer_config_ranges.end()) { + add_error("Found duplicated layer config range"); + continue; + } + + t_layer_config_ranges config_ranges; + + for (const auto& range : object_tree) { + if (range.first != "range") + continue; + pt::ptree range_tree = range.second; + double min_z = range_tree.get(".min_z"); + double max_z = range_tree.get(".max_z"); + + // get Z range information + DynamicPrintConfig config; + + for (const auto& option : range_tree) { + if (option.first != "option") + continue; + std::string opt_key = option.second.get(".opt_key"); + std::string value = option.second.data(); + config.set_deserialize(opt_key, value, config_substitutions); + } + + config_ranges[{ min_z, max_z }].assign_config(std::move(config)); + } + + if (!config_ranges.empty()) + m_layer_config_ranges.insert({ obj_idx, std::move(config_ranges) }); + } + } + } + + void _3MF_Importer::_extract_sla_support_points_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("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 + } + + 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; + } + + IdToSlaSupportPointsMap::iterator object_item = m_sla_support_points.find(object_id); + if (object_item != m_sla_support_points.end()) { + add_error("Found duplicated SLA support points"); + continue; + } + + std::vector object_data_points; + boost::split(object_data_points, object_data[1], boost::is_any_of(" "), boost::token_compress_off); + + std::vector sla_support_points; + + if (version == 0) { + for (unsigned int i=0; i 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; i 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 custom Gcodes per height data to buffer"); + return; + } + + std::istringstream iss(buffer); // wrap returned xml to istringstream + pt::ptree main_tree; + pt::read_xml(iss, main_tree); + + if (main_tree.front().first != "custom_gcodes_per_print_z") + return; + pt::ptree code_tree = main_tree.front().second; + + m_model->custom_gcode_per_print_z.gcodes.clear(); + + for (const auto& code : code_tree) { + if (code.first == "mode") { + pt::ptree tree = code.second; + std::string mode = tree.get(".value"); + m_model->custom_gcode_per_print_z.mode = mode == CustomGCode::SingleExtruderMode ? CustomGCode::Mode::SingleExtruder : + mode == CustomGCode::MultiAsSingleMode ? CustomGCode::Mode::MultiAsSingle : + CustomGCode::Mode::MultiExtruder; + } + if (code.first != "code") + continue; + + pt::ptree tree = code.second; + double print_z = tree.get (".print_z" ); + int extruder = tree.get (".extruder"); + std::string color = tree.get (".color" ); + + CustomGCode::Type type; + std::string extra; + pt::ptree attr_tree = tree.find("")->second; + if (attr_tree.find("type") == attr_tree.not_found()) { + // It means that data was saved in old version (2.2.0 and older) of PrusaSlicer + // read old data ... + std::string gcode = tree.get (".gcode"); + // ... and interpret them to the new data + type = gcode == "M600" ? CustomGCode::ColorChange : + gcode == "M601" ? CustomGCode::PausePrint : + gcode == "tool_change" ? CustomGCode::ToolChange : CustomGCode::Custom; + extra = type == CustomGCode::PausePrint ? color : + type == CustomGCode::Custom ? gcode : ""; + } + else { + type = static_cast(tree.get(".type")); + extra = tree.get(".extra"); + } + m_model->custom_gcode_per_print_z.gcodes.push_back(CustomGCode::Item{print_z, type, extruder, color, extra}) ; + } + } + } + + void _3MF_Importer::_handle_start_model_xml_element(const char* name, const char** attributes) + { + if (m_xml_parser == nullptr) + return; + + bool res = true; + unsigned int num_attributes = (unsigned int)XML_GetSpecifiedAttributeCount(m_xml_parser); + + if (::strcmp(MODEL_TAG, name) == 0) + res = _handle_start_model(attributes, num_attributes); + else if (::strcmp(RESOURCES_TAG, name) == 0) + res = _handle_start_resources(attributes, num_attributes); + else if (::strcmp(OBJECT_TAG, name) == 0) + res = _handle_start_object(attributes, num_attributes); + else if (::strcmp(MESH_TAG, name) == 0) + res = _handle_start_mesh(attributes, num_attributes); + else if (::strcmp(VERTICES_TAG, name) == 0) + res = _handle_start_vertices(attributes, num_attributes); + else if (::strcmp(VERTEX_TAG, name) == 0) + res = _handle_start_vertex(attributes, num_attributes); + else if (::strcmp(TRIANGLES_TAG, name) == 0) + res = _handle_start_triangles(attributes, num_attributes); + else if (::strcmp(TRIANGLE_TAG, name) == 0) + res = _handle_start_triangle(attributes, num_attributes); + else if (::strcmp(COMPONENTS_TAG, name) == 0) + res = _handle_start_components(attributes, num_attributes); + else if (::strcmp(COMPONENT_TAG, name) == 0) + res = _handle_start_component(attributes, num_attributes); + else if (::strcmp(BUILD_TAG, name) == 0) + res = _handle_start_build(attributes, num_attributes); + else if (::strcmp(ITEM_TAG, name) == 0) + res = _handle_start_item(attributes, num_attributes); + else if (::strcmp(METADATA_TAG, name) == 0) + res = _handle_start_metadata(attributes, num_attributes); + + if (!res) + _stop_xml_parser(); + } + + void _3MF_Importer::_handle_end_model_xml_element(const char* name) + { + if (m_xml_parser == nullptr) + return; + + bool res = true; + + if (::strcmp(MODEL_TAG, name) == 0) + res = _handle_end_model(); + else if (::strcmp(RESOURCES_TAG, name) == 0) + res = _handle_end_resources(); + else if (::strcmp(OBJECT_TAG, name) == 0) + res = _handle_end_object(); + else if (::strcmp(MESH_TAG, name) == 0) + res = _handle_end_mesh(); + else if (::strcmp(VERTICES_TAG, name) == 0) + res = _handle_end_vertices(); + else if (::strcmp(VERTEX_TAG, name) == 0) + res = _handle_end_vertex(); + else if (::strcmp(TRIANGLES_TAG, name) == 0) + res = _handle_end_triangles(); + else if (::strcmp(TRIANGLE_TAG, name) == 0) + res = _handle_end_triangle(); + else if (::strcmp(COMPONENTS_TAG, name) == 0) + res = _handle_end_components(); + else if (::strcmp(COMPONENT_TAG, name) == 0) + res = _handle_end_component(); + else if (::strcmp(BUILD_TAG, name) == 0) + res = _handle_end_build(); + else if (::strcmp(ITEM_TAG, name) == 0) + res = _handle_end_item(); + else if (::strcmp(METADATA_TAG, name) == 0) + res = _handle_end_metadata(); + + if (!res) + _stop_xml_parser(); + } + + void _3MF_Importer::_handle_model_xml_characters(const XML_Char* s, int len) + { + m_curr_characters.append(s, len); + } + + void _3MF_Importer::_handle_start_config_xml_element(const char* name, const char** attributes) + { + if (m_xml_parser == nullptr) + return; + + bool res = true; + unsigned int num_attributes = (unsigned int)XML_GetSpecifiedAttributeCount(m_xml_parser); + + if (::strcmp(CONFIG_TAG, name) == 0) + res = _handle_start_config(attributes, num_attributes); + else if (::strcmp(OBJECT_TAG, name) == 0) + res = _handle_start_config_object(attributes, num_attributes); + else if (::strcmp(VOLUME_TAG, name) == 0) + res = _handle_start_config_volume(attributes, num_attributes); + else if (::strcmp(MESH_TAG, name) == 0) + res = _handle_start_config_volume_mesh(attributes, num_attributes); + else if (::strcmp(METADATA_TAG, name) == 0) + res = _handle_start_config_metadata(attributes, num_attributes); + + if (!res) + _stop_xml_parser(); + } + + void _3MF_Importer::_handle_end_config_xml_element(const char* name) + { + if (m_xml_parser == nullptr) + return; + + bool res = true; + + if (::strcmp(CONFIG_TAG, name) == 0) + res = _handle_end_config(); + else if (::strcmp(OBJECT_TAG, name) == 0) + res = _handle_end_config_object(); + else if (::strcmp(VOLUME_TAG, name) == 0) + res = _handle_end_config_volume(); + else if (::strcmp(MESH_TAG, name) == 0) + res = _handle_end_config_volume_mesh(); + else if (::strcmp(METADATA_TAG, name) == 0) + res = _handle_end_config_metadata(); + + if (!res) + _stop_xml_parser(); + } + + bool _3MF_Importer::_handle_start_model(const char** attributes, unsigned int num_attributes) + { + m_unit_factor = get_unit_factor(get_attribute_value_string(attributes, num_attributes, UNIT_ATTR)); + return true; + } + + bool _3MF_Importer::_handle_end_model() + { + // deletes all non-built or non-instanced objects + for (const IdToModelObjectMap::value_type& object : m_objects) { + if (object.second >= int(m_model->objects.size())) { + add_error("Unable to find object"); + return false; + } + ModelObject *model_object = m_model->objects[object.second]; + if (model_object != nullptr && model_object->instances.size() == 0) + m_model->delete_object(model_object); + } + + if (m_version == 0) { + // if the 3mf was not produced by PrusaSlicer and there is only one object, + // set the object name to match the filename + if (m_model->objects.size() == 1) + m_model->objects.front()->name = m_name; + } + + // applies instances' matrices + for (Instance& instance : m_instances) { + if (instance.instance != nullptr && instance.instance->get_object() != nullptr) + // apply the transform to the instance + _apply_transform(*instance.instance, instance.transform); + } + + return true; + } + + bool _3MF_Importer::_handle_start_resources(const char** attributes, unsigned int num_attributes) + { + // do nothing + return true; + } + + bool _3MF_Importer::_handle_end_resources() + { + // do nothing + return true; + } + + bool _3MF_Importer::_handle_start_object(const char** attributes, unsigned int num_attributes) + { + // reset current data + m_curr_object.reset(); + + if (is_valid_object_type(get_attribute_value_string(attributes, num_attributes, TYPE_ATTR))) { + // create new object (it may be removed later if no instances are generated from it) + m_curr_object.model_object_idx = (int)m_model->objects.size(); + m_curr_object.object = m_model->add_object(); + if (m_curr_object.object == nullptr) { + add_error("Unable to create object"); + return false; + } + + // set object data + m_curr_object.object->name = get_attribute_value_string(attributes, num_attributes, NAME_ATTR); + if (m_curr_object.object->name.empty()) + m_curr_object.object->name = m_name + "_" + std::to_string(m_model->objects.size()); + + m_curr_object.id = get_attribute_value_int(attributes, num_attributes, ID_ATTR); + } + + return true; + } + + bool _3MF_Importer::_handle_end_object() + { + if (m_curr_object.object != nullptr) { + if (m_curr_object.geometry.empty()) { + // no geometry defined + // remove the object from the model + m_model->delete_object(m_curr_object.object); + + if (m_curr_object.components.empty()) { + // no components defined -> invalid object, delete it + IdToModelObjectMap::iterator object_item = m_objects.find(m_curr_object.id); + if (object_item != m_objects.end()) + m_objects.erase(object_item); + + IdToAliasesMap::iterator alias_item = m_objects_aliases.find(m_curr_object.id); + if (alias_item != m_objects_aliases.end()) + m_objects_aliases.erase(alias_item); + } + else + // adds components to aliases + m_objects_aliases.insert({ m_curr_object.id, m_curr_object.components }); + } + else { + // geometry defined, store it for later use + m_geometries.insert({ m_curr_object.id, std::move(m_curr_object.geometry) }); + + // stores the object for later use + if (m_objects.find(m_curr_object.id) == m_objects.end()) { + m_objects.insert({ m_curr_object.id, m_curr_object.model_object_idx }); + m_objects_aliases.insert({ m_curr_object.id, { 1, Component(m_curr_object.id) } }); // aliases itself + } + else { + add_error("Found object with duplicate id"); + return false; + } + } + } + + return true; + } + + bool _3MF_Importer::_handle_start_mesh(const char** attributes, unsigned int num_attributes) + { + // reset current geometry + m_curr_object.geometry.reset(); + return true; + } + + bool _3MF_Importer::_handle_end_mesh() + { + // do nothing + return true; + } + + bool _3MF_Importer::_handle_start_vertices(const char** attributes, unsigned int num_attributes) + { + // reset current vertices + m_curr_object.geometry.vertices.clear(); + return true; + } + + bool _3MF_Importer::_handle_end_vertices() + { + // do nothing + return true; + } + + bool _3MF_Importer::_handle_start_vertex(const char** attributes, unsigned int num_attributes) + { + // appends the vertex coordinates + // missing values are set equal to ZERO + m_curr_object.geometry.vertices.emplace_back( + m_unit_factor * get_attribute_value_float(attributes, num_attributes, X_ATTR), + m_unit_factor * get_attribute_value_float(attributes, num_attributes, Y_ATTR), + m_unit_factor * get_attribute_value_float(attributes, num_attributes, Z_ATTR)); + return true; + } + + bool _3MF_Importer::_handle_end_vertex() + { + // do nothing + return true; + } + + bool _3MF_Importer::_handle_start_triangles(const char** attributes, unsigned int num_attributes) + { + // reset current triangles + m_curr_object.geometry.triangles.clear(); + return true; + } + + bool _3MF_Importer::_handle_end_triangles() + { + // do nothing + return true; + } + + bool _3MF_Importer::_handle_start_triangle(const char** attributes, unsigned int num_attributes) + { + // we are ignoring the following attributes: + // p1 + // p2 + // p3 + // pid + // see specifications + + // appends the triangle's vertices indices + // missing values are set equal to ZERO + m_curr_object.geometry.triangles.emplace_back( + get_attribute_value_int(attributes, num_attributes, V1_ATTR), + get_attribute_value_int(attributes, num_attributes, V2_ATTR), + get_attribute_value_int(attributes, num_attributes, V3_ATTR)); + + m_curr_object.geometry.custom_supports.push_back(get_attribute_value_string(attributes, num_attributes, CUSTOM_SUPPORTS_ATTR)); + m_curr_object.geometry.custom_seam.push_back(get_attribute_value_string(attributes, num_attributes, CUSTOM_SEAM_ATTR)); + m_curr_object.geometry.mmu_segmentation.push_back(get_attribute_value_string(attributes, num_attributes, MMU_SEGMENTATION_ATTR)); + return true; + } + + bool _3MF_Importer::_handle_end_triangle() + { + // do nothing + return true; + } + + bool _3MF_Importer::_handle_start_components(const char** attributes, unsigned int num_attributes) + { + // reset current components + m_curr_object.components.clear(); + return true; + } + + bool _3MF_Importer::_handle_end_components() + { + // do nothing + return true; + } + + bool _3MF_Importer::_handle_start_component(const char** attributes, unsigned int num_attributes) + { + int object_id = get_attribute_value_int(attributes, num_attributes, OBJECTID_ATTR); + Transform3d transform = get_transform_from_3mf_specs_string(get_attribute_value_string(attributes, num_attributes, TRANSFORM_ATTR)); + + IdToModelObjectMap::iterator object_item = m_objects.find(object_id); + if (object_item == m_objects.end()) { + IdToAliasesMap::iterator alias_item = m_objects_aliases.find(object_id); + if (alias_item == m_objects_aliases.end()) { + add_error("Found component with invalid object id"); + return false; + } + } + + m_curr_object.components.emplace_back(object_id, transform); + + return true; + } + + bool _3MF_Importer::_handle_end_component() + { + // do nothing + return true; + } + + bool _3MF_Importer::_handle_start_build(const char** attributes, unsigned int num_attributes) + { + // do nothing + return true; + } + + bool _3MF_Importer::_handle_end_build() + { + // do nothing + return true; + } + + bool _3MF_Importer::_handle_start_item(const char** attributes, unsigned int num_attributes) + { + // we are ignoring the following attributes + // thumbnail + // partnumber + // pid + // pindex + // see specifications + + int object_id = get_attribute_value_int(attributes, num_attributes, OBJECTID_ATTR); + Transform3d transform = get_transform_from_3mf_specs_string(get_attribute_value_string(attributes, num_attributes, TRANSFORM_ATTR)); + int printable = get_attribute_value_bool(attributes, num_attributes, PRINTABLE_ATTR); + + return _create_object_instance(object_id, transform, printable, 1); + } + + bool _3MF_Importer::_handle_end_item() + { + // do nothing + return true; + } + + bool _3MF_Importer::_handle_start_metadata(const char** attributes, unsigned int num_attributes) + { + m_curr_characters.clear(); + + std::string name = get_attribute_value_string(attributes, num_attributes, NAME_ATTR); + if (!name.empty()) + m_curr_metadata_name = name; + + return true; + } + + inline static void check_painting_version(unsigned int loaded_version, unsigned int highest_supported_version, const std::string &error_msg) + { + if (loaded_version > highest_supported_version) + throw version_error(error_msg); + } + + bool _3MF_Importer::_handle_end_metadata() + { + if (m_curr_metadata_name == SLIC3RPE_3MF_VERSION) { + m_version = (unsigned int)atoi(m_curr_characters.c_str()); + if (m_check_version && (m_version > VERSION_3MF_COMPATIBLE)) { + // std::string msg = _(L("The selected 3mf file has been saved with a newer version of " + std::string(SLIC3R_APP_NAME) + " and is not compatible.")); + // throw version_error(msg.c_str()); + const std::string msg = (boost::format(_(L("The selected 3mf file has been saved with a newer version of %1% and is not compatible."))) % std::string(SLIC3R_APP_NAME)).str(); + throw version_error(msg); + } + } else if (m_curr_metadata_name == "Application") { + // Generator application of the 3MF. + // SLIC3R_APP_KEY - SLIC3R_VERSION + if (boost::starts_with(m_curr_characters, "PrusaSlicer-")) + m_prusaslicer_generator_version = Semver::parse(m_curr_characters.substr(12)); + } else if (m_curr_metadata_name == SLIC3RPE_FDM_SUPPORTS_PAINTING_VERSION) { + m_fdm_supports_painting_version = (unsigned int) atoi(m_curr_characters.c_str()); + check_painting_version(m_fdm_supports_painting_version, FDM_SUPPORTS_PAINTING_VERSION, + _(L("The selected 3MF contains FDM supports painted object using a newer version of PrusaSlicer and is not compatible."))); + } else if (m_curr_metadata_name == SLIC3RPE_SEAM_PAINTING_VERSION) { + m_seam_painting_version = (unsigned int) atoi(m_curr_characters.c_str()); + check_painting_version(m_seam_painting_version, SEAM_PAINTING_VERSION, + _(L("The selected 3MF contains seam painted object using a newer version of PrusaSlicer and is not compatible."))); + } else if (m_curr_metadata_name == SLIC3RPE_MM_PAINTING_VERSION) { + m_mm_painting_version = (unsigned int) atoi(m_curr_characters.c_str()); + check_painting_version(m_mm_painting_version, MM_PAINTING_VERSION, + _(L("The selected 3MF contains multi-material painted object using a newer version of PrusaSlicer and is not compatible."))); + } + + return true; + } + + bool _3MF_Importer::_create_object_instance(int object_id, const Transform3d& transform, const bool printable, unsigned int recur_counter) + { + static const unsigned int MAX_RECURSIONS = 10; + + // escape from circular aliasing + if (recur_counter > MAX_RECURSIONS) { + add_error("Too many recursions"); + return false; + } + + IdToAliasesMap::iterator it = m_objects_aliases.find(object_id); + if (it == m_objects_aliases.end()) { + add_error("Found item with invalid object id"); + return false; + } + + if (it->second.size() == 1 && it->second[0].object_id == object_id) { + // aliasing to itself + + IdToModelObjectMap::iterator object_item = m_objects.find(object_id); + if (object_item == m_objects.end() || object_item->second == -1) { + add_error("Found invalid object"); + return false; + } + else { + ModelInstance* instance = m_model->objects[object_item->second]->add_instance(); + if (instance == nullptr) { + add_error("Unable to add object instance"); + return false; + } + instance->printable = printable; + + m_instances.emplace_back(instance, transform); + } + } + else { + // recursively process nested components + for (const Component& component : it->second) { + if (!_create_object_instance(component.object_id, transform * component.transform, printable, recur_counter + 1)) + return false; + } + } + + return true; + } + + void _3MF_Importer::_apply_transform(ModelInstance& instance, const Transform3d& transform) + { + Slic3r::Geometry::Transformation t(transform); + // invalid scale value, return + if (!t.get_scaling_factor().all()) + return; + + instance.set_transformation(t); + } + + bool _3MF_Importer::_handle_start_config(const char** attributes, unsigned int num_attributes) + { + // do nothing + return true; + } + + bool _3MF_Importer::_handle_end_config() + { + // do nothing + return true; + } + + bool _3MF_Importer::_handle_start_config_object(const char** attributes, unsigned int num_attributes) + { + int object_id = get_attribute_value_int(attributes, num_attributes, ID_ATTR); + IdToMetadataMap::iterator object_item = m_objects_metadata.find(object_id); + if (object_item != m_objects_metadata.end()) { + add_error("Found duplicated object id"); + return false; + } + + // Added because of github #3435, currently not used by PrusaSlicer + // int instances_count_id = get_attribute_value_int(attributes, num_attributes, INSTANCESCOUNT_ATTR); + + m_objects_metadata.insert({ object_id, ObjectMetadata() }); + m_curr_config.object_id = object_id; + return true; + } + + bool _3MF_Importer::_handle_end_config_object() + { + // do nothing + return true; + } + + bool _3MF_Importer::_handle_start_config_volume(const char** attributes, unsigned int num_attributes) + { + IdToMetadataMap::iterator object = m_objects_metadata.find(m_curr_config.object_id); + if (object == m_objects_metadata.end()) { + add_error("Cannot assign volume to a valid object"); + return false; + } + + m_curr_config.volume_id = (int)object->second.volumes.size(); + + unsigned int first_triangle_id = (unsigned int)get_attribute_value_int(attributes, num_attributes, FIRST_TRIANGLE_ID_ATTR); + unsigned int last_triangle_id = (unsigned int)get_attribute_value_int(attributes, num_attributes, LAST_TRIANGLE_ID_ATTR); + + object->second.volumes.emplace_back(first_triangle_id, last_triangle_id); + return true; + } + + bool _3MF_Importer::_handle_start_config_volume_mesh(const char** attributes, unsigned int num_attributes) + { + IdToMetadataMap::iterator object = m_objects_metadata.find(m_curr_config.object_id); + if (object == m_objects_metadata.end()) { + add_error("Cannot assign volume mesh to a valid object"); + return false; + } + if (object->second.volumes.empty()) { + add_error("Cannot assign mesh to a valid olume"); + return false; + } + + ObjectMetadata::VolumeMetadata& volume = object->second.volumes.back(); + + int edges_fixed = get_attribute_value_int(attributes, num_attributes, MESH_STAT_EDGES_FIXED ); + int degenerate_facets = get_attribute_value_int(attributes, num_attributes, MESH_STAT_DEGENERATED_FACETS); + int facets_removed = get_attribute_value_int(attributes, num_attributes, MESH_STAT_FACETS_REMOVED ); + int facets_reversed = get_attribute_value_int(attributes, num_attributes, MESH_STAT_FACETS_RESERVED ); + int backwards_edges = get_attribute_value_int(attributes, num_attributes, MESH_STAT_BACKWARDS_EDGES ); + + volume.mesh_stats = { edges_fixed, degenerate_facets, facets_removed, facets_reversed, backwards_edges }; + + return true; + } + + bool _3MF_Importer::_handle_end_config_volume() + { + // do nothing + return true; + } + + bool _3MF_Importer::_handle_end_config_volume_mesh() + { + // do nothing + return true; + } + + bool _3MF_Importer::_handle_start_config_metadata(const char** attributes, unsigned int num_attributes) + { + IdToMetadataMap::iterator object = m_objects_metadata.find(m_curr_config.object_id); + if (object == m_objects_metadata.end()) { + add_error("Cannot assign metadata to valid object id"); + return false; + } + + std::string type = get_attribute_value_string(attributes, num_attributes, TYPE_ATTR); + std::string key = get_attribute_value_string(attributes, num_attributes, KEY_ATTR); + std::string value = get_attribute_value_string(attributes, num_attributes, VALUE_ATTR); + + if (type == OBJECT_TYPE) + object->second.metadata.emplace_back(key, value); + else if (type == VOLUME_TYPE) { + if (size_t(m_curr_config.volume_id) < object->second.volumes.size()) + object->second.volumes[m_curr_config.volume_id].metadata.emplace_back(key, value); + } + else { + add_error("Found invalid metadata type"); + return false; + } + + return true; + } + + bool _3MF_Importer::_handle_end_config_metadata() + { + // do nothing + return true; + } + + bool _3MF_Importer::_generate_volumes(ModelObject& object, const Geometry& geometry, const ObjectMetadata::VolumeMetadataList& volumes, ConfigSubstitutionContext& config_substitutions) + { + if (!object.volumes.empty()) { + add_error("Found invalid volumes count"); + return false; + } + + unsigned int geo_tri_count = (unsigned int)geometry.triangles.size(); + unsigned int renamed_volumes_count = 0; + + for (const ObjectMetadata::VolumeMetadata& volume_data : volumes) { + if (geo_tri_count <= volume_data.first_triangle_id || geo_tri_count <= volume_data.last_triangle_id || volume_data.last_triangle_id < volume_data.first_triangle_id) { + add_error("Found invalid triangle id"); + return false; + } + + Transform3d volume_matrix_to_object = Transform3d::Identity(); + bool has_transform = false; + // extract the volume transformation from the volume's metadata, if present + for (const Metadata& metadata : volume_data.metadata) { + if (metadata.key == MATRIX_KEY) { + volume_matrix_to_object = Slic3r::Geometry::transform3d_from_string(metadata.value); + has_transform = ! volume_matrix_to_object.isApprox(Transform3d::Identity(), 1e-10); + break; + } + } + + // splits volume out of imported geometry + indexed_triangle_set its; + its.indices.assign(geometry.triangles.begin() + volume_data.first_triangle_id, geometry.triangles.begin() + volume_data.last_triangle_id + 1); + const size_t triangles_count = its.indices.size(); + if (triangles_count == 0) { + add_error("An empty triangle mesh found"); + return false; + } + + { + int min_id = its.indices.front()[0]; + int max_id = min_id; + for (const Vec3i& face : its.indices) { + for (const int tri_id : face) { + if (tri_id < 0 || tri_id >= int(geometry.vertices.size())) { + add_error("Found invalid vertex id"); + return false; + } + min_id = std::min(min_id, tri_id); + max_id = std::max(max_id, tri_id); + } + } + its.vertices.assign(geometry.vertices.begin() + min_id, geometry.vertices.begin() + max_id + 1); + + // rebase indices to the current vertices list + for (Vec3i& face : its.indices) + for (int& tri_id : face) + tri_id -= min_id; + } + + if (m_prusaslicer_generator_version && + *m_prusaslicer_generator_version >= *Semver::parse("2.4.0-alpha1") && + *m_prusaslicer_generator_version < *Semver::parse("2.4.0-alpha3")) + // PrusaSlicer 2.4.0-alpha2 contained a bug, where all vertices of a single object were saved for each volume the object contained. + // Remove the vertices, that are not referenced by any face. + its_compactify_vertices(its, true); + + TriangleMesh triangle_mesh(std::move(its), volume_data.mesh_stats); + + if (m_version == 0) { + // if the 3mf was not produced by PrusaSlicer and there is only one instance, + // bake the transformation into the geometry to allow the reload from disk command + // to work properly + if (object.instances.size() == 1) { + triangle_mesh.transform(object.instances.front()->get_transformation().get_matrix(), false); + object.instances.front()->set_transformation(Slic3r::Geometry::Transformation()); + //FIXME do the mesh fixing? + } + } + if (triangle_mesh.volume() < 0) + triangle_mesh.flip_triangles(); + + ModelVolume* volume = object.add_volume(std::move(triangle_mesh)); + // stores the volume matrix taken from the metadata, if present + if (has_transform) + volume->source.transform = Slic3r::Geometry::Transformation(volume_matrix_to_object); + + // recreate custom supports, seam and mmu segmentation from previously loaded attribute + volume->supported_facets.reserve(triangles_count); + volume->seam_facets.reserve(triangles_count); + volume->mmu_segmentation_facets.reserve(triangles_count); + for (size_t i=0; isupported_facets.set_triangle_from_string(i, geometry.custom_supports[index]); + if (! geometry.custom_seam[index].empty()) + volume->seam_facets.set_triangle_from_string(i, geometry.custom_seam[index]); + if (! geometry.mmu_segmentation[index].empty()) + volume->mmu_segmentation_facets.set_triangle_from_string(i, geometry.mmu_segmentation[index]); + } + volume->supported_facets.shrink_to_fit(); + volume->seam_facets.shrink_to_fit(); + volume->mmu_segmentation_facets.shrink_to_fit(); + + // apply the remaining volume's metadata + for (const Metadata& metadata : volume_data.metadata) { + if (metadata.key == NAME_KEY) + volume->name = metadata.value; + else if ((metadata.key == MODIFIER_KEY) && (metadata.value == "1")) + volume->set_type(ModelVolumeType::PARAMETER_MODIFIER); + else if (metadata.key == VOLUME_TYPE_KEY) + volume->set_type(ModelVolume::type_from_string(metadata.value)); + else if (metadata.key == SOURCE_FILE_KEY) + volume->source.input_file = metadata.value; + else if (metadata.key == SOURCE_OBJECT_ID_KEY) + volume->source.object_idx = ::atoi(metadata.value.c_str()); + else if (metadata.key == SOURCE_VOLUME_ID_KEY) + volume->source.volume_idx = ::atoi(metadata.value.c_str()); + else if (metadata.key == SOURCE_OFFSET_X_KEY) + volume->source.mesh_offset.x() = ::atof(metadata.value.c_str()); + else if (metadata.key == SOURCE_OFFSET_Y_KEY) + volume->source.mesh_offset.y() = ::atof(metadata.value.c_str()); + else if (metadata.key == SOURCE_OFFSET_Z_KEY) + volume->source.mesh_offset.z() = ::atof(metadata.value.c_str()); + else if (metadata.key == SOURCE_IN_INCHES_KEY) + volume->source.is_converted_from_inches = metadata.value == "1"; + else if (metadata.key == SOURCE_IN_METERS_KEY) + volume->source.is_converted_from_meters = metadata.value == "1"; +#if ENABLE_RELOAD_FROM_DISK_REWORK + else if (metadata.key == SOURCE_IS_BUILTIN_VOLUME_KEY) + volume->source.is_from_builtin_objects = metadata.value == "1"; +#endif // ENABLE_RELOAD_FROM_DISK_REWORK + else + volume->config.set_deserialize(metadata.key, metadata.value, config_substitutions); + } + + // this may happen for 3mf saved by 3rd part softwares + if (volume->name.empty()) { + volume->name = object.name; + if (renamed_volumes_count > 0) + volume->name += "_" + std::to_string(renamed_volumes_count + 1); + ++renamed_volumes_count; + } + } + + return true; + } + + void XMLCALL _3MF_Importer::_handle_start_model_xml_element(void* userData, const char* name, const char** attributes) + { + _3MF_Importer* importer = (_3MF_Importer*)userData; + if (importer != nullptr) + importer->_handle_start_model_xml_element(name, attributes); + } + + void XMLCALL _3MF_Importer::_handle_end_model_xml_element(void* userData, const char* name) + { + _3MF_Importer* importer = (_3MF_Importer*)userData; + if (importer != nullptr) + importer->_handle_end_model_xml_element(name); + } + + void XMLCALL _3MF_Importer::_handle_model_xml_characters(void* userData, const XML_Char* s, int len) + { + _3MF_Importer* importer = (_3MF_Importer*)userData; + if (importer != nullptr) + importer->_handle_model_xml_characters(s, len); + } + + void XMLCALL _3MF_Importer::_handle_start_config_xml_element(void* userData, const char* name, const char** attributes) + { + _3MF_Importer* importer = (_3MF_Importer*)userData; + if (importer != nullptr) + importer->_handle_start_config_xml_element(name, attributes); + } + + void XMLCALL _3MF_Importer::_handle_end_config_xml_element(void* userData, const char* name) + { + _3MF_Importer* importer = (_3MF_Importer*)userData; + if (importer != nullptr) + importer->_handle_end_config_xml_element(name); + } + + class _3MF_Exporter : public _3MF_Base + { + struct BuildItem + { + unsigned int id; + Transform3d transform; + bool printable; + + BuildItem(unsigned int id, const Transform3d& transform, const bool printable) + : id(id) + , transform(transform) + , printable(printable) + { + } + }; + + struct Offsets + { + unsigned int first_vertex_id; + unsigned int first_triangle_id; + unsigned int last_triangle_id; + + Offsets(unsigned int first_vertex_id) + : first_vertex_id(first_vertex_id) + , first_triangle_id(-1) + , last_triangle_id(-1) + { + } + }; + + typedef std::map VolumeToOffsetsMap; + + struct ObjectData + { + ModelObject* object; + VolumeToOffsetsMap volumes_offsets; + + explicit ObjectData(ModelObject* object) + : object(object) + { + } + }; + + typedef std::vector BuildItemsList; + typedef std::map IdToObjectDataMap; + + bool m_fullpath_sources{ true }; + bool m_zip64 { true }; + + public: + bool save_model_to_file(const std::string& filename, Model& model, const DynamicPrintConfig* config, bool fullpath_sources, const ThumbnailData* thumbnail_data, bool zip64); + + private: + bool _save_model_to_file(const std::string& filename, Model& model, const DynamicPrintConfig* config, const ThumbnailData* thumbnail_data); + bool _add_content_types_file_to_archive(mz_zip_archive& archive); + bool _add_thumbnail_file_to_archive(mz_zip_archive& archive, const ThumbnailData& thumbnail_data); + bool _add_relationships_file_to_archive(mz_zip_archive& archive); + bool _add_model_file_to_archive(const std::string& filename, mz_zip_archive& archive, const Model& model, IdToObjectDataMap& objects_data); + bool _add_object_to_model_stream(mz_zip_writer_staged_context &context, unsigned int& object_id, ModelObject& object, BuildItemsList& build_items, VolumeToOffsetsMap& volumes_offsets); + bool _add_mesh_to_object_stream(mz_zip_writer_staged_context &context, ModelObject& object, VolumeToOffsetsMap& volumes_offsets); + bool _add_build_to_model_stream(std::stringstream& stream, const BuildItemsList& build_items); + bool _add_layer_height_profile_file_to_archive(mz_zip_archive& archive, Model& model); + bool _add_layer_config_ranges_file_to_archive(mz_zip_archive& archive, Model& model); + bool _add_sla_support_points_file_to_archive(mz_zip_archive& archive, Model& model); + bool _add_sla_drain_holes_file_to_archive(mz_zip_archive& archive, Model& model); + bool _add_print_config_file_to_archive(mz_zip_archive& archive, const DynamicPrintConfig &config); + bool _add_model_config_file_to_archive(mz_zip_archive& archive, const Model& model, const IdToObjectDataMap &objects_data); + bool _add_custom_gcode_per_print_z_file_to_archive(mz_zip_archive& archive, Model& model, const DynamicPrintConfig* config); + }; + + bool _3MF_Exporter::save_model_to_file(const std::string& filename, Model& model, const DynamicPrintConfig* config, bool fullpath_sources, const ThumbnailData* thumbnail_data, bool zip64) + { + clear_errors(); + m_fullpath_sources = fullpath_sources; + m_zip64 = zip64; + return _save_model_to_file(filename, model, config, thumbnail_data); + } + + bool _3MF_Exporter::_save_model_to_file(const std::string& filename, Model& model, const DynamicPrintConfig* config, const ThumbnailData* thumbnail_data) + { + mz_zip_archive archive; + mz_zip_zero_struct(&archive); + + if (!open_zip_writer(&archive, filename)) { + add_error("Unable to open the file"); + return false; + } + + // Adds content types file ("[Content_Types].xml";). + // The content of this file is the same for each PrusaSlicer 3mf. + if (!_add_content_types_file_to_archive(archive)) { + close_zip_writer(&archive); + boost::filesystem::remove(filename); + return false; + } + + if (thumbnail_data != nullptr && thumbnail_data->is_valid()) { + // Adds the file Metadata/thumbnail.png. + if (!_add_thumbnail_file_to_archive(archive, *thumbnail_data)) { + close_zip_writer(&archive); + boost::filesystem::remove(filename); + return false; + } + } + + // Adds relationships file ("_rels/.rels"). + // The content of this file is the same for each PrusaSlicer 3mf. + // The relationshis file contains a reference to the geometry file "3D/3dmodel.model", the name was chosen to be compatible with CURA. + if (!_add_relationships_file_to_archive(archive)) { + close_zip_writer(&archive); + boost::filesystem::remove(filename); + return false; + } + + // Adds model file ("3D/3dmodel.model"). + // This is the one and only file that contains all the geometry (vertices and triangles) of all ModelVolumes. + IdToObjectDataMap objects_data; + if (!_add_model_file_to_archive(filename, archive, model, objects_data)) { + close_zip_writer(&archive); + boost::filesystem::remove(filename); + return false; + } + + // Adds layer height profile file ("Metadata/Slic3r_PE_layer_heights_profile.txt"). + // All layer height profiles of all ModelObjects are stored here, indexed by 1 based index of the ModelObject in Model. + // The index differes from the index of an object ID of an object instance of a 3MF file! + if (!_add_layer_height_profile_file_to_archive(archive, model)) { + close_zip_writer(&archive); + boost::filesystem::remove(filename); + return false; + } + + // Adds layer config ranges file ("Metadata/Slic3r_PE_layer_config_ranges.txt"). + // All layer height profiles of all ModelObjects are stored here, indexed by 1 based index of the ModelObject in Model. + // The index differes from the index of an object ID of an object instance of a 3MF file! + if (!_add_layer_config_ranges_file_to_archive(archive, model)) { + close_zip_writer(&archive); + boost::filesystem::remove(filename); + return false; + } + + // Adds sla support points file ("Metadata/Slic3r_PE_sla_support_points.txt"). + // All sla support points of all ModelObjects are stored here, indexed by 1 based index of the ModelObject in Model. + // The index differes from the index of an object ID of an object instance of a 3MF file! + if (!_add_sla_support_points_file_to_archive(archive, model)) { + close_zip_writer(&archive); + boost::filesystem::remove(filename); + return false; + } + + if (!_add_sla_drain_holes_file_to_archive(archive, model)) { + close_zip_writer(&archive); + boost::filesystem::remove(filename); + return false; + } + + + // Adds custom gcode per height file ("Metadata/Prusa_Slicer_custom_gcode_per_print_z.xml"). + // All custom gcode per height of whole Model are stored here + if (!_add_custom_gcode_per_print_z_file_to_archive(archive, model, config)) { + close_zip_writer(&archive); + boost::filesystem::remove(filename); + return false; + } + + // Adds slic3r print config file ("Metadata/Slic3r_PE.config"). + // This file contains the content of FullPrintConfing / SLAFullPrintConfig. + if (config != nullptr) { + if (!_add_print_config_file_to_archive(archive, *config)) { + close_zip_writer(&archive); + boost::filesystem::remove(filename); + return false; + } + } + + // Adds slic3r model config file ("Metadata/Slic3r_PE_model.config"). + // This file contains all the attributes of all ModelObjects and their ModelVolumes (names, parameter overrides). + // As there is just a single Indexed Triangle Set data stored per ModelObject, offsets of volumes into their respective Indexed Triangle Set data + // is stored here as well. + if (!_add_model_config_file_to_archive(archive, model, objects_data)) { + close_zip_writer(&archive); + boost::filesystem::remove(filename); + return false; + } + + if (!mz_zip_writer_finalize_archive(&archive)) { + close_zip_writer(&archive); + boost::filesystem::remove(filename); + add_error("Unable to finalize the archive"); + return false; + } + + close_zip_writer(&archive); + + return true; + } + + bool _3MF_Exporter::_add_content_types_file_to_archive(mz_zip_archive& archive) + { + std::stringstream stream; + stream << "\n"; + stream << "\n"; + stream << " \n"; + stream << " \n"; + stream << " \n"; + stream << ""; + + std::string out = stream.str(); + + if (!mz_zip_writer_add_mem(&archive, CONTENT_TYPES_FILE.c_str(), (const void*)out.data(), out.length(), MZ_DEFAULT_COMPRESSION)) { + add_error("Unable to add content types file to archive"); + return false; + } + + return true; + } + + bool _3MF_Exporter::_add_thumbnail_file_to_archive(mz_zip_archive& archive, const ThumbnailData& thumbnail_data) + { + bool res = false; + + size_t png_size = 0; + void* png_data = tdefl_write_image_to_png_file_in_memory_ex((const void*)thumbnail_data.pixels.data(), thumbnail_data.width, thumbnail_data.height, 4, &png_size, MZ_DEFAULT_LEVEL, 1); + if (png_data != nullptr) { + res = mz_zip_writer_add_mem(&archive, THUMBNAIL_FILE.c_str(), (const void*)png_data, png_size, MZ_DEFAULT_COMPRESSION); + mz_free(png_data); + } + + if (!res) + add_error("Unable to add thumbnail file to archive"); + + return res; + } + + bool _3MF_Exporter::_add_relationships_file_to_archive(mz_zip_archive& archive) + { + std::stringstream stream; + stream << "\n"; + stream << "\n"; + stream << " \n"; + stream << " \n"; + stream << ""; + + std::string out = stream.str(); + + if (!mz_zip_writer_add_mem(&archive, RELATIONSHIPS_FILE.c_str(), (const void*)out.data(), out.length(), MZ_DEFAULT_COMPRESSION)) { + add_error("Unable to add relationships file to archive"); + return false; + } + + return true; + } + + static void reset_stream(std::stringstream &stream) + { + stream.str(""); + stream.clear(); + // https://en.cppreference.com/w/cpp/types/numeric_limits/max_digits10 + // Conversion of a floating-point value to text and back is exact as long as at least max_digits10 were used (9 for float, 17 for double). + // It is guaranteed to produce the same floating-point value, even though the intermediate text representation is not exact. + // The default value of std::stream precision is 6 digits only! + stream << std::setprecision(std::numeric_limits::max_digits10); + } + + bool _3MF_Exporter::_add_model_file_to_archive(const std::string& filename, mz_zip_archive& archive, const Model& model, IdToObjectDataMap& objects_data) + { + mz_zip_writer_staged_context context; + if (!mz_zip_writer_add_staged_open(&archive, &context, MODEL_FILE.c_str(), + m_zip64 ? + // Maximum expected and allowed 3MF file size is 16GiB. + // This switches the ZIP file to a 64bit mode, which adds a tiny bit of overhead to file records. + (uint64_t(1) << 30) * 16 : + // Maximum expected 3MF file size is 4GB-1. This is a workaround for interoperability with Windows 10 3D model fixing API, see + // GH issue #6193. + (uint64_t(1) << 32) - 1, + nullptr, nullptr, 0, MZ_DEFAULT_COMPRESSION, nullptr, 0, nullptr, 0)) { + add_error("Unable to add model file to archive"); + return false; + } + + { + std::stringstream stream; + reset_stream(stream); + stream << "\n"; + stream << "<" << MODEL_TAG << " unit=\"millimeter\" xml:lang=\"en-US\" xmlns=\"http://schemas.microsoft.com/3dmanufacturing/core/2015/02\" xmlns:slic3rpe=\"http://schemas.slic3r.org/3mf/2017/06\">\n"; + stream << " <" << METADATA_TAG << " name=\"" << SLIC3RPE_3MF_VERSION << "\">" << VERSION_3MF << "\n"; + + if (model.is_fdm_support_painted()) + stream << " <" << METADATA_TAG << " name=\"" << SLIC3RPE_FDM_SUPPORTS_PAINTING_VERSION << "\">" << FDM_SUPPORTS_PAINTING_VERSION << "\n"; + + if (model.is_seam_painted()) + stream << " <" << METADATA_TAG << " name=\"" << SLIC3RPE_SEAM_PAINTING_VERSION << "\">" << SEAM_PAINTING_VERSION << "\n"; + + if (model.is_mm_painted()) + stream << " <" << METADATA_TAG << " name=\"" << SLIC3RPE_MM_PAINTING_VERSION << "\">" << MM_PAINTING_VERSION << "\n"; + + std::string name = xml_escape(boost::filesystem::path(filename).stem().string()); + stream << " <" << METADATA_TAG << " name=\"Title\">" << name << "\n"; + stream << " <" << METADATA_TAG << " name=\"Designer\">" << "\n"; + stream << " <" << METADATA_TAG << " name=\"Description\">" << name << "\n"; + stream << " <" << METADATA_TAG << " name=\"Copyright\">" << "\n"; + stream << " <" << METADATA_TAG << " name=\"LicenseTerms\">" << "\n"; + stream << " <" << METADATA_TAG << " name=\"Rating\">" << "\n"; + std::string date = Slic3r::Utils::utc_timestamp(Slic3r::Utils::get_current_time_utc()); + // keep only the date part of the string + date = date.substr(0, 10); + stream << " <" << METADATA_TAG << " name=\"CreationDate\">" << date << "\n"; + stream << " <" << METADATA_TAG << " name=\"ModificationDate\">" << date << "\n"; + stream << " <" << METADATA_TAG << " name=\"Application\">" << SLIC3R_APP_KEY << "-" << SLIC3R_VERSION << "\n"; + stream << " <" << RESOURCES_TAG << ">\n"; + std::string buf = stream.str(); + if (! buf.empty() && ! mz_zip_writer_add_staged_data(&context, buf.data(), buf.size())) { + add_error("Unable to add model file to archive"); + return false; + } + } + + // Instance transformations, indexed by the 3MF object ID (which is a linear serialization of all instances of all ModelObjects). + BuildItemsList build_items; + + // The object_id here is a one based identifier of the first instance of a ModelObject in the 3MF file, where + // all the object instances of all ModelObjects are stored and indexed in a 1 based linear fashion. + // Therefore the list of object_ids here may not be continuous. + unsigned int object_id = 1; + for (ModelObject* obj : model.objects) { + if (obj == nullptr) + continue; + + // Index of an object in the 3MF file corresponding to the 1st instance of a ModelObject. + unsigned int curr_id = object_id; + IdToObjectDataMap::iterator object_it = objects_data.insert({ curr_id, ObjectData(obj) }).first; + // Store geometry of all ModelVolumes contained in a single ModelObject into a single 3MF indexed triangle set object. + // object_it->second.volumes_offsets will contain the offsets of the ModelVolumes in that single indexed triangle set. + // object_id will be increased to point to the 1st instance of the next ModelObject. + if (!_add_object_to_model_stream(context, object_id, *obj, build_items, object_it->second.volumes_offsets)) { + add_error("Unable to add object to archive"); + mz_zip_writer_add_staged_finish(&context); + return false; + } + } + + { + std::stringstream stream; + reset_stream(stream); + stream << " \n"; + + // Store the transformations of all the ModelInstances of all ModelObjects, indexed in a linear fashion. + if (!_add_build_to_model_stream(stream, build_items)) { + add_error("Unable to add build to archive"); + mz_zip_writer_add_staged_finish(&context); + return false; + } + + stream << "\n"; + + std::string buf = stream.str(); + + if ((! buf.empty() && ! mz_zip_writer_add_staged_data(&context, buf.data(), buf.size())) || + ! mz_zip_writer_add_staged_finish(&context)) { + add_error("Unable to add model file to archive"); + return false; + } + } + + return true; + } + + bool _3MF_Exporter::_add_object_to_model_stream(mz_zip_writer_staged_context &context, unsigned int& object_id, ModelObject& object, BuildItemsList& build_items, VolumeToOffsetsMap& volumes_offsets) + { + std::stringstream stream; + reset_stream(stream); + unsigned int id = 0; + for (const ModelInstance* instance : object.instances) { + assert(instance != nullptr); + if (instance == nullptr) + continue; + + unsigned int instance_id = object_id + id; + stream << " <" << OBJECT_TAG << " id=\"" << instance_id << "\" type=\"model\">\n"; + + if (id == 0) { + std::string buf = stream.str(); + reset_stream(stream); + if ((! buf.empty() && ! mz_zip_writer_add_staged_data(&context, buf.data(), buf.size())) || + ! _add_mesh_to_object_stream(context, object, volumes_offsets)) { + add_error("Unable to add mesh to archive"); + return false; + } + } + else { + stream << " <" << COMPONENTS_TAG << ">\n"; + stream << " <" << COMPONENT_TAG << " objectid=\"" << object_id << "\"/>\n"; + stream << " \n"; + } + + Transform3d t = instance->get_matrix(); + // instance_id is just a 1 indexed index in build_items. + assert(instance_id == build_items.size() + 1); + build_items.emplace_back(instance_id, t, instance->printable); + + stream << " \n"; + + ++id; + } + + object_id += id; + std::string buf = stream.str(); + return buf.empty() || mz_zip_writer_add_staged_data(&context, buf.data(), buf.size()); + } + +#if EXPORT_3MF_USE_SPIRIT_KARMA_FP + template + struct coordinate_policy_fixed : boost::spirit::karma::real_policies + { + static int floatfield(Num n) { return fmtflags::fixed; } + // Number of decimal digits to maintain float accuracy when storing into a text file and parsing back. + static unsigned precision(Num /* n */) { return std::numeric_limits::max_digits10 + 1; } + // No trailing zeros, thus for fmtflags::fixed usually much less than max_digits10 decimal numbers will be produced. + static bool trailing_zeros(Num /* n */) { return false; } + }; + template + struct coordinate_policy_scientific : coordinate_policy_fixed + { + static int floatfield(Num n) { return fmtflags::scientific; } + }; + // Define a new generator type based on the new coordinate policy. + using coordinate_type_fixed = boost::spirit::karma::real_generator>; + using coordinate_type_scientific = boost::spirit::karma::real_generator>; +#endif // EXPORT_3MF_USE_SPIRIT_KARMA_FP + + bool _3MF_Exporter::_add_mesh_to_object_stream(mz_zip_writer_staged_context &context, ModelObject& object, VolumeToOffsetsMap& volumes_offsets) + { + std::string output_buffer; + output_buffer += " <"; + output_buffer += MESH_TAG; + output_buffer += ">\n <"; + output_buffer += VERTICES_TAG; + output_buffer += ">\n"; + + auto flush = [this, &output_buffer, &context](bool force = false) { + if ((force && ! output_buffer.empty()) || output_buffer.size() >= 65536 * 16) { + if (! mz_zip_writer_add_staged_data(&context, output_buffer.data(), output_buffer.size())) { + add_error("Error during writing or compression"); + return false; + } + output_buffer.clear(); + } + return true; + }; + + auto format_coordinate = [](float f, char *buf) -> char* { + assert(is_decimal_separator_point()); +#if EXPORT_3MF_USE_SPIRIT_KARMA_FP + // Slightly faster than sprintf("%.9g"), but there is an issue with the karma floating point formatter, + // https://github.com/boostorg/spirit/pull/586 + // where the exported string is one digit shorter than it should be to guarantee lossless round trip. + // The code is left here for the ocasion boost guys improve. + coordinate_type_fixed const coordinate_fixed = coordinate_type_fixed(); + coordinate_type_scientific const coordinate_scientific = coordinate_type_scientific(); + // Format "f" in a fixed format. + char *ptr = buf; + boost::spirit::karma::generate(ptr, coordinate_fixed, f); + // Format "f" in a scientific format. + char *ptr2 = ptr; + boost::spirit::karma::generate(ptr2, coordinate_scientific, f); + // Return end of the shorter string. + auto len2 = ptr2 - ptr; + if (ptr - buf > len2) { + // Move the shorter scientific form to the front. + memcpy(buf, ptr, len2); + ptr = buf + len2; + } + // Return pointer to the end. + return ptr; +#else + // Round-trippable float, shortest possible. + return buf + sprintf(buf, "%.9g", f); +#endif + }; + + char buf[256]; + unsigned int vertices_count = 0; + for (ModelVolume* volume : object.volumes) { + if (volume == nullptr) + continue; + + volumes_offsets.insert({ volume, Offsets(vertices_count) }); + + const indexed_triangle_set &its = volume->mesh().its; + if (its.vertices.empty()) { + add_error("Found invalid mesh"); + return false; + } + + vertices_count += (int)its.vertices.size(); + + const Transform3d& matrix = volume->get_matrix(); + + for (size_t i = 0; i < its.vertices.size(); ++i) { + Vec3f v = (matrix * its.vertices[i].cast()).cast(); + char *ptr = buf; + boost::spirit::karma::generate(ptr, boost::spirit::lit(" <") << VERTEX_TAG << " x=\""); + ptr = format_coordinate(v.x(), ptr); + boost::spirit::karma::generate(ptr, "\" y=\""); + ptr = format_coordinate(v.y(), ptr); + boost::spirit::karma::generate(ptr, "\" z=\""); + ptr = format_coordinate(v.z(), ptr); + boost::spirit::karma::generate(ptr, "\"/>\n"); + *ptr = '\0'; + output_buffer += buf; + if (! flush()) + return false; + } + } + + output_buffer += " \n <"; + output_buffer += TRIANGLES_TAG; + output_buffer += ">\n"; + + unsigned int triangles_count = 0; + for (ModelVolume* volume : object.volumes) { + if (volume == nullptr) + continue; + + bool is_left_handed = volume->is_left_handed(); + VolumeToOffsetsMap::iterator volume_it = volumes_offsets.find(volume); + assert(volume_it != volumes_offsets.end()); + + const indexed_triangle_set &its = volume->mesh().its; + + // updates triangle offsets + volume_it->second.first_triangle_id = triangles_count; + triangles_count += (int)its.indices.size(); + volume_it->second.last_triangle_id = triangles_count - 1; + + for (int i = 0; i < int(its.indices.size()); ++ i) { + { + const Vec3i &idx = its.indices[i]; + char *ptr = buf; + boost::spirit::karma::generate(ptr, boost::spirit::lit(" <") << TRIANGLE_TAG << + " v1=\"" << boost::spirit::int_ << + "\" v2=\"" << boost::spirit::int_ << + "\" v3=\"" << boost::spirit::int_ << "\"", + idx[is_left_handed ? 2 : 0] + volume_it->second.first_vertex_id, + idx[1] + volume_it->second.first_vertex_id, + idx[is_left_handed ? 0 : 2] + volume_it->second.first_vertex_id); + *ptr = '\0'; + output_buffer += buf; + } + + std::string custom_supports_data_string = volume->supported_facets.get_triangle_as_string(i); + if (! custom_supports_data_string.empty()) { + output_buffer += " "; + output_buffer += CUSTOM_SUPPORTS_ATTR; + output_buffer += "=\""; + output_buffer += custom_supports_data_string; + output_buffer += "\""; + } + + std::string custom_seam_data_string = volume->seam_facets.get_triangle_as_string(i); + if (! custom_seam_data_string.empty()) { + output_buffer += " "; + output_buffer += CUSTOM_SEAM_ATTR; + output_buffer += "=\""; + output_buffer += custom_seam_data_string; + output_buffer += "\""; + } + + std::string mmu_painting_data_string = volume->mmu_segmentation_facets.get_triangle_as_string(i); + if (! mmu_painting_data_string.empty()) { + output_buffer += " "; + output_buffer += MMU_SEGMENTATION_ATTR; + output_buffer += "=\""; + output_buffer += mmu_painting_data_string; + output_buffer += "\""; + } + + output_buffer += "/>\n"; + + if (! flush()) + return false; + } + } + + output_buffer += " \n \n"; + + // Force flush. + return flush(true); + } + + bool _3MF_Exporter::_add_build_to_model_stream(std::stringstream& stream, const BuildItemsList& build_items) + { + // This happens for empty projects + if (build_items.size() == 0) { + add_error("No build item found"); + return true; + } + + stream << " <" << BUILD_TAG << ">\n"; + + for (const BuildItem& item : build_items) { + stream << " <" << ITEM_TAG << " " << OBJECTID_ATTR << "=\"" << item.id << "\" " << TRANSFORM_ATTR << "=\""; + for (unsigned c = 0; c < 4; ++c) { + for (unsigned r = 0; r < 3; ++r) { + stream << item.transform(r, c); + if (r != 2 || c != 3) + stream << " "; + } + } + stream << "\" " << PRINTABLE_ATTR << "=\"" << item.printable << "\"/>\n"; + } + + stream << " \n"; + + return true; + } + + bool _3MF_Exporter::_add_layer_height_profile_file_to_archive(mz_zip_archive& archive, Model& model) + { + assert(is_decimal_separator_point()); + std::string out = ""; + char buffer[1024]; + + unsigned int count = 0; + for (const ModelObject* object : model.objects) { + ++count; + const std::vector& layer_height_profile = object->layer_height_profile.get(); + if (layer_height_profile.size() >= 4 && layer_height_profile.size() % 2 == 0) { + sprintf(buffer, "object_id=%d|", count); + out += buffer; + + // Store the layer height profile as a single semicolon separated list. + for (size_t i = 0; i < layer_height_profile.size(); ++i) { + sprintf(buffer, (i == 0) ? "%f" : ";%f", layer_height_profile[i]); + out += buffer; + } + + out += "\n"; + } + } + + if (!out.empty()) { + if (!mz_zip_writer_add_mem(&archive, LAYER_HEIGHTS_PROFILE_FILE.c_str(), (const void*)out.data(), out.length(), MZ_DEFAULT_COMPRESSION)) { + add_error("Unable to add layer heights profile file to archive"); + return false; + } + } + + return true; + } + + bool _3MF_Exporter::_add_layer_config_ranges_file_to_archive(mz_zip_archive& archive, Model& model) + { + std::string out = ""; + pt::ptree tree; + + unsigned int object_cnt = 0; + for (const ModelObject* object : model.objects) { + object_cnt++; + const t_layer_config_ranges& ranges = object->layer_config_ranges; + if (!ranges.empty()) + { + pt::ptree& obj_tree = tree.add("objects.object",""); + + obj_tree.put(".id", object_cnt); + + // Store the layer config ranges. + for (const auto& range : ranges) { + pt::ptree& range_tree = obj_tree.add("range", ""); + + // store minX and maxZ + range_tree.put(".min_z", range.first.first); + range_tree.put(".max_z", range.first.second); + + // store range configuration + const ModelConfig& config = range.second; + for (const std::string& opt_key : config.keys()) { + pt::ptree& opt_tree = range_tree.add("option", config.opt_serialize(opt_key)); + opt_tree.put(".opt_key", opt_key); + } + } + } + } + + if (!tree.empty()) { + std::ostringstream oss; + pt::write_xml(oss, tree); + out = oss.str(); + + // Post processing("beautification") of the output string for a better preview + boost::replace_all(out, ">\n \n \n ", ">\n "); + boost::replace_all(out, ">", ">\n "); + // OR just + boost::replace_all(out, "><", ">\n<"); + } + + if (!out.empty()) { + if (!mz_zip_writer_add_mem(&archive, LAYER_CONFIG_RANGES_FILE.c_str(), (const void*)out.data(), out.length(), MZ_DEFAULT_COMPRESSION)) { + add_error("Unable to add layer heights profile file to archive"); + return false; + } + } + + return true; + } + + bool _3MF_Exporter::_add_sla_support_points_file_to_archive(mz_zip_archive& archive, Model& model) + { + assert(is_decimal_separator_point()); + std::string out = ""; + char buffer[1024]; + + unsigned int count = 0; + for (const ModelObject* object : model.objects) { + ++count; + const std::vector& sla_support_points = object->sla_support_points; + if (!sla_support_points.empty()) { + sprintf(buffer, "object_id=%d|", count); + out += buffer; + + // Store the layer height profile as a single space separated list. + for (size_t i = 0; i < sla_support_points.size(); ++i) { + sprintf(buffer, (i==0 ? "%f %f %f %f %f" : " %f %f %f %f %f"), sla_support_points[i].pos(0), sla_support_points[i].pos(1), sla_support_points[i].pos(2), sla_support_points[i].head_front_radius, (float)sla_support_points[i].is_new_island); + out += buffer; + } + out += "\n"; + } + } + + if (!out.empty()) { + // Adds version header at the beginning: + out = std::string("support_points_format_version=") + std::to_string(support_points_format_version) + std::string("\n") + out; + + if (!mz_zip_writer_add_mem(&archive, SLA_SUPPORT_POINTS_FILE.c_str(), (const void*)out.data(), out.length(), MZ_DEFAULT_COMPRESSION)) { + add_error("Unable to add sla support points file to archive"); + return false; + } + } + return true; + } + + bool _3MF_Exporter::_add_sla_drain_holes_file_to_archive(mz_zip_archive& archive, Model& model) + { + assert(is_decimal_separator_point()); + const char *const fmt = "object_id=%d|"; + std::string out; + + unsigned int count = 0; + for (const ModelObject* object : model.objects) { + ++count; + sla::DrainHoles drain_holes = object->sla_drain_holes; + + // The holes were placed 1mm above the mesh in the first implementation. + // This was a bad idea and the reference point was changed in 2.3 so + // to be on the mesh exactly. The elevated position is still saved + // in 3MFs for compatibility reasons. + for (sla::DrainHole& hole : drain_holes) { + hole.pos -= hole.normal.normalized(); + hole.height += 1.f; + } + + 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) + { + assert(is_decimal_separator_point()); + char buffer[1024]; + sprintf(buffer, "; %s\n\n", header_slic3r_generated().c_str()); + std::string out = buffer; + + for (const std::string &key : config.keys()) + if (key != "compatible_printers") + out += "; " + key + " = " + config.opt_serialize(key) + "\n"; + + if (!out.empty()) { + if (!mz_zip_writer_add_mem(&archive, PRINT_CONFIG_FILE.c_str(), (const void*)out.data(), out.length(), MZ_DEFAULT_COMPRESSION)) { + add_error("Unable to add print config file to archive"); + return false; + } + } + + return true; + } + + bool _3MF_Exporter::_add_model_config_file_to_archive(mz_zip_archive& archive, const Model& model, const IdToObjectDataMap &objects_data) + { + std::stringstream stream; + // Store mesh transformation in full precision, as the volumes are stored transformed and they need to be transformed back + // when loaded as accurately as possible. + stream << std::setprecision(std::numeric_limits::max_digits10); + stream << "\n"; + stream << "<" << CONFIG_TAG << ">\n"; + + for (const IdToObjectDataMap::value_type& obj_metadata : objects_data) { + const ModelObject* obj = obj_metadata.second.object; + if (obj != nullptr) { + // Output of instances count added because of github #3435, currently not used by PrusaSlicer + stream << " <" << OBJECT_TAG << " " << ID_ATTR << "=\"" << obj_metadata.first << "\" " << INSTANCESCOUNT_ATTR << "=\"" << obj->instances.size() << "\">\n"; + + // stores object's name + if (!obj->name.empty()) + stream << " <" << METADATA_TAG << " " << TYPE_ATTR << "=\"" << OBJECT_TYPE << "\" " << KEY_ATTR << "=\"name\" " << VALUE_ATTR << "=\"" << xml_escape(obj->name) << "\"/>\n"; + + // stores object's config data + for (const std::string& key : obj->config.keys()) { + stream << " <" << METADATA_TAG << " " << TYPE_ATTR << "=\"" << OBJECT_TYPE << "\" " << KEY_ATTR << "=\"" << key << "\" " << VALUE_ATTR << "=\"" << obj->config.opt_serialize(key) << "\"/>\n"; + } + + for (const ModelVolume* volume : obj_metadata.second.object->volumes) { + if (volume != nullptr) { + const VolumeToOffsetsMap& offsets = obj_metadata.second.volumes_offsets; + VolumeToOffsetsMap::const_iterator it = offsets.find(volume); + if (it != offsets.end()) { + // stores volume's offsets + stream << " <" << VOLUME_TAG << " "; + stream << FIRST_TRIANGLE_ID_ATTR << "=\"" << it->second.first_triangle_id << "\" "; + stream << LAST_TRIANGLE_ID_ATTR << "=\"" << it->second.last_triangle_id << "\">\n"; + + // stores volume's name + if (!volume->name.empty()) + stream << " <" << METADATA_TAG << " " << TYPE_ATTR << "=\"" << VOLUME_TYPE << "\" " << KEY_ATTR << "=\"" << NAME_KEY << "\" " << VALUE_ATTR << "=\"" << xml_escape(volume->name) << "\"/>\n"; + + // stores volume's modifier field (legacy, to support old slicers) + if (volume->is_modifier()) + stream << " <" << METADATA_TAG << " " << TYPE_ATTR << "=\"" << VOLUME_TYPE << "\" " << KEY_ATTR << "=\"" << MODIFIER_KEY << "\" " << VALUE_ATTR << "=\"1\"/>\n"; + // stores volume's type (overrides the modifier field above) + stream << " <" << METADATA_TAG << " " << TYPE_ATTR << "=\"" << VOLUME_TYPE << "\" " << KEY_ATTR << "=\"" << VOLUME_TYPE_KEY << "\" " << + VALUE_ATTR << "=\"" << ModelVolume::type_to_string(volume->type()) << "\"/>\n"; + + // stores volume's local matrix + stream << " <" << METADATA_TAG << " " << TYPE_ATTR << "=\"" << VOLUME_TYPE << "\" " << KEY_ATTR << "=\"" << MATRIX_KEY << "\" " << VALUE_ATTR << "=\""; + const Transform3d matrix = volume->get_matrix() * volume->source.transform.get_matrix(); + for (int r = 0; r < 4; ++r) { + for (int c = 0; c < 4; ++c) { + stream << matrix(r, c); + if (r != 3 || c != 3) + stream << " "; + } + } + stream << "\"/>\n"; + + // stores volume's source data + { + std::string input_file = xml_escape(m_fullpath_sources ? volume->source.input_file : boost::filesystem::path(volume->source.input_file).filename().string()); + std::string prefix = std::string(" <") + METADATA_TAG + " " + TYPE_ATTR + "=\"" + VOLUME_TYPE + "\" " + KEY_ATTR + "=\""; + if (! volume->source.input_file.empty()) { + stream << prefix << SOURCE_FILE_KEY << "\" " << VALUE_ATTR << "=\"" << input_file << "\"/>\n"; + stream << prefix << SOURCE_OBJECT_ID_KEY << "\" " << VALUE_ATTR << "=\"" << volume->source.object_idx << "\"/>\n"; + stream << prefix << SOURCE_VOLUME_ID_KEY << "\" " << VALUE_ATTR << "=\"" << volume->source.volume_idx << "\"/>\n"; + stream << prefix << SOURCE_OFFSET_X_KEY << "\" " << VALUE_ATTR << "=\"" << volume->source.mesh_offset(0) << "\"/>\n"; + stream << prefix << SOURCE_OFFSET_Y_KEY << "\" " << VALUE_ATTR << "=\"" << volume->source.mesh_offset(1) << "\"/>\n"; + stream << prefix << SOURCE_OFFSET_Z_KEY << "\" " << VALUE_ATTR << "=\"" << volume->source.mesh_offset(2) << "\"/>\n"; + } + assert(! volume->source.is_converted_from_inches || ! volume->source.is_converted_from_meters); + if (volume->source.is_converted_from_inches) + stream << prefix << SOURCE_IN_INCHES_KEY << "\" " << VALUE_ATTR << "=\"1\"/>\n"; + else if (volume->source.is_converted_from_meters) + stream << prefix << SOURCE_IN_METERS_KEY << "\" " << VALUE_ATTR << "=\"1\"/>\n"; +#if ENABLE_RELOAD_FROM_DISK_REWORK + if (volume->source.is_from_builtin_objects) + stream << prefix << SOURCE_IS_BUILTIN_VOLUME_KEY << "\" " << VALUE_ATTR << "=\"1\"/>\n"; +#endif // ENABLE_RELOAD_FROM_DISK_REWORK + } + + // stores volume's config data + for (const std::string& key : volume->config.keys()) { + stream << " <" << METADATA_TAG << " " << TYPE_ATTR << "=\"" << VOLUME_TYPE << "\" " << KEY_ATTR << "=\"" << key << "\" " << VALUE_ATTR << "=\"" << volume->config.opt_serialize(key) << "\"/>\n"; + } + + // stores mesh's statistics + const RepairedMeshErrors& stats = volume->mesh().stats().repaired_errors; + stream << " <" << MESH_TAG << " "; + stream << MESH_STAT_EDGES_FIXED << "=\"" << stats.edges_fixed << "\" "; + stream << MESH_STAT_DEGENERATED_FACETS << "=\"" << stats.degenerate_facets << "\" "; + stream << MESH_STAT_FACETS_REMOVED << "=\"" << stats.facets_removed << "\" "; + stream << MESH_STAT_FACETS_RESERVED << "=\"" << stats.facets_reversed << "\" "; + stream << MESH_STAT_BACKWARDS_EDGES << "=\"" << stats.backwards_edges << "\"/>\n"; + + stream << " \n"; + } + } + } + + stream << " \n"; + } + } + + stream << "\n"; + + std::string out = stream.str(); + + if (!mz_zip_writer_add_mem(&archive, MODEL_CONFIG_FILE.c_str(), (const void*)out.data(), out.length(), MZ_DEFAULT_COMPRESSION)) { + add_error("Unable to add model config file to archive"); + return false; + } + + return true; + } + +bool _3MF_Exporter::_add_custom_gcode_per_print_z_file_to_archive( mz_zip_archive& archive, Model& model, const DynamicPrintConfig* config) +{ + std::string out = ""; + + if (!model.custom_gcode_per_print_z.gcodes.empty()) { + pt::ptree tree; + pt::ptree& main_tree = tree.add("custom_gcodes_per_print_z", ""); + + for (const CustomGCode::Item& code : model.custom_gcode_per_print_z.gcodes) { + pt::ptree& code_tree = main_tree.add("code", ""); + + // store data of custom_gcode_per_print_z + code_tree.put(".print_z" , code.print_z ); + code_tree.put(".type" , static_cast(code.type)); + code_tree.put(".extruder" , code.extruder ); + code_tree.put(".color" , code.color ); + code_tree.put(".extra" , code.extra ); + + // add gcode field data for the old version of the PrusaSlicer + std::string gcode = code.type == CustomGCode::ColorChange ? config->opt_string("color_change_gcode") : + code.type == CustomGCode::PausePrint ? config->opt_string("pause_print_gcode") : + code.type == CustomGCode::Template ? config->opt_string("template_custom_gcode") : + code.type == CustomGCode::ToolChange ? "tool_change" : code.extra; + code_tree.put(".gcode" , gcode ); + } + + pt::ptree& mode_tree = main_tree.add("mode", ""); + // store mode of a custom_gcode_per_print_z + mode_tree.put(".value", model.custom_gcode_per_print_z.mode == CustomGCode::Mode::SingleExtruder ? CustomGCode::SingleExtruderMode : + model.custom_gcode_per_print_z.mode == CustomGCode::Mode::MultiAsSingle ? CustomGCode::MultiAsSingleMode : + CustomGCode::MultiExtruderMode); + + if (!tree.empty()) { + std::ostringstream oss; + boost::property_tree::write_xml(oss, tree); + out = oss.str(); + + // Post processing("beautification") of the output string + boost::replace_all(out, "><", ">\n<"); + } + } + + if (!out.empty()) { + if (!mz_zip_writer_add_mem(&archive, CUSTOM_GCODE_PER_PRINT_Z_FILE.c_str(), (const void*)out.data(), out.length(), MZ_DEFAULT_COMPRESSION)) { + add_error("Unable to add custom Gcodes per print_z file to archive"); + return false; + } + } + + return true; +} + +// Perform conversions based on the config values available. +//FIXME provide a version of PrusaSlicer that stored the project file (3MF). +static void handle_legacy_project_loaded(unsigned int version_project_file, DynamicPrintConfig& config) +{ + if (! config.has("brim_separation")) { + if (auto *opt_elephant_foot = config.option("elefant_foot_compensation", false); opt_elephant_foot) { + // Conversion from older PrusaSlicer which applied brim separation equal to elephant foot compensation. + auto *opt_brim_separation = config.option("brim_separation", true); + opt_brim_separation->value = opt_elephant_foot->value; + } + } +} + +bool is_project_3mf(const std::string& filename) +{ + mz_zip_archive archive; + mz_zip_zero_struct(&archive); + + if (!open_zip_reader(&archive, filename)) + return false; + + mz_uint num_entries = mz_zip_reader_get_num_files(&archive); + + // loop the entries to search for config + mz_zip_archive_file_stat stat; + bool config_found = false; + for (mz_uint i = 0; i < num_entries; ++i) { + if (mz_zip_reader_file_stat(&archive, i, &stat)) { + std::string name(stat.m_filename); + std::replace(name.begin(), name.end(), '\\', '/'); + + if (boost::algorithm::iequals(name, PRINT_CONFIG_FILE)) { + config_found = true; + break; + } + } + } + + close_zip_reader(&archive); + + return config_found; +} + +bool load_3mf(const char* path, DynamicPrintConfig& config, ConfigSubstitutionContext& config_substitutions, Model* model, bool check_version) +{ + if (path == nullptr || model == nullptr) + return false; + + // All import should use "C" locales for number formatting. + CNumericLocalesSetter locales_setter; + _3MF_Importer importer; + bool res = importer.load_model_from_file(path, *model, config, config_substitutions, check_version); + importer.log_errors(); + handle_legacy_project_loaded(importer.version(), config); + + return !model->objects.empty() || !config.empty(); +} + +bool store_3mf(const char* path, Model* model, const DynamicPrintConfig* config, bool fullpath_sources, const ThumbnailData* thumbnail_data, bool zip64) +{ + // All export should use "C" locales for number formatting. + CNumericLocalesSetter locales_setter; + + if (path == nullptr || model == nullptr) + return false; + + _3MF_Exporter exporter; + bool res = exporter.save_model_to_file(path, *model, config, fullpath_sources, thumbnail_data, zip64); + if (!res) + exporter.log_errors(); + + return res; +} +} // namespace Slic3r diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index aef83f21f..58951ffa5 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -35,6 +35,8 @@ #include "SVG.hpp" #include +#include +#include // Intel redesigned some TBB interface considerably when merging TBB with their oneAPI set of libraries, see GH #7332. // We are using quite an old TBB 2017 U7. Before we update our build servers, let's use the old API, which is deprecated in up to date TBB. @@ -1488,6 +1490,32 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato print.throw_if_canceled(); } +// For unknown reasons and in sporadic cases when GCode export is processing, some participating thread +// in tbb::parallel_pipeline has not set locales to "C", probably because this thread is newly spawned. +// So in this class method on_scheduler_entry is called for every thread before it starts participating +// in tbb::parallel_pipeline to ensure that locales are set correctly + +// For tbb::parallel_pipeline, it seems that on_scheduler_entry is called for every layer and every filter. +// We ensure using thread-local storage that locales will be set to "C" just once for any participating thread. +class TBBLocalesSetter : public tbb::task_scheduler_observer +{ +public: + TBBLocalesSetter() { this->observe(true); } + ~TBBLocalesSetter() override { this->observe(false); }; + + void on_scheduler_entry(bool is_worker) override + { + if (bool &is_locales_sets = m_is_locales_sets.local(); !is_locales_sets) { + // Set locales of the worker thread to "C". + set_c_locales(); + is_locales_sets = true; + } + } + +private: + tbb::enumerable_thread_specific, tbb::ets_key_usage_type::ets_key_per_instance> m_is_locales_sets{false}; +}; + // Process all layers of all objects (non-sequential mode) with a parallel pipeline: // Generate G-code, run the filters (vase mode, cooling buffer), run the G-code analyser // and export G-code into file. @@ -1531,6 +1559,10 @@ void GCode::process_layers( [&output_stream](std::string s) { output_stream.write(s); } ); + // It registers a handler that sets locales to "C" before any TBB thread starts participating in tbb::parallel_pipeline. + // Handler is unregistered when the destructor is called. + TBBLocalesSetter locales_setter; + // The pipeline elements are joined using const references, thus no copying is performed. output_stream.find_replace_supress(); if (m_spiral_vase && m_find_replace) @@ -1584,6 +1616,10 @@ void GCode::process_layers( [&output_stream](std::string s) { output_stream.write(s); } ); + // It registers a handler that sets locales to "C" before any TBB thread starts participating in tbb::parallel_pipeline. + // Handler is unregistered when the destructor is called. + TBBLocalesSetter locales_setter; + // The pipeline elements are joined using const references, thus no copying is performed. output_stream.find_replace_supress(); if (m_spiral_vase && m_find_replace) diff --git a/src/libslic3r/Geometry.cpp b/src/libslic3r/Geometry.cpp index 6eae83277..115d21693 100644 --- a/src/libslic3r/Geometry.cpp +++ b/src/libslic3r/Geometry.cpp @@ -313,9 +313,60 @@ Transform3d assemble_transform(const Vec3d& translation, const Vec3d& rotation, return transform; } +void assemble_transform(Transform3d& transform, const Transform3d& translation, const Transform3d& rotation, const Transform3d& scale, const Transform3d& mirror) +{ + transform = translation * rotation * scale * mirror; +} + +Transform3d assemble_transform(const Transform3d& translation, const Transform3d& rotation, const Transform3d& scale, const Transform3d& mirror) +{ + Transform3d transform; + assemble_transform(transform, translation, rotation, scale, mirror); + return transform; +} + +void translation_transform(Transform3d& transform, const Vec3d& translation) +{ + transform = Transform3d::Identity(); + transform.translate(translation); +} + +Transform3d translation_transform(const Vec3d& translation) +{ + Transform3d transform; + translation_transform(transform, translation); + return transform; +} + +void rotation_transform(Transform3d& transform, const Vec3d& rotation) +{ + transform = Transform3d::Identity(); + transform.rotate(Eigen::AngleAxisd(rotation.z(), Vec3d::UnitZ()) * Eigen::AngleAxisd(rotation.y(), Vec3d::UnitY()) * Eigen::AngleAxisd(rotation.x(), Vec3d::UnitX())); +} + +Transform3d rotation_transform(const Vec3d& rotation) +{ + Transform3d transform; + rotation_transform(transform, rotation); + return transform; +} + +void scale_transform(Transform3d& transform, const Vec3d& scale) +{ + transform = Transform3d::Identity(); + transform.scale(scale); +} + +Transform3d scale_transform(const Vec3d& scale) +{ + Transform3d transform; + scale_transform(transform, scale); + return transform; +} + Vec3d extract_euler_angles(const Eigen::Matrix& rotation_matrix) { - // reference: http://www.gregslabaugh.net/publications/euler.pdf + // reference: http://eecs.qmul.ac.uk/~gslabaugh/publications/euler.pdf Vec3d angles1 = Vec3d::Zero(); Vec3d angles2 = Vec3d::Zero(); if (std::abs(std::abs(rotation_matrix(2, 0)) - 1.0) < 1e-5) { @@ -363,6 +414,54 @@ Vec3d extract_euler_angles(const Transform3d& transform) return extract_euler_angles(m); } +#if ENABLE_WORLD_COORDINATE +Transform3d Transformation::get_offset_matrix() const +{ + return assemble_transform(get_offset()); +} + +static Transform3d extract_rotation(const Transform3d& trafo) +{ + Matrix3d rotation; + Matrix3d scale; + trafo.computeRotationScaling(&rotation, &scale); + return Transform3d(rotation); +} + +static Transform3d extract_scale(const Transform3d& trafo) +{ + Matrix3d rotation; + Matrix3d scale; + trafo.computeRotationScaling(&rotation, &scale); + return Transform3d(scale); +} + +static std::pair extract_rotation_scale(const Transform3d& trafo) +{ + Matrix3d rotation; + Matrix3d scale; + trafo.computeRotationScaling(&rotation, &scale); + return { Transform3d(rotation), Transform3d(scale) }; +} + +static bool contains_skew(const Transform3d& trafo) +{ + Matrix3d rotation; + Matrix3d scale; + trafo.computeRotationScaling(&rotation, &scale); + return !scale.isDiagonal(); +} + +Vec3d Transformation::get_rotation() const +{ + return extract_euler_angles(extract_rotation(m_matrix)); +} + +Transform3d Transformation::get_rotation_matrix() const +{ + return extract_rotation(m_matrix); +} +#else bool Transformation::Flags::needs_update(bool dont_translate, bool dont_rotate, bool dont_scale, bool dont_mirror) const { return (this->dont_translate != dont_translate) || (this->dont_rotate != dont_rotate) || (this->dont_scale != dont_scale) || (this->dont_mirror != dont_mirror); @@ -400,12 +499,19 @@ void Transformation::set_offset(Axis axis, double offset) m_dirty = true; } } +#endif // ENABLE_WORLD_COORDINATE void Transformation::set_rotation(const Vec3d& rotation) { +#if ENABLE_WORLD_COORDINATE + const Vec3d offset = get_offset(); + m_matrix = rotation_transform(rotation) * extract_scale(m_matrix); + m_matrix.translation() = offset; +#else set_rotation(X, rotation.x()); set_rotation(Y, rotation.y()); set_rotation(Z, rotation.z()); +#endif // ENABLE_WORLD_COORDINATE } void Transformation::set_rotation(Axis axis, double rotation) @@ -414,32 +520,106 @@ void Transformation::set_rotation(Axis axis, double rotation) if (is_approx(std::abs(rotation), 2.0 * double(PI))) rotation = 0.0; +#if ENABLE_WORLD_COORDINATE + auto [curr_rotation, scale] = extract_rotation_scale(m_matrix); + Vec3d angles = extract_euler_angles(curr_rotation); + angles[axis] = rotation; + + const Vec3d offset = get_offset(); + m_matrix = rotation_transform(angles) * scale; + m_matrix.translation() = offset; +#else if (m_rotation(axis) != rotation) { m_rotation(axis) = rotation; m_dirty = true; } +#endif // ENABLE_WORLD_COORDINATE } +#if ENABLE_WORLD_COORDINATE +Vec3d Transformation::get_scaling_factor() const +{ + const Transform3d scale = extract_scale(m_matrix); + return { scale(0, 0), scale(1, 1), scale(2, 2) }; +} + +Transform3d Transformation::get_scaling_factor_matrix() const +{ + return extract_scale(m_matrix); +} +#endif // ENABLE_WORLD_COORDINATE + void Transformation::set_scaling_factor(const Vec3d& scaling_factor) { +#if ENABLE_WORLD_COORDINATE + assert(scaling_factor.x() > 0.0 && scaling_factor.y() > 0.0 && scaling_factor.z() > 0.0); + + const Vec3d offset = get_offset(); + m_matrix = extract_rotation(m_matrix) * scale_transform(scaling_factor); + m_matrix.translation() = offset; +#else set_scaling_factor(X, scaling_factor.x()); set_scaling_factor(Y, scaling_factor.y()); set_scaling_factor(Z, scaling_factor.z()); +#endif // ENABLE_WORLD_COORDINATE } void Transformation::set_scaling_factor(Axis axis, double scaling_factor) { +#if ENABLE_WORLD_COORDINATE + assert(scaling_factor > 0.0); + auto [rotation, scale] = extract_rotation_scale(m_matrix); + scale(axis, axis) = scaling_factor; + + const Vec3d offset = get_offset(); + m_matrix = rotation * scale; + m_matrix.translation() = offset; +#else if (m_scaling_factor(axis) != std::abs(scaling_factor)) { m_scaling_factor(axis) = std::abs(scaling_factor); m_dirty = true; } +#endif // ENABLE_WORLD_COORDINATE } +#if ENABLE_WORLD_COORDINATE +Vec3d Transformation::get_mirror() const +{ + const Transform3d scale = extract_scale(m_matrix); + return { scale(0, 0) / std::abs(scale(0, 0)), scale(1, 1) / std::abs(scale(1, 1)), scale(2, 2) / std::abs(scale(2, 2)) }; +} + +Transform3d Transformation::get_mirror_matrix() const +{ + const Vec3d scale = get_scaling_factor(); + return scale_transform({ scale.x() / std::abs(scale.x()), scale.y() / std::abs(scale.y()), scale.z() / std::abs(scale.z()) }); +} +#endif // ENABLE_WORLD_COORDINATE + void Transformation::set_mirror(const Vec3d& mirror) { +#if ENABLE_WORLD_COORDINATE + Vec3d copy(mirror); + const Vec3d abs_mirror = copy.cwiseAbs(); + for (int i = 0; i < 3; ++i) { + if (abs_mirror(i) == 0.0) + copy(i) = 1.0; + else if (abs_mirror(i) != 1.0) + copy(i) /= abs_mirror(i); + } + + const Vec3d curr_scale = get_scaling_factor(); + const Vec3d signs = curr_scale.cwiseProduct(copy); + set_scaling_factor({ + signs.x() < 0.0 ? std::abs(curr_scale.x()) * copy.x() : curr_scale.x(), + signs.y() < 0.0 ? std::abs(curr_scale.y()) * copy.y() : curr_scale.y(), + signs.z() < 0.0 ? std::abs(curr_scale.z()) * copy.z() : curr_scale.z() + }); +#else set_mirror(X, mirror.x()); set_mirror(Y, mirror.y()); set_mirror(Z, mirror.z()); +#endif // ENABLE_WORLD_COORDINATE } void Transformation::set_mirror(Axis axis, double mirror) @@ -450,12 +630,24 @@ void Transformation::set_mirror(Axis axis, double mirror) else if (abs_mirror != 1.0) mirror /= abs_mirror; +#if ENABLE_WORLD_COORDINATE + const double curr_scale = get_scaling_factor(axis); + const double sign = curr_scale * mirror; + set_scaling_factor(axis, sign < 0.0 ? std::abs(curr_scale) * mirror : curr_scale); +#else if (m_mirror(axis) != mirror) { m_mirror(axis) = mirror; m_dirty = true; } +#endif // ENABLE_WORLD_COORDINATE } +#if ENABLE_WORLD_COORDINATE +bool Transformation::has_skew() const +{ + return contains_skew(m_matrix); +} +#else void Transformation::set_from_transform(const Transform3d& transform) { // offset @@ -493,17 +685,62 @@ void Transformation::set_from_transform(const Transform3d& transform) // if (!m_matrix.isApprox(transform)) // std::cout << "something went wrong in extracting data from matrix" << std::endl; } +#endif // ENABLE_WORLD_COORDINATE void Transformation::reset() { +#if !ENABLE_WORLD_COORDINATE m_offset = Vec3d::Zero(); m_rotation = Vec3d::Zero(); m_scaling_factor = Vec3d::Ones(); m_mirror = Vec3d::Ones(); +#endif // !ENABLE_WORLD_COORDINATE m_matrix = Transform3d::Identity(); +#if !ENABLE_WORLD_COORDINATE m_dirty = false; +#endif // !ENABLE_WORLD_COORDINATE } +#if ENABLE_WORLD_COORDINATE +void Transformation::reset_skew() +{ + Matrix3d rotation; + Matrix3d scale; + m_matrix.computeRotationScaling(&rotation, &scale); + + const double average_scale = std::cbrt(scale(0, 0) * scale(1, 1) * scale(2, 2)); + + scale(0, 0) = average_scale; + scale(1, 1) = average_scale; + scale(2, 2) = average_scale; + + scale(0, 1) = 0.0; + scale(0, 2) = 0.0; + scale(1, 0) = 0.0; + scale(1, 2) = 0.0; + scale(2, 0) = 0.0; + scale(2, 1) = 0.0; + + const Vec3d offset = get_offset(); + m_matrix = rotation * scale; + m_matrix.translation() = offset; +} + +Transform3d Transformation::get_matrix_no_offset() const +{ + Transformation copy(*this); + copy.reset_offset(); + return copy.get_matrix(); +} + +Transform3d Transformation::get_matrix_no_scaling_factor() const +{ + Transformation copy(*this); + copy.reset_scaling_factor(); + copy.reset_mirror(); + return copy.get_matrix(); +} +#else const Transform3d& Transformation::get_matrix(bool dont_translate, bool dont_rotate, bool dont_scale, bool dont_mirror) const { if (m_dirty || m_flags.needs_update(dont_translate, dont_rotate, dont_scale, dont_mirror)) { @@ -520,12 +757,14 @@ const Transform3d& Transformation::get_matrix(bool dont_translate, bool dont_rot return m_matrix; } +#endif // ENABLE_WORLD_COORDINATE Transformation Transformation::operator * (const Transformation& other) const { return Transformation(get_matrix() * other.get_matrix()); } +#if !ENABLE_WORLD_COORDINATE Transformation Transformation::volume_to_bed_transformation(const Transformation& instance_transformation, const BoundingBoxf3& bbox) { Transformation out; @@ -571,8 +810,7 @@ Transformation Transformation::volume_to_bed_transformation(const Transformation out.set_scaling_factor(Vec3d(std::abs(scale.x()), std::abs(scale.y()), std::abs(scale.z()))); out.set_mirror(Vec3d(scale.x() > 0 ? 1. : -1, scale.y() > 0 ? 1. : -1, scale.z() > 0 ? 1. : -1)); } - else - { + else { // General anisotropic scaling, general rotation. // Keep the modifier mesh in the instance coordinate system, so the modifier mesh will not be aligned with the world. // Scale it to get the required size. @@ -581,6 +819,7 @@ Transformation Transformation::volume_to_bed_transformation(const Transformation return out; } +#endif // !ENABLE_WORLD_COORDINATE // For parsing a transformation matrix from 3MF / AMF. Transform3d transform3d_from_string(const std::string& transform_str) @@ -619,7 +858,7 @@ Eigen::Quaterniond rotation_xyz_diff(const Vec3d &rot_xyz_from, const Vec3d &rot double rotation_diff_z(const Vec3d &rot_xyz_from, const Vec3d &rot_xyz_to) { const Eigen::AngleAxisd angle_axis(rotation_xyz_diff(rot_xyz_from, rot_xyz_to)); - const Vec3d axis = angle_axis.axis(); + const Vec3d& axis = angle_axis.axis(); const double angle = angle_axis.angle(); #ifndef NDEBUG if (std::abs(angle) > 1e-8) { diff --git a/src/libslic3r/Geometry.hpp b/src/libslic3r/Geometry.hpp index 2ca4ef884..aa09a0d3e 100644 --- a/src/libslic3r/Geometry.hpp +++ b/src/libslic3r/Geometry.hpp @@ -323,7 +323,8 @@ bool arrange( // 4) rotate Y // 5) rotate Z // 6) translate -void assemble_transform(Transform3d& transform, const Vec3d& translation = Vec3d::Zero(), const Vec3d& rotation = Vec3d::Zero(), const Vec3d& scale = Vec3d::Ones(), const Vec3d& mirror = Vec3d::Ones()); +void assemble_transform(Transform3d& transform, const Vec3d& translation = Vec3d::Zero(), const Vec3d& rotation = Vec3d::Zero(), + const Vec3d& scale = Vec3d::Ones(), const Vec3d& mirror = Vec3d::Ones()); // Returns the transform obtained by assembling the given transformations in the following order: // 1) mirror @@ -332,7 +333,43 @@ void assemble_transform(Transform3d& transform, const Vec3d& translation = Vec3d // 4) rotate Y // 5) rotate Z // 6) translate -Transform3d assemble_transform(const Vec3d& translation = Vec3d::Zero(), const Vec3d& rotation = Vec3d::Zero(), const Vec3d& scale = Vec3d::Ones(), const Vec3d& mirror = Vec3d::Ones()); +Transform3d assemble_transform(const Vec3d& translation = Vec3d::Zero(), const Vec3d& rotation = Vec3d::Zero(), + const Vec3d& scale = Vec3d::Ones(), const Vec3d& mirror = Vec3d::Ones()); + +// Sets the given transform by multiplying the given transformations in the following order: +// T = translation * rotation * scale * mirror +void assemble_transform(Transform3d& transform, const Transform3d& translation = Transform3d::Identity(), + const Transform3d& rotation = Transform3d::Identity(), const Transform3d& scale = Transform3d::Identity(), + const Transform3d& mirror = Transform3d::Identity()); + +// Returns the transform obtained by multiplying the given transformations in the following order: +// T = translation * rotation * scale * mirror +Transform3d assemble_transform(const Transform3d& translation = Transform3d::Identity(), const Transform3d& rotation = Transform3d::Identity(), + const Transform3d& scale = Transform3d::Identity(), const Transform3d& mirror = Transform3d::Identity()); + +// Sets the given transform by assembling the given translation +void translation_transform(Transform3d& transform, const Vec3d& translation); + +// Returns the transform obtained by assembling the given translation +Transform3d translation_transform(const Vec3d& translation); + +// Sets the given transform by assembling the given rotations in the following order: +// 1) rotate X +// 2) rotate Y +// 3) rotate Z +void rotation_transform(Transform3d& transform, const Vec3d& rotation); + +// Returns the transform obtained by assembling the given rotations in the following order: +// 1) rotate X +// 2) rotate Y +// 3) rotate Z +Transform3d rotation_transform(const Vec3d& rotation); + +// Sets the given transform by assembling the given scale factors +void scale_transform(Transform3d& transform, const Vec3d& scale); + +// Returns the transform obtained by assembling the given scale factors +Transform3d scale_transform(const Vec3d& scale); // Returns the euler angles extracted from the given rotation matrix // Warning -> The matrix should not contain any scale or shear !!! @@ -344,6 +381,9 @@ Vec3d extract_euler_angles(const Transform3d& transform); class Transformation { +#if ENABLE_WORLD_COORDINATE + Transform3d m_matrix{ Transform3d::Identity() }; +#else struct Flags { bool dont_translate{ true }; @@ -363,8 +403,26 @@ class Transformation mutable Transform3d m_matrix{ Transform3d::Identity() }; mutable Flags m_flags; mutable bool m_dirty{ false }; +#endif // ENABLE_WORLD_COORDINATE public: +#if ENABLE_WORLD_COORDINATE + Transformation() = default; + explicit Transformation(const Transform3d& transform) : m_matrix(transform) {} + + Vec3d get_offset() const { return m_matrix.translation(); } + double get_offset(Axis axis) const { return get_offset()[axis]; } + + Transform3d get_offset_matrix() const; + + void set_offset(const Vec3d& offset) { m_matrix.translation() = offset; } + void set_offset(Axis axis, double offset) { m_matrix.translation()[axis] = offset; } + + Vec3d get_rotation() const; + double get_rotation(Axis axis) const { return get_rotation()[axis]; } + + Transform3d get_rotation_matrix() const; +#else Transformation(); explicit Transformation(const Transform3d& transform); @@ -376,47 +434,103 @@ public: const Vec3d& get_rotation() const { return m_rotation; } double get_rotation(Axis axis) const { return m_rotation(axis); } +#endif // ENABLE_WORLD_COORDINATE void set_rotation(const Vec3d& rotation); void set_rotation(Axis axis, double rotation); +#if ENABLE_WORLD_COORDINATE + Vec3d get_scaling_factor() const; + double get_scaling_factor(Axis axis) const { return get_scaling_factor()[axis]; } + + Transform3d get_scaling_factor_matrix() const; + + bool is_scaling_uniform() const { + const Vec3d scale = get_scaling_factor(); + return std::abs(scale.x() - scale.y()) < 1e-8 && std::abs(scale.x() - scale.z()) < 1e-8; + } +#else const Vec3d& get_scaling_factor() const { return m_scaling_factor; } double get_scaling_factor(Axis axis) const { return m_scaling_factor(axis); } +#endif // ENABLE_WORLD_COORDINATE void set_scaling_factor(const Vec3d& scaling_factor); void set_scaling_factor(Axis axis, double scaling_factor); + +#if ENABLE_WORLD_COORDINATE + Vec3d get_mirror() const; + double get_mirror(Axis axis) const { return get_mirror()[axis]; } + + Transform3d get_mirror_matrix() const; + + bool is_left_handed() const { + const Vec3d mirror = get_mirror(); + return mirror.x() * mirror.y() * mirror.z() < 0.0; + } +#else bool is_scaling_uniform() const { return std::abs(m_scaling_factor.x() - m_scaling_factor.y()) < 1e-8 && std::abs(m_scaling_factor.x() - m_scaling_factor.z()) < 1e-8; } const Vec3d& get_mirror() const { return m_mirror; } double get_mirror(Axis axis) const { return m_mirror(axis); } bool is_left_handed() const { return m_mirror.x() * m_mirror.y() * m_mirror.z() < 0.; } +#endif // ENABLE_WORLD_COORDINATE void set_mirror(const Vec3d& mirror); void set_mirror(Axis axis, double mirror); +#if ENABLE_WORLD_COORDINATE + bool has_skew() const; +#else void set_from_transform(const Transform3d& transform); +#endif // ENABLE_WORLD_COORDINATE void reset(); +#if ENABLE_WORLD_COORDINATE + void reset_offset() { set_offset(Vec3d::Zero()); } + void reset_rotation() { set_rotation(Vec3d::Zero()); } + void reset_scaling_factor() { set_scaling_factor(Vec3d::Ones()); } + void reset_mirror() { set_mirror(Vec3d::Ones()); } + void reset_skew(); + const Transform3d& get_matrix() const { return m_matrix; } + Transform3d get_matrix_no_offset() const; + Transform3d get_matrix_no_scaling_factor() const; + + void set_matrix(const Transform3d& transform) { m_matrix = transform; } +#else const Transform3d& get_matrix(bool dont_translate = false, bool dont_rotate = false, bool dont_scale = false, bool dont_mirror = false) const; +#endif // ENABLE_WORLD_COORDINATE Transformation operator * (const Transformation& other) const; +#if !ENABLE_WORLD_COORDINATE // Find volume transformation, so that the chained (instance_trafo * volume_trafo) will be as close to identity // as possible in least squares norm in regard to the 8 corners of bbox. // Bounding box is expected to be centered around zero in all axes. static Transformation volume_to_bed_transformation(const Transformation& instance_transformation, const BoundingBoxf3& bbox); +#endif // !ENABLE_WORLD_COORDINATE private: - friend class cereal::access; - template void serialize(Archive & ar) { ar(m_offset, m_rotation, m_scaling_factor, m_mirror); } - explicit Transformation(int) : m_dirty(true) {} - template static void load_and_construct(Archive &ar, cereal::construct &construct) - { - // Calling a private constructor with special "int" parameter to indicate that no construction is necessary. - construct(1); - ar(construct.ptr()->m_offset, construct.ptr()->m_rotation, construct.ptr()->m_scaling_factor, construct.ptr()->m_mirror); - } + friend class cereal::access; +#if ENABLE_WORLD_COORDINATE + template void serialize(Archive& ar) { ar(m_matrix); } + explicit Transformation(int) {} + template static void load_and_construct(Archive& ar, cereal::construct& construct) + { + // Calling a private constructor with special "int" parameter to indicate that no construction is necessary. + construct(1); + ar(construct.ptr()->m_matrix); + } +#else + template void serialize(Archive& ar) { ar(m_offset, m_rotation, m_scaling_factor, m_mirror); } + explicit Transformation(int) : m_dirty(true) {} + template static void load_and_construct(Archive& ar, cereal::construct& construct) + { + // Calling a private constructor with special "int" parameter to indicate that no construction is necessary. + construct(1); + ar(construct.ptr()->m_offset, construct.ptr()->m_rotation, construct.ptr()->m_scaling_factor, construct.ptr()->m_mirror); + } +#endif // ENABLE_WORLD_COORDINATE }; // For parsing a transformation matrix from 3MF / AMF. diff --git a/src/libslic3r/LocalesUtils.hpp b/src/libslic3r/LocalesUtils.hpp index f63c3572f..ce8030ceb 100644 --- a/src/libslic3r/LocalesUtils.hpp +++ b/src/libslic3r/LocalesUtils.hpp @@ -43,6 +43,25 @@ std::string float_to_string_decimal_point(double value, int precision = -1); //std::string float_to_string_decimal_point(float value, int precision = -1); double string_to_double_decimal_point(const std::string_view str, size_t* pos = nullptr); +// Set locales to "C". +inline void set_c_locales() +{ +#ifdef _WIN32 + _configthreadlocale(_ENABLE_PER_THREAD_LOCALE); + std::setlocale(LC_ALL, "C"); +#else + // We are leaking some memory here, because the newlocale() produced memory will never be released. + // This is not a problem though, as there will be a maximum one worker thread created per physical thread. + uselocale(newlocale( +#ifdef __APPLE__ + LC_ALL_MASK +#else // some Unix / Linux / BSD + LC_ALL +#endif + , "C", nullptr)); +#endif +} + } // namespace Slic3r #endif // slic3r_LocalesUtils_hpp_ diff --git a/src/libslic3r/Model.cpp b/src/libslic3r/Model.cpp index 8a8bb9490..19b2f8f7d 100644 --- a/src/libslic3r/Model.cpp +++ b/src/libslic3r/Model.cpp @@ -947,7 +947,11 @@ const BoundingBoxf3& ModelObject::raw_bounding_box() const if (this->instances.empty()) throw Slic3r::InvalidArgument("Can't call raw_bounding_box() with no instances"); +#if ENABLE_WORLD_COORDINATE + const Transform3d inst_matrix = this->instances.front()->get_transformation().get_matrix_no_offset(); +#else const Transform3d& inst_matrix = this->instances.front()->get_transformation().get_matrix(true); +#endif // ENABLE_WORLD_COORDINATE for (const ModelVolume *v : this->volumes) if (v->is_model_part()) m_raw_bounding_box.merge(v->mesh().transformed_bounding_box(inst_matrix * v->get_matrix())); @@ -959,9 +963,15 @@ const BoundingBoxf3& ModelObject::raw_bounding_box() const BoundingBoxf3 ModelObject::instance_bounding_box(size_t instance_idx, bool dont_translate) const { BoundingBoxf3 bb; +#if ENABLE_WORLD_COORDINATE + const Transform3d inst_matrix = dont_translate ? + this->instances[instance_idx]->get_transformation().get_matrix_no_offset() : + this->instances[instance_idx]->get_transformation().get_matrix(); + +#else const Transform3d& inst_matrix = this->instances[instance_idx]->get_transformation().get_matrix(dont_translate); - for (ModelVolume *v : this->volumes) - { +#endif // ENABLE_WORLD_COORDINATE + for (ModelVolume *v : this->volumes) { if (v->is_model_part()) bb.merge(v->mesh().transformed_bounding_box(inst_matrix * v->get_matrix())); } @@ -1730,9 +1740,12 @@ void ModelObject::split(ModelObjectPtrs* new_objects) new_object->add_instance(*model_instance); ModelVolume* new_vol = new_object->add_volume(*volume, std::move(mesh)); - for (ModelInstance* model_instance : new_object->instances) - { + for (ModelInstance* model_instance : new_object->instances) { +#if ENABLE_WORLD_COORDINATE + Vec3d shift = model_instance->get_transformation().get_matrix_no_offset() * new_vol->get_offset(); +#else Vec3d shift = model_instance->get_transformation().get_matrix(true) * new_vol->get_offset(); +#endif // ENABLE_WORLD_COORDINATE model_instance->set_offset(model_instance->get_offset() + shift); } @@ -1774,9 +1787,11 @@ void ModelObject::bake_xy_rotation_into_meshes(size_t instance_idx) assert(instance_idx < this->instances.size()); const Geometry::Transformation reference_trafo = this->instances[instance_idx]->get_transformation(); +#if !ENABLE_WORLD_COORDINATE if (Geometry::is_rotation_ninety_degrees(reference_trafo.get_rotation())) // nothing to do, scaling in the world coordinate space is possible in the representation of Geometry::Transformation. return; +#endif // !ENABLE_WORLD_COORDINATE bool left_handed = reference_trafo.is_left_handed(); bool has_mirrorring = ! reference_trafo.get_mirror().isApprox(Vec3d(1., 1., 1.)); @@ -1794,8 +1809,18 @@ void ModelObject::bake_xy_rotation_into_meshes(size_t instance_idx) // Adjust the meshes. // Transformation to be applied to the meshes. +#if ENABLE_WORLD_COORDINATE + Geometry::Transformation reference_trafo_mod = reference_trafo; + reference_trafo_mod.reset_offset(); + if (uniform_scaling) + reference_trafo_mod.reset_scaling_factor(); + if (!has_mirrorring) + reference_trafo_mod.reset_mirror(); + Eigen::Matrix3d mesh_trafo_3x3 = reference_trafo_mod.get_matrix().matrix().block<3, 3>(0, 0); +#else Eigen::Matrix3d mesh_trafo_3x3 = reference_trafo.get_matrix(true, false, uniform_scaling, ! has_mirrorring).matrix().block<3, 3>(0, 0); - Transform3d volume_offset_correction = this->instances[instance_idx]->get_transformation().get_matrix().inverse() * reference_trafo.get_matrix(); +#endif // ENABLE_WORLD_COORDINATE + Transform3d volume_offset_correction = this->instances[instance_idx]->get_transformation().get_matrix().inverse() * reference_trafo.get_matrix(); for (ModelVolume *model_volume : this->volumes) { const Geometry::Transformation volume_trafo = model_volume->get_transformation(); bool volume_left_handed = volume_trafo.is_left_handed(); @@ -1804,7 +1829,17 @@ void ModelObject::bake_xy_rotation_into_meshes(size_t instance_idx) std::abs(volume_trafo.get_scaling_factor().x() - volume_trafo.get_scaling_factor().z()) < EPSILON; double volume_new_scaling_factor = volume_uniform_scaling ? volume_trafo.get_scaling_factor().x() : 1.; // Transform the mesh. - Matrix3d volume_trafo_3x3 = volume_trafo.get_matrix(true, false, volume_uniform_scaling, !volume_has_mirrorring).matrix().block<3, 3>(0, 0); +#if ENABLE_WORLD_COORDINATE + Geometry::Transformation volume_trafo_mod = volume_trafo; + volume_trafo_mod.reset_offset(); + if (volume_uniform_scaling) + volume_trafo_mod.reset_scaling_factor(); + if (!volume_has_mirrorring) + volume_trafo_mod.reset_mirror(); + Eigen::Matrix3d volume_trafo_3x3 = volume_trafo_mod.get_matrix().matrix().block<3, 3>(0, 0); +#else + Matrix3d volume_trafo_3x3 = volume_trafo.get_matrix(true, false, volume_uniform_scaling, !volume_has_mirrorring).matrix().block<3, 3>(0, 0); +#endif // ENABLE_WORLD_COORDINATE // Following method creates a new shared_ptr model_volume->transform_this_mesh(mesh_trafo_3x3 * volume_trafo_3x3, left_handed != volume_left_handed); // Reset the rotation, scaling and mirroring. @@ -1851,7 +1886,11 @@ double ModelObject::get_instance_min_z(size_t instance_idx) const double min_z = DBL_MAX; const ModelInstance* inst = instances[instance_idx]; +#if ENABLE_WORLD_COORDINATE + const Transform3d mi = inst->get_matrix_no_offset(); +#else const Transform3d& mi = inst->get_matrix(true); +#endif // ENABLE_WORLD_COORDINATE for (const ModelVolume* v : volumes) { if (!v->is_model_part()) @@ -1872,7 +1911,11 @@ double ModelObject::get_instance_max_z(size_t instance_idx) const double max_z = -DBL_MAX; const ModelInstance* inst = instances[instance_idx]; +#if ENABLE_WORLD_COORDINATE + const Transform3d mi = inst->get_matrix_no_offset(); +#else const Transform3d& mi = inst->get_matrix(true); +#endif // ENABLE_WORLD_COORDINATE for (const ModelVolume* v : volumes) { if (!v->is_model_part()) @@ -2298,14 +2341,22 @@ void ModelVolume::convert_from_meters() void ModelInstance::transform_mesh(TriangleMesh* mesh, bool dont_translate) const { +#if ENABLE_WORLD_COORDINATE + mesh->transform(dont_translate ? get_matrix_no_offset() : get_matrix()); +#else mesh->transform(get_matrix(dont_translate)); +#endif // ENABLE_WORLD_COORDINATE } BoundingBoxf3 ModelInstance::transform_mesh_bounding_box(const TriangleMesh& mesh, bool dont_translate) const { // Rotate around mesh origin. TriangleMesh copy(mesh); +#if ENABLE_WORLD_COORDINATE + copy.transform(get_transformation().get_rotation_matrix()); +#else copy.transform(get_matrix(true, false, true, true)); +#endif // ENABLE_WORLD_COORDINATE BoundingBoxf3 bbox = copy.bounding_box(); if (!empty(bbox)) { @@ -2330,12 +2381,20 @@ BoundingBoxf3 ModelInstance::transform_mesh_bounding_box(const TriangleMesh& mes BoundingBoxf3 ModelInstance::transform_bounding_box(const BoundingBoxf3 &bbox, bool dont_translate) const { +#if ENABLE_WORLD_COORDINATE + return bbox.transformed(dont_translate ? get_matrix_no_offset() : get_matrix()); +#else return bbox.transformed(get_matrix(dont_translate)); +#endif // ENABLE_WORLD_COORDINATE } Vec3d ModelInstance::transform_vector(const Vec3d& v, bool dont_translate) const { +#if ENABLE_WORLD_COORDINATE + return dont_translate ? get_matrix_no_offset() * v : get_matrix() * v; +#else return get_matrix(dont_translate) * v; +#endif // ENABLE_WORLD_COORDINATE } void ModelInstance::transform_polygon(Polygon* polygon) const diff --git a/src/libslic3r/Model.hpp b/src/libslic3r/Model.hpp index 871859cf0..72d9b33c1 100644 --- a/src/libslic3r/Model.hpp +++ b/src/libslic3r/Model.hpp @@ -781,15 +781,26 @@ public: const Geometry::Transformation& get_transformation() const { return m_transformation; } void set_transformation(const Geometry::Transformation& transformation) { m_transformation = transformation; } +#if ENABLE_WORLD_COORDINATE + void set_transformation(const Transform3d& trafo) { m_transformation.set_matrix(trafo); } + + Vec3d get_offset() const { return m_transformation.get_offset(); } +#else void set_transformation(const Transform3d &trafo) { m_transformation.set_from_transform(trafo); } const Vec3d& get_offset() const { return m_transformation.get_offset(); } +#endif // ENABLE_WORLD_COORDINATE + double get_offset(Axis axis) const { return m_transformation.get_offset(axis); } void set_offset(const Vec3d& offset) { m_transformation.set_offset(offset); } void set_offset(Axis axis, double offset) { m_transformation.set_offset(axis, offset); } +#if ENABLE_WORLD_COORDINATE + Vec3d get_rotation() const { return m_transformation.get_rotation(); } +#else const Vec3d& get_rotation() const { return m_transformation.get_rotation(); } +#endif // ENABLE_WORLD_COORDINATE double get_rotation(Axis axis) const { return m_transformation.get_rotation(axis); } void set_rotation(const Vec3d& rotation) { m_transformation.set_rotation(rotation); } @@ -801,7 +812,11 @@ public: void set_scaling_factor(const Vec3d& scaling_factor) { m_transformation.set_scaling_factor(scaling_factor); } void set_scaling_factor(Axis axis, double scaling_factor) { m_transformation.set_scaling_factor(axis, scaling_factor); } +#if ENABLE_WORLD_COORDINATE + Vec3d get_mirror() const { return m_transformation.get_mirror(); } +#else const Vec3d& get_mirror() const { return m_transformation.get_mirror(); } +#endif // ENABLE_WORLD_COORDINATE double get_mirror(Axis axis) const { return m_transformation.get_mirror(axis); } bool is_left_handed() const { return m_transformation.is_left_handed(); } @@ -810,7 +825,12 @@ public: void convert_from_imperial_units(); void convert_from_meters(); +#if ENABLE_WORLD_COORDINATE + const Transform3d& get_matrix() const { return m_transformation.get_matrix(); } + Transform3d get_matrix_no_offset() const { return m_transformation.get_matrix_no_offset(); } +#else const Transform3d& get_matrix(bool dont_translate = false, bool dont_rotate = false, bool dont_scale = false, bool dont_mirror = false) const { return m_transformation.get_matrix(dont_translate, dont_rotate, dont_scale, dont_mirror); } +#endif // ENABLE_WORLD_COORDINATE void set_new_unique_id() { ObjectBase::set_new_unique_id(); @@ -1013,25 +1033,41 @@ public: const Geometry::Transformation& get_transformation() const { return m_transformation; } void set_transformation(const Geometry::Transformation& transformation) { m_transformation = transformation; } +#if ENABLE_WORLD_COORDINATE + Vec3d get_offset() const { return m_transformation.get_offset(); } +#else const Vec3d& get_offset() const { return m_transformation.get_offset(); } +#endif // ENABLE_WORLD_COORDINATE double get_offset(Axis axis) const { return m_transformation.get_offset(axis); } void set_offset(const Vec3d& offset) { m_transformation.set_offset(offset); } void set_offset(Axis axis, double offset) { m_transformation.set_offset(axis, offset); } +#if ENABLE_WORLD_COORDINATE + Vec3d get_rotation() const { return m_transformation.get_rotation(); } +#else const Vec3d& get_rotation() const { return m_transformation.get_rotation(); } +#endif // ENABLE_WORLD_COORDINATE double get_rotation(Axis axis) const { return m_transformation.get_rotation(axis); } void set_rotation(const Vec3d& rotation) { m_transformation.set_rotation(rotation); } void set_rotation(Axis axis, double rotation) { m_transformation.set_rotation(axis, rotation); } +#if ENABLE_WORLD_COORDINATE + Vec3d get_scaling_factor() const { return m_transformation.get_scaling_factor(); } +#else const Vec3d& get_scaling_factor() const { return m_transformation.get_scaling_factor(); } +#endif // ENABLE_WORLD_COORDINATE double get_scaling_factor(Axis axis) const { return m_transformation.get_scaling_factor(axis); } void set_scaling_factor(const Vec3d& scaling_factor) { m_transformation.set_scaling_factor(scaling_factor); } void set_scaling_factor(Axis axis, double scaling_factor) { m_transformation.set_scaling_factor(axis, scaling_factor); } +#if ENABLE_WORLD_COORDINATE + Vec3d get_mirror() const { return m_transformation.get_mirror(); } +#else const Vec3d& get_mirror() const { return m_transformation.get_mirror(); } +#endif // ENABLE_WORLD_COORDINATE double get_mirror(Axis axis) const { return m_transformation.get_mirror(axis); } bool is_left_handed() const { return m_transformation.is_left_handed(); } @@ -1049,7 +1085,12 @@ public: // To be called on an external polygon. It does not translate the polygon, only rotates and scales. void transform_polygon(Polygon* polygon) const; +#if ENABLE_WORLD_COORDINATE + const Transform3d& get_matrix() const { return m_transformation.get_matrix(); } + Transform3d get_matrix_no_offset() const { return m_transformation.get_matrix_no_offset(); } +#else const Transform3d& get_matrix(bool dont_translate = false, bool dont_rotate = false, bool dont_scale = false, bool dont_mirror = false) const { return m_transformation.get_matrix(dont_translate, dont_rotate, dont_scale, dont_mirror); } +#endif // ENABLE_WORLD_COORDINATE bool is_printable() const { return object->printable && printable && (print_volume_state == ModelInstancePVS_Inside); } diff --git a/src/libslic3r/MutablePriorityQueue.hpp b/src/libslic3r/MutablePriorityQueue.hpp index 418d8deff..fd3e7ac2d 100644 --- a/src/libslic3r/MutablePriorityQueue.hpp +++ b/src/libslic3r/MutablePriorityQueue.hpp @@ -3,6 +3,13 @@ #include #include +#include +#include +#include // adds size_t (without std::) + +namespace Slic3r { + +constexpr auto InvalidQueueID = std::numeric_limits::max(); template class MutablePriorityQueue @@ -21,7 +28,7 @@ public: MutablePriorityQueue& operator=(MutablePriorityQueue &&) = default; // This class modifies the outside data through the m_index_setter - // and thus it should not be copied. The semantics are similar to std::unique_ptr + // and thus it should not be copied. The semantics is similar to std::unique_ptr MutablePriorityQueue(const MutablePriorityQueue &) = delete; MutablePriorityQueue& operator=(const MutablePriorityQueue &) = delete; @@ -38,6 +45,7 @@ public: bool empty() const { return m_heap.empty(); } T& operator[](std::size_t idx) noexcept { return m_heap[idx]; } const T& operator[](std::size_t idx) const noexcept { return m_heap[idx]; } + static constexpr size_t invalid_id() { return InvalidQueueID; } using iterator = typename std::vector::iterator; using const_iterator = typename std::vector::const_iterator; @@ -66,14 +74,10 @@ MutablePriorityQueue make_ template inline void MutablePriorityQueue::clear() { -#ifdef NDEBUG - // Only mark as removed from the queue in release mode, if configured so. - if (ResetIndexWhenRemoved) -#endif /* NDEBUG */ - { + if constexpr (ResetIndexWhenRemoved) { for (size_t idx = 0; idx < m_heap.size(); ++ idx) // Mark as removed from the queue. - m_index_setter(m_heap[idx], std::numeric_limits::max()); + m_index_setter(m_heap[idx], this->invalid_id()); } m_heap.clear(); } @@ -100,13 +104,9 @@ template::pop() { assert(! m_heap.empty()); -#ifdef NDEBUG - // Only mark as removed from the queue in release mode, if configured so. - if (ResetIndexWhenRemoved) -#endif /* NDEBUG */ - { + if constexpr (ResetIndexWhenRemoved) { // Mark as removed from the queue. - m_index_setter(m_heap.front(), std::numeric_limits::max()); + m_index_setter(m_heap.front(), this->invalid_id()); } if (m_heap.size() > 1) { m_heap.front() = m_heap.back(); @@ -121,13 +121,10 @@ template::remove(size_t idx) { assert(idx < m_heap.size()); -#ifdef NDEBUG // Only mark as removed from the queue in release mode, if configured so. - if (ResetIndexWhenRemoved) -#endif /* NDEBUG */ - { + if constexpr (ResetIndexWhenRemoved) { // Mark as removed from the queue. - m_index_setter(m_heap[idx], std::numeric_limits::max()); + m_index_setter(m_heap[idx], this->invalid_id()); } if (idx + 1 == m_heap.size()) { m_heap.pop_back(); @@ -274,6 +271,14 @@ public: {} ~MutableSkipHeapPriorityQueue() { clear(); } + MutableSkipHeapPriorityQueue(MutableSkipHeapPriorityQueue &&) = default; + MutableSkipHeapPriorityQueue &operator=(MutableSkipHeapPriorityQueue &&) = default; + + // This class modifies the outside data through the m_index_setter + // and thus it should not be copied. The semantics is similar to std::unique_ptr + MutableSkipHeapPriorityQueue(const MutableSkipHeapPriorityQueue &) = delete; + MutableSkipHeapPriorityQueue &operator=(const MutableSkipHeapPriorityQueue &) = delete; + void clear(); // Reserve one unused element per miniheap. void reserve(size_t cnt) { m_heap.reserve(cnt + ((cnt + (address::block_size - 1)) / (address::block_size - 1))); } @@ -285,9 +290,12 @@ public: void update(size_t idx) { assert(! address::is_padding(idx)); T item = m_heap[idx]; remove(idx); push(item); } // There is one padding element storead at each miniheap, thus lower the number of elements by the number of miniheaps. size_t size() const noexcept { return m_heap.size() - (m_heap.size() + address::block_size - 1) / address::block_size; } + // Number of heap elements including padding. heap_size() >= size(). + size_t heap_size() const noexcept { return m_heap.size(); } bool empty() const { return m_heap.empty(); } T& operator[](std::size_t idx) noexcept { assert(! address::is_padding(idx)); return m_heap[idx]; } const T& operator[](std::size_t idx) const noexcept { assert(! address::is_padding(idx)); return m_heap[idx]; } + static constexpr size_t invalid_id() { return InvalidQueueID; } protected: void update_heap_up(size_t top, size_t bottom); @@ -317,15 +325,11 @@ MutableSkipHeapPriorityQueue inline void MutableSkipHeapPriorityQueue::clear() { -#ifdef NDEBUG - // Only mark as removed from the queue in release mode, if configured so. - if (ResetIndexWhenRemoved) -#endif /* NDEBUG */ - { + if constexpr (ResetIndexWhenRemoved) { for (size_t idx = 0; idx < m_heap.size(); ++ idx) // Mark as removed from the queue. if (! address::is_padding(idx)) - m_index_setter(m_heap[idx], std::numeric_limits::max()); + m_index_setter(m_heap[idx], this->invalid_id()); } m_heap.clear(); } @@ -356,13 +360,9 @@ template::pop() { assert(! m_heap.empty()); -#ifdef NDEBUG - // Only mark as removed from the queue in release mode, if configured so. - if (ResetIndexWhenRemoved) -#endif /* NDEBUG */ - { + if constexpr (ResetIndexWhenRemoved) { // Mark as removed from the queue. - m_index_setter(m_heap[1], std::numeric_limits::max()); + m_index_setter(m_heap[1], this->invalid_id()); } // Zero'th element is padding, thus non-empty queue must have at least two elements. if (m_heap.size() > 2) { @@ -379,13 +379,9 @@ inline void MutableSkipHeapPriorityQueue::max()); + m_index_setter(m_heap[idx], this->invalid_id()); } if (idx + 1 == m_heap.size()) { this->pop_back(); @@ -458,4 +454,6 @@ inline void MutableSkipHeapPriorityQueue -#include -#include -#include -#include -#include - -#include - -#include "LocalesUtils.hpp" - -namespace Slic3r { - -class BoundingBox; -class BoundingBoxf; -class Line; -class MultiPoint; -class Point; -using Vector = Point; - -// Base template for eigen derived vectors -template -using Mat = Eigen::Matrix; - -template using Vec = Mat; - -template -using DynVec = Eigen::Matrix; - -// Eigen types, to replace the Slic3r's own types in the future. -// Vector types with a fixed point coordinate base type. -using Vec2crd = Eigen::Matrix; -using Vec3crd = Eigen::Matrix; -using Vec2i = Eigen::Matrix; -using Vec3i = Eigen::Matrix; -using Vec4i = Eigen::Matrix; -using Vec2i32 = Eigen::Matrix; -using Vec2i64 = Eigen::Matrix; -using Vec3i32 = Eigen::Matrix; -using Vec3i64 = Eigen::Matrix; - -// Vector types with a double coordinate base type. -using Vec2f = Eigen::Matrix; -using Vec3f = Eigen::Matrix; -using Vec2d = Eigen::Matrix; -using Vec3d = Eigen::Matrix; - -using Points = std::vector; -using PointPtrs = std::vector; -using PointConstPtrs = std::vector; -using Points3 = std::vector; -using Pointfs = std::vector; -using Vec2ds = std::vector; -using Pointf3s = std::vector; - -using Matrix2f = Eigen::Matrix; -using Matrix2d = Eigen::Matrix; -using Matrix3f = Eigen::Matrix; -using Matrix3d = Eigen::Matrix; -using Matrix4f = Eigen::Matrix; -using Matrix4d = Eigen::Matrix; - -template -using Transform = Eigen::Transform; - -using Transform2f = Eigen::Transform; -using Transform2d = Eigen::Transform; -using Transform3f = Eigen::Transform; -using Transform3d = Eigen::Transform; - -// I don't know why Eigen::Transform::Identity() return a const object... -template Transform identity() { return Transform::Identity(); } -inline const auto &identity3f = identity<3, float>; -inline const auto &identity3d = identity<3, double>; - -inline bool operator<(const Vec2d &lhs, const Vec2d &rhs) { return lhs.x() < rhs.x() || (lhs.x() == rhs.x() && lhs.y() < rhs.y()); } - -// Cross product of two 2D vectors. -// None of the vectors may be of int32_t type as the result would overflow. -template -inline typename Derived::Scalar cross2(const Eigen::MatrixBase &v1, const Eigen::MatrixBase &v2) -{ - static_assert(Derived::IsVectorAtCompileTime && int(Derived::SizeAtCompileTime) == 2, "cross2(): first parameter is not a 2D vector"); - static_assert(Derived2::IsVectorAtCompileTime && int(Derived2::SizeAtCompileTime) == 2, "cross2(): first parameter is not a 2D vector"); - static_assert(! std::is_same::value, "cross2(): Scalar type must not be int32_t, otherwise the cross product would overflow."); - static_assert(std::is_same::value, "cross2(): Scalar types of 1st and 2nd operand must be equal."); - return v1.x() * v2.y() - v1.y() * v2.x(); -} - -// 2D vector perpendicular to the argument. -template -inline Eigen::Matrix perp(const Eigen::MatrixBase &v) -{ - static_assert(Derived::IsVectorAtCompileTime && int(Derived::SizeAtCompileTime) == 2, "perp(): parameter is not a 2D vector"); - return { - v.y(), v.x() }; -} - -// Angle from v1 to v2, returning double atan2(y, x) normalized to <-PI, PI>. -template -inline double angle(const Eigen::MatrixBase &v1, const Eigen::MatrixBase &v2) { - static_assert(Derived::IsVectorAtCompileTime && int(Derived::SizeAtCompileTime) == 2, "angle(): first parameter is not a 2D vector"); - static_assert(Derived2::IsVectorAtCompileTime && int(Derived2::SizeAtCompileTime) == 2, "angle(): second parameter is not a 2D vector"); - auto v1d = v1.template cast(); - auto v2d = v2.template cast(); - return atan2(cross2(v1d, v2d), v1d.dot(v2d)); -} - -template -Eigen::Matrix to_2d(const Eigen::MatrixBase> &ptN) { return { ptN.x(), ptN.y() }; } - -template -Eigen::Matrix to_3d(const Eigen::MatrixBase> & pt, const T z) { return { pt.x(), pt.y(), z }; } - -inline Vec2d unscale(coord_t x, coord_t y) { return Vec2d(unscale(x), unscale(y)); } -inline Vec2d unscale(const Vec2crd &pt) { return Vec2d(unscale(pt.x()), unscale(pt.y())); } -inline Vec2d unscale(const Vec2d &pt) { return Vec2d(unscale(pt.x()), unscale(pt.y())); } -inline Vec3d unscale(coord_t x, coord_t y, coord_t z) { return Vec3d(unscale(x), unscale(y), unscale(z)); } -inline Vec3d unscale(const Vec3crd &pt) { return Vec3d(unscale(pt.x()), unscale(pt.y()), unscale(pt.z())); } -inline Vec3d unscale(const Vec3d &pt) { return Vec3d(unscale(pt.x()), unscale(pt.y()), unscale(pt.z())); } - -inline std::string to_string(const Vec2crd &pt) { return std::string("[") + float_to_string_decimal_point(pt.x()) + ", " + float_to_string_decimal_point(pt.y()) + "]"; } -inline std::string to_string(const Vec2d &pt) { return std::string("[") + float_to_string_decimal_point(pt.x()) + ", " + float_to_string_decimal_point(pt.y()) + "]"; } -inline std::string to_string(const Vec3crd &pt) { return std::string("[") + float_to_string_decimal_point(pt.x()) + ", " + float_to_string_decimal_point(pt.y()) + ", " + float_to_string_decimal_point(pt.z()) + "]"; } -inline std::string to_string(const Vec3d &pt) { return std::string("[") + float_to_string_decimal_point(pt.x()) + ", " + float_to_string_decimal_point(pt.y()) + ", " + float_to_string_decimal_point(pt.z()) + "]"; } - -std::vector transform(const std::vector& points, const Transform3f& t); -Pointf3s transform(const Pointf3s& points, const Transform3d& t); - -template using Vec = Eigen::Matrix; - -class Point : public Vec2crd -{ -public: - using coord_type = coord_t; - - Point() : Vec2crd(0, 0) {} - Point(int32_t x, int32_t y) : Vec2crd(coord_t(x), coord_t(y)) {} - Point(int64_t x, int64_t y) : Vec2crd(coord_t(x), coord_t(y)) {} - Point(double x, double y) : Vec2crd(coord_t(lrint(x)), coord_t(lrint(y))) {} - Point(const Point &rhs) { *this = rhs; } - explicit Point(const Vec2d& rhs) : Vec2crd(coord_t(lrint(rhs.x())), coord_t(lrint(rhs.y()))) {} - // This constructor allows you to construct Point from Eigen expressions - template - Point(const Eigen::MatrixBase &other) : Vec2crd(other) {} - static Point new_scale(coordf_t x, coordf_t y) { return Point(coord_t(scale_(x)), coord_t(scale_(y))); } - static Point new_scale(const Vec2d &v) { return Point(coord_t(scale_(v.x())), coord_t(scale_(v.y()))); } - static Point new_scale(const Vec2f &v) { return Point(coord_t(scale_(v.x())), coord_t(scale_(v.y()))); } - - // This method allows you to assign Eigen expressions to MyVectorType - template - Point& operator=(const Eigen::MatrixBase &other) - { - this->Vec2crd::operator=(other); - return *this; - } - - Point& operator+=(const Point& rhs) { this->x() += rhs.x(); this->y() += rhs.y(); return *this; } - Point& operator-=(const Point& rhs) { this->x() -= rhs.x(); this->y() -= rhs.y(); return *this; } - Point& operator*=(const double &rhs) { this->x() = coord_t(this->x() * rhs); this->y() = coord_t(this->y() * rhs); return *this; } - Point operator*(const double &rhs) { return Point(this->x() * rhs, this->y() * rhs); } - - void rotate(double angle) { this->rotate(std::cos(angle), std::sin(angle)); } - void rotate(double cos_a, double sin_a) { - double cur_x = (double)this->x(); - double cur_y = (double)this->y(); - this->x() = (coord_t)round(cos_a * cur_x - sin_a * cur_y); - this->y() = (coord_t)round(cos_a * cur_y + sin_a * cur_x); - } - - void rotate(double angle, const Point ¢er); - Point rotated(double angle) const { Point res(*this); res.rotate(angle); return res; } - Point rotated(double cos_a, double sin_a) const { Point res(*this); res.rotate(cos_a, sin_a); return res; } - Point rotated(double angle, const Point ¢er) const { Point res(*this); res.rotate(angle, center); return res; } - int nearest_point_index(const Points &points) const; - int nearest_point_index(const PointConstPtrs &points) const; - int nearest_point_index(const PointPtrs &points) const; - bool nearest_point(const Points &points, Point* point) const; - Point projection_onto(const MultiPoint &poly) const; - Point projection_onto(const Line &line) const; -}; - -inline bool operator<(const Point &l, const Point &r) -{ - return l.x() < r.x() || (l.x() == r.x() && l.y() < r.y()); -} - -inline Point operator* (const Point& l, const double &r) -{ - return {coord_t(l.x() * r), coord_t(l.y() * r)}; -} - -inline bool is_approx(const Point &p1, const Point &p2, coord_t epsilon = coord_t(SCALED_EPSILON)) -{ - Point d = (p2 - p1).cwiseAbs(); - return d.x() < epsilon && d.y() < epsilon; -} - -inline bool is_approx(const Vec2f &p1, const Vec2f &p2, float epsilon = float(EPSILON)) -{ - Vec2f d = (p2 - p1).cwiseAbs(); - return d.x() < epsilon && d.y() < epsilon; -} - -inline bool is_approx(const Vec2d &p1, const Vec2d &p2, double epsilon = EPSILON) -{ - Vec2d d = (p2 - p1).cwiseAbs(); - return d.x() < epsilon && d.y() < epsilon; -} - -inline bool is_approx(const Vec3f &p1, const Vec3f &p2, float epsilon = float(EPSILON)) -{ - Vec3f d = (p2 - p1).cwiseAbs(); - return d.x() < epsilon && d.y() < epsilon && d.z() < epsilon; -} - -inline bool is_approx(const Vec3d &p1, const Vec3d &p2, double epsilon = EPSILON) -{ - Vec3d d = (p2 - p1).cwiseAbs(); - return d.x() < epsilon && d.y() < epsilon && d.z() < epsilon; -} - -inline Point lerp(const Point &a, const Point &b, double t) -{ - assert((t >= -EPSILON) && (t <= 1. + EPSILON)); - return ((1. - t) * a.cast() + t * b.cast()).cast(); -} - -BoundingBox get_extents(const Points &pts); -BoundingBox get_extents(const std::vector &pts); -BoundingBoxf get_extents(const std::vector &pts); - -// Test for duplicate points in a vector of points. -// The points are copied, sorted and checked for duplicates globally. -bool has_duplicate_points(std::vector &&pts); -inline bool has_duplicate_points(const std::vector &pts) -{ - std::vector cpy = pts; - return has_duplicate_points(std::move(cpy)); -} - -// Test for duplicate points in a vector of points. -// Only successive points are checked for equality. -inline bool has_duplicate_successive_points(const std::vector &pts) -{ - for (size_t i = 1; i < pts.size(); ++ i) - if (pts[i - 1] == pts[i]) - return true; - return false; -} - -// Test for duplicate points in a vector of points. -// Only successive points are checked for equality. Additionally, first and last points are compared for equality. -inline bool has_duplicate_successive_points_closed(const std::vector &pts) -{ - return has_duplicate_successive_points(pts) || (pts.size() >= 2 && pts.front() == pts.back()); -} - -namespace int128 { - // Exact orientation predicate, - // returns +1: CCW, 0: collinear, -1: CW. - int orient(const Vec2crd &p1, const Vec2crd &p2, const Vec2crd &p3); - // Exact orientation predicate, - // returns +1: CCW, 0: collinear, -1: CW. - int cross(const Vec2crd &v1, const Vec2crd &v2); -} - -// To be used by std::unordered_map, std::unordered_multimap and friends. -struct PointHash { - size_t operator()(const Vec2crd &pt) const { - return coord_t((89 * 31 + int64_t(pt.x())) * 31 + pt.y()); - } -}; - -// A generic class to search for a closest Point in a given radius. -// It uses std::unordered_multimap to implement an efficient 2D spatial hashing. -// The PointAccessor has to return const Point*. -// If a nullptr is returned, it is ignored by the query. -template class ClosestPointInRadiusLookup -{ -public: - ClosestPointInRadiusLookup(coord_t search_radius, PointAccessor point_accessor = PointAccessor()) : - m_search_radius(search_radius), m_point_accessor(point_accessor), m_grid_log2(0) - { - // Resolution of a grid, twice the search radius + some epsilon. - coord_t gridres = 2 * m_search_radius + 4; - m_grid_resolution = gridres; - assert(m_grid_resolution > 0); - assert(m_grid_resolution < (coord_t(1) << 30)); - // Compute m_grid_log2 = log2(m_grid_resolution) - if (m_grid_resolution > 32767) { - m_grid_resolution >>= 16; - m_grid_log2 += 16; - } - if (m_grid_resolution > 127) { - m_grid_resolution >>= 8; - m_grid_log2 += 8; - } - if (m_grid_resolution > 7) { - m_grid_resolution >>= 4; - m_grid_log2 += 4; - } - if (m_grid_resolution > 1) { - m_grid_resolution >>= 2; - m_grid_log2 += 2; - } - if (m_grid_resolution > 0) - ++ m_grid_log2; - m_grid_resolution = 1 << m_grid_log2; - assert(m_grid_resolution >= gridres); - assert(gridres > m_grid_resolution / 2); - } - - void insert(const ValueType &value) { - const Vec2crd *pt = m_point_accessor(value); - if (pt != nullptr) - m_map.emplace(std::make_pair(Vec2crd(pt->x()>>m_grid_log2, pt->y()>>m_grid_log2), value)); - } - - void insert(ValueType &&value) { - const Vec2crd *pt = m_point_accessor(value); - if (pt != nullptr) - m_map.emplace(std::make_pair(Vec2crd(pt->x()>>m_grid_log2, pt->y()>>m_grid_log2), std::move(value))); - } - - // Erase a data point equal to value. (ValueType has to declare the operator==). - // Returns true if the data point equal to value was found and removed. - bool erase(const ValueType &value) { - const Point *pt = m_point_accessor(value); - if (pt != nullptr) { - // Range of fragment starts around grid_corner, close to pt. - auto range = m_map.equal_range(Point((*pt).x()>>m_grid_log2, (*pt).y()>>m_grid_log2)); - // Remove the first item. - for (auto it = range.first; it != range.second; ++ it) { - if (it->second == value) { - m_map.erase(it); - return true; - } - } - } - return false; - } - - // Return a pair of - std::pair find(const Vec2crd &pt) { - // Iterate over 4 closest grid cells around pt, - // find the closest start point inside these cells to pt. - const ValueType *value_min = nullptr; - double dist_min = std::numeric_limits::max(); - // Round pt to a closest grid_cell corner. - Vec2crd grid_corner((pt.x()+(m_grid_resolution>>1))>>m_grid_log2, (pt.y()+(m_grid_resolution>>1))>>m_grid_log2); - // For four neighbors of grid_corner: - for (coord_t neighbor_y = -1; neighbor_y < 1; ++ neighbor_y) { - for (coord_t neighbor_x = -1; neighbor_x < 1; ++ neighbor_x) { - // Range of fragment starts around grid_corner, close to pt. - auto range = m_map.equal_range(Vec2crd(grid_corner.x() + neighbor_x, grid_corner.y() + neighbor_y)); - // Find the map entry closest to pt. - for (auto it = range.first; it != range.second; ++it) { - const ValueType &value = it->second; - const Vec2crd *pt2 = m_point_accessor(value); - if (pt2 != nullptr) { - const double d2 = (pt - *pt2).cast().squaredNorm(); - if (d2 < dist_min) { - dist_min = d2; - value_min = &value; - } - } - } - } - } - return (value_min != nullptr && dist_min < coordf_t(m_search_radius) * coordf_t(m_search_radius)) ? - std::make_pair(value_min, dist_min) : - std::make_pair(nullptr, std::numeric_limits::max()); - } - - // Returns all pairs of values and squared distances. - std::vector> find_all(const Vec2crd &pt) { - // Iterate over 4 closest grid cells around pt, - // Round pt to a closest grid_cell corner. - Vec2crd grid_corner((pt.x()+(m_grid_resolution>>1))>>m_grid_log2, (pt.y()+(m_grid_resolution>>1))>>m_grid_log2); - // For four neighbors of grid_corner: - std::vector> out; - const double r2 = double(m_search_radius) * m_search_radius; - for (coord_t neighbor_y = -1; neighbor_y < 1; ++ neighbor_y) { - for (coord_t neighbor_x = -1; neighbor_x < 1; ++ neighbor_x) { - // Range of fragment starts around grid_corner, close to pt. - auto range = m_map.equal_range(Vec2crd(grid_corner.x() + neighbor_x, grid_corner.y() + neighbor_y)); - // Find the map entry closest to pt. - for (auto it = range.first; it != range.second; ++it) { - const ValueType &value = it->second; - const Vec2crd *pt2 = m_point_accessor(value); - if (pt2 != nullptr) { - const double d2 = (pt - *pt2).cast().squaredNorm(); - if (d2 <= r2) - out.emplace_back(&value, d2); - } - } - } - } - return out; - } - -private: - using map_type = typename std::unordered_multimap; - PointAccessor m_point_accessor; - map_type m_map; - coord_t m_search_radius; - coord_t m_grid_resolution; - coord_t m_grid_log2; -}; - -std::ostream& operator<<(std::ostream &stm, const Vec2d &pointf); - - -// ///////////////////////////////////////////////////////////////////////////// -// Type safe conversions to and from scaled and unscaled coordinates -// ///////////////////////////////////////////////////////////////////////////// - -// Semantics are the following: -// Upscaling (scaled()): only from floating point types (or Vec) to either -// floating point or integer 'scaled coord' coordinates. -// Downscaling (unscaled()): from arithmetic (or Vec) to floating point only - -// Conversion definition from unscaled to floating point scaled -template> -inline constexpr FloatingOnly scaled(const Tin &v) noexcept -{ - return Tout(v / Tin(SCALING_FACTOR)); -} - -// Conversion definition from unscaled to integer 'scaled coord'. -// TODO: is the rounding necessary? Here it is commented out to show that -// it can be different for integers but it does not have to be. Using -// std::round means loosing noexcept and constexpr modifiers -template> -inline constexpr ScaledCoordOnly scaled(const Tin &v) noexcept -{ - //return static_cast(std::round(v / SCALING_FACTOR)); - return Tout(v / Tin(SCALING_FACTOR)); -} - -// Conversion for Eigen vectors (N dimensional points) -template, - int...EigenArgs> -inline Eigen::Matrix, N, EigenArgs...> -scaled(const Eigen::Matrix &v) -{ - return (v / SCALING_FACTOR).template cast(); -} - -// Conversion from arithmetic scaled type to floating point unscaled -template, - class = FloatingOnly> -inline constexpr Tout unscaled(const Tin &v) noexcept -{ - return Tout(v) * Tout(SCALING_FACTOR); -} - -// Unscaling for Eigen vectors. Input base type can be arithmetic, output base -// type can only be floating point. -template, - class = FloatingOnly, - int...EigenArgs> -inline constexpr Eigen::Matrix -unscaled(const Eigen::Matrix &v) noexcept -{ - return v.template cast() * Tout(SCALING_FACTOR); -} - -// Align a coordinate to a grid. The coordinate may be negative, -// the aligned value will never be bigger than the original one. -inline coord_t align_to_grid(const coord_t coord, const coord_t spacing) { - // Current C++ standard defines the result of integer division to be rounded to zero, - // for both positive and negative numbers. Here we want to round down for negative - // numbers as well. - coord_t aligned = (coord < 0) ? - ((coord - spacing + 1) / spacing) * spacing : - (coord / spacing) * spacing; - assert(aligned <= coord); - return aligned; -} -inline Point align_to_grid(Point coord, Point spacing) - { return Point(align_to_grid(coord.x(), spacing.x()), align_to_grid(coord.y(), spacing.y())); } -inline coord_t align_to_grid(coord_t coord, coord_t spacing, coord_t base) - { return base + align_to_grid(coord - base, spacing); } -inline Point align_to_grid(Point coord, Point spacing, Point base) - { return Point(align_to_grid(coord.x(), spacing.x(), base.x()), align_to_grid(coord.y(), spacing.y(), base.y())); } - -} // namespace Slic3r - -// start Boost -#include -#include -namespace boost { namespace polygon { - template <> - struct geometry_concept { using type = point_concept; }; - - template <> - struct point_traits { - using coordinate_type = coord_t; - - static inline coordinate_type get(const Slic3r::Point& point, orientation_2d orient) { - return static_cast(point((orient == HORIZONTAL) ? 0 : 1)); - } - }; - - template <> - struct point_mutable_traits { - using coordinate_type = coord_t; - static inline void set(Slic3r::Point& point, orientation_2d orient, coord_t value) { - point((orient == HORIZONTAL) ? 0 : 1) = value; - } - static inline Slic3r::Point construct(coord_t x_value, coord_t y_value) { - return Slic3r::Point(x_value, y_value); - } - }; -} } -// end Boost - -// Serialization through the Cereal library -namespace cereal { -// template void serialize(Archive& archive, Slic3r::Vec2crd &v) { archive(v.x(), v.y()); } -// template void serialize(Archive& archive, Slic3r::Vec3crd &v) { archive(v.x(), v.y(), v.z()); } - template void serialize(Archive& archive, Slic3r::Vec2i &v) { archive(v.x(), v.y()); } - template void serialize(Archive& archive, Slic3r::Vec3i &v) { archive(v.x(), v.y(), v.z()); } -// template void serialize(Archive& archive, Slic3r::Vec2i64 &v) { archive(v.x(), v.y()); } -// template void serialize(Archive& archive, Slic3r::Vec3i64 &v) { archive(v.x(), v.y(), v.z()); } - template void serialize(Archive& archive, Slic3r::Vec2f &v) { archive(v.x(), v.y()); } - template void serialize(Archive& archive, Slic3r::Vec3f &v) { archive(v.x(), v.y(), v.z()); } - template void serialize(Archive& archive, Slic3r::Vec2d &v) { archive(v.x(), v.y()); } - template void serialize(Archive& archive, Slic3r::Vec3d &v) { archive(v.x(), v.y(), v.z()); } - - template void load(Archive& archive, Slic3r::Matrix2f &m) { archive.loadBinary((char*)m.data(), sizeof(float) * 4); } - template void save(Archive& archive, Slic3r::Matrix2f &m) { archive.saveBinary((char*)m.data(), sizeof(float) * 4); } -} - -// To be able to use Vec<> and Mat<> in range based for loops: -namespace Eigen { -template -T* begin(Slic3r::Mat &mat) { return mat.data(); } - -template -T* end(Slic3r::Mat &mat) { return mat.data() + N * M; } - -template -const T* begin(const Slic3r::Mat &mat) { return mat.data(); } - -template -const T* end(const Slic3r::Mat &mat) { return mat.data() + N * M; } -} // namespace Eigen - -#endif +#ifndef slic3r_Point_hpp_ +#define slic3r_Point_hpp_ + +#include "libslic3r.h" +#include +#include +#include +#include +#include +#include + +#include + +#include "LocalesUtils.hpp" + +namespace Slic3r { + +class BoundingBox; +class BoundingBoxf; +class Line; +class MultiPoint; +class Point; +using Vector = Point; + +// Base template for eigen derived vectors +template +using Mat = Eigen::Matrix; + +template using Vec = Mat; + +template +using DynVec = Eigen::Matrix; + +// Eigen types, to replace the Slic3r's own types in the future. +// Vector types with a fixed point coordinate base type. +using Vec2crd = Eigen::Matrix; +using Vec3crd = Eigen::Matrix; +using Vec2i = Eigen::Matrix; +using Vec3i = Eigen::Matrix; +using Vec4i = Eigen::Matrix; +using Vec2i32 = Eigen::Matrix; +using Vec2i64 = Eigen::Matrix; +using Vec3i32 = Eigen::Matrix; +using Vec3i64 = Eigen::Matrix; + +// Vector types with a double coordinate base type. +using Vec2f = Eigen::Matrix; +using Vec3f = Eigen::Matrix; +using Vec2d = Eigen::Matrix; +using Vec3d = Eigen::Matrix; + +using Points = std::vector; +using PointPtrs = std::vector; +using PointConstPtrs = std::vector; +using Points3 = std::vector; +using Pointfs = std::vector; +using Vec2ds = std::vector; +using Pointf3s = std::vector; + +using Matrix2f = Eigen::Matrix; +using Matrix2d = Eigen::Matrix; +using Matrix3f = Eigen::Matrix; +using Matrix3d = Eigen::Matrix; +using Matrix4f = Eigen::Matrix; +using Matrix4d = Eigen::Matrix; + +template +using Transform = Eigen::Transform; + +using Transform2f = Eigen::Transform; +using Transform2d = Eigen::Transform; +using Transform3f = Eigen::Transform; +using Transform3d = Eigen::Transform; + +// I don't know why Eigen::Transform::Identity() return a const object... +template Transform identity() { return Transform::Identity(); } +inline const auto &identity3f = identity<3, float>; +inline const auto &identity3d = identity<3, double>; + +inline bool operator<(const Vec2d &lhs, const Vec2d &rhs) { return lhs.x() < rhs.x() || (lhs.x() == rhs.x() && lhs.y() < rhs.y()); } + +// Cross product of two 2D vectors. +// None of the vectors may be of int32_t type as the result would overflow. +template +inline typename Derived::Scalar cross2(const Eigen::MatrixBase &v1, const Eigen::MatrixBase &v2) +{ + static_assert(Derived::IsVectorAtCompileTime && int(Derived::SizeAtCompileTime) == 2, "cross2(): first parameter is not a 2D vector"); + static_assert(Derived2::IsVectorAtCompileTime && int(Derived2::SizeAtCompileTime) == 2, "cross2(): first parameter is not a 2D vector"); + static_assert(! std::is_same::value, "cross2(): Scalar type must not be int32_t, otherwise the cross product would overflow."); + static_assert(std::is_same::value, "cross2(): Scalar types of 1st and 2nd operand must be equal."); + return v1.x() * v2.y() - v1.y() * v2.x(); +} + +// 2D vector perpendicular to the argument. +template +inline Eigen::Matrix perp(const Eigen::MatrixBase &v) +{ + static_assert(Derived::IsVectorAtCompileTime && int(Derived::SizeAtCompileTime) == 2, "perp(): parameter is not a 2D vector"); + return { - v.y(), v.x() }; +} + +// Angle from v1 to v2, returning double atan2(y, x) normalized to <-PI, PI>. +template +inline double angle(const Eigen::MatrixBase &v1, const Eigen::MatrixBase &v2) { + static_assert(Derived::IsVectorAtCompileTime && int(Derived::SizeAtCompileTime) == 2, "angle(): first parameter is not a 2D vector"); + static_assert(Derived2::IsVectorAtCompileTime && int(Derived2::SizeAtCompileTime) == 2, "angle(): second parameter is not a 2D vector"); + auto v1d = v1.template cast(); + auto v2d = v2.template cast(); + return atan2(cross2(v1d, v2d), v1d.dot(v2d)); +} + +template +Eigen::Matrix to_2d(const Eigen::MatrixBase> &ptN) { return { ptN.x(), ptN.y() }; } + +template +Eigen::Matrix to_3d(const Eigen::MatrixBase> & pt, const T z) { return { pt.x(), pt.y(), z }; } + +inline Vec2d unscale(coord_t x, coord_t y) { return Vec2d(unscale(x), unscale(y)); } +inline Vec2d unscale(const Vec2crd &pt) { return Vec2d(unscale(pt.x()), unscale(pt.y())); } +inline Vec2d unscale(const Vec2d &pt) { return Vec2d(unscale(pt.x()), unscale(pt.y())); } +inline Vec3d unscale(coord_t x, coord_t y, coord_t z) { return Vec3d(unscale(x), unscale(y), unscale(z)); } +inline Vec3d unscale(const Vec3crd &pt) { return Vec3d(unscale(pt.x()), unscale(pt.y()), unscale(pt.z())); } +inline Vec3d unscale(const Vec3d &pt) { return Vec3d(unscale(pt.x()), unscale(pt.y()), unscale(pt.z())); } + +inline std::string to_string(const Vec2crd &pt) { return std::string("[") + float_to_string_decimal_point(pt.x()) + ", " + float_to_string_decimal_point(pt.y()) + "]"; } +inline std::string to_string(const Vec2d &pt) { return std::string("[") + float_to_string_decimal_point(pt.x()) + ", " + float_to_string_decimal_point(pt.y()) + "]"; } +inline std::string to_string(const Vec3crd &pt) { return std::string("[") + float_to_string_decimal_point(pt.x()) + ", " + float_to_string_decimal_point(pt.y()) + ", " + float_to_string_decimal_point(pt.z()) + "]"; } +inline std::string to_string(const Vec3d &pt) { return std::string("[") + float_to_string_decimal_point(pt.x()) + ", " + float_to_string_decimal_point(pt.y()) + ", " + float_to_string_decimal_point(pt.z()) + "]"; } + +std::vector transform(const std::vector& points, const Transform3f& t); +Pointf3s transform(const Pointf3s& points, const Transform3d& t); + +template using Vec = Eigen::Matrix; + +class Point : public Vec2crd +{ +public: + using coord_type = coord_t; + + Point() : Vec2crd(0, 0) {} + Point(int32_t x, int32_t y) : Vec2crd(coord_t(x), coord_t(y)) {} + Point(int64_t x, int64_t y) : Vec2crd(coord_t(x), coord_t(y)) {} + Point(double x, double y) : Vec2crd(coord_t(lrint(x)), coord_t(lrint(y))) {} + Point(const Point &rhs) { *this = rhs; } + explicit Point(const Vec2d& rhs) : Vec2crd(coord_t(lrint(rhs.x())), coord_t(lrint(rhs.y()))) {} + // This constructor allows you to construct Point from Eigen expressions + template + Point(const Eigen::MatrixBase &other) : Vec2crd(other) {} + static Point new_scale(coordf_t x, coordf_t y) { return Point(coord_t(scale_(x)), coord_t(scale_(y))); } + static Point new_scale(const Vec2d &v) { return Point(coord_t(scale_(v.x())), coord_t(scale_(v.y()))); } + static Point new_scale(const Vec2f &v) { return Point(coord_t(scale_(v.x())), coord_t(scale_(v.y()))); } + + // This method allows you to assign Eigen expressions to MyVectorType + template + Point& operator=(const Eigen::MatrixBase &other) + { + this->Vec2crd::operator=(other); + return *this; + } + + Point& operator+=(const Point& rhs) { this->x() += rhs.x(); this->y() += rhs.y(); return *this; } + Point& operator-=(const Point& rhs) { this->x() -= rhs.x(); this->y() -= rhs.y(); return *this; } + Point& operator*=(const double &rhs) { this->x() = coord_t(this->x() * rhs); this->y() = coord_t(this->y() * rhs); return *this; } + Point operator*(const double &rhs) { return Point(this->x() * rhs, this->y() * rhs); } + + void rotate(double angle) { this->rotate(std::cos(angle), std::sin(angle)); } + void rotate(double cos_a, double sin_a) { + double cur_x = (double)this->x(); + double cur_y = (double)this->y(); + this->x() = (coord_t)round(cos_a * cur_x - sin_a * cur_y); + this->y() = (coord_t)round(cos_a * cur_y + sin_a * cur_x); + } + + void rotate(double angle, const Point ¢er); + Point rotated(double angle) const { Point res(*this); res.rotate(angle); return res; } + Point rotated(double cos_a, double sin_a) const { Point res(*this); res.rotate(cos_a, sin_a); return res; } + Point rotated(double angle, const Point ¢er) const { Point res(*this); res.rotate(angle, center); return res; } + int nearest_point_index(const Points &points) const; + int nearest_point_index(const PointConstPtrs &points) const; + int nearest_point_index(const PointPtrs &points) const; + bool nearest_point(const Points &points, Point* point) const; + Point projection_onto(const MultiPoint &poly) const; + Point projection_onto(const Line &line) const; +}; + +inline bool operator<(const Point &l, const Point &r) +{ + return l.x() < r.x() || (l.x() == r.x() && l.y() < r.y()); +} + +inline Point operator* (const Point& l, const double &r) +{ + return {coord_t(l.x() * r), coord_t(l.y() * r)}; +} + +inline bool is_approx(const Point &p1, const Point &p2, coord_t epsilon = coord_t(SCALED_EPSILON)) +{ + Point d = (p2 - p1).cwiseAbs(); + return d.x() < epsilon && d.y() < epsilon; +} + +inline bool is_approx(const Vec2f &p1, const Vec2f &p2, float epsilon = float(EPSILON)) +{ + Vec2f d = (p2 - p1).cwiseAbs(); + return d.x() < epsilon && d.y() < epsilon; +} + +inline bool is_approx(const Vec2d &p1, const Vec2d &p2, double epsilon = EPSILON) +{ + Vec2d d = (p2 - p1).cwiseAbs(); + return d.x() < epsilon && d.y() < epsilon; +} + +inline bool is_approx(const Vec3f &p1, const Vec3f &p2, float epsilon = float(EPSILON)) +{ + Vec3f d = (p2 - p1).cwiseAbs(); + return d.x() < epsilon && d.y() < epsilon && d.z() < epsilon; +} + +inline bool is_approx(const Vec3d &p1, const Vec3d &p2, double epsilon = EPSILON) +{ + Vec3d d = (p2 - p1).cwiseAbs(); + return d.x() < epsilon && d.y() < epsilon && d.z() < epsilon; +} + +inline Point lerp(const Point &a, const Point &b, double t) +{ + assert((t >= -EPSILON) && (t <= 1. + EPSILON)); + return ((1. - t) * a.cast() + t * b.cast()).cast(); +} + +BoundingBox get_extents(const Points &pts); +BoundingBox get_extents(const std::vector &pts); +BoundingBoxf get_extents(const std::vector &pts); + +// Test for duplicate points in a vector of points. +// The points are copied, sorted and checked for duplicates globally. +bool has_duplicate_points(std::vector &&pts); +inline bool has_duplicate_points(const std::vector &pts) +{ + std::vector cpy = pts; + return has_duplicate_points(std::move(cpy)); +} + +// Test for duplicate points in a vector of points. +// Only successive points are checked for equality. +inline bool has_duplicate_successive_points(const std::vector &pts) +{ + for (size_t i = 1; i < pts.size(); ++ i) + if (pts[i - 1] == pts[i]) + return true; + return false; +} + +// Test for duplicate points in a vector of points. +// Only successive points are checked for equality. Additionally, first and last points are compared for equality. +inline bool has_duplicate_successive_points_closed(const std::vector &pts) +{ + return has_duplicate_successive_points(pts) || (pts.size() >= 2 && pts.front() == pts.back()); +} + +namespace int128 { + // Exact orientation predicate, + // returns +1: CCW, 0: collinear, -1: CW. + int orient(const Vec2crd &p1, const Vec2crd &p2, const Vec2crd &p3); + // Exact orientation predicate, + // returns +1: CCW, 0: collinear, -1: CW. + int cross(const Vec2crd &v1, const Vec2crd &v2); +} + +// To be used by std::unordered_map, std::unordered_multimap and friends. +struct PointHash { + size_t operator()(const Vec2crd &pt) const { + return coord_t((89 * 31 + int64_t(pt.x())) * 31 + pt.y()); + } +}; + +// A generic class to search for a closest Point in a given radius. +// It uses std::unordered_multimap to implement an efficient 2D spatial hashing. +// The PointAccessor has to return const Point*. +// If a nullptr is returned, it is ignored by the query. +template class ClosestPointInRadiusLookup +{ +public: + ClosestPointInRadiusLookup(coord_t search_radius, PointAccessor point_accessor = PointAccessor()) : + m_search_radius(search_radius), m_point_accessor(point_accessor), m_grid_log2(0) + { + // Resolution of a grid, twice the search radius + some epsilon. + coord_t gridres = 2 * m_search_radius + 4; + m_grid_resolution = gridres; + assert(m_grid_resolution > 0); + assert(m_grid_resolution < (coord_t(1) << 30)); + // Compute m_grid_log2 = log2(m_grid_resolution) + if (m_grid_resolution > 32767) { + m_grid_resolution >>= 16; + m_grid_log2 += 16; + } + if (m_grid_resolution > 127) { + m_grid_resolution >>= 8; + m_grid_log2 += 8; + } + if (m_grid_resolution > 7) { + m_grid_resolution >>= 4; + m_grid_log2 += 4; + } + if (m_grid_resolution > 1) { + m_grid_resolution >>= 2; + m_grid_log2 += 2; + } + if (m_grid_resolution > 0) + ++ m_grid_log2; + m_grid_resolution = 1 << m_grid_log2; + assert(m_grid_resolution >= gridres); + assert(gridres > m_grid_resolution / 2); + } + + void insert(const ValueType &value) { + const Vec2crd *pt = m_point_accessor(value); + if (pt != nullptr) + m_map.emplace(std::make_pair(Vec2crd(pt->x()>>m_grid_log2, pt->y()>>m_grid_log2), value)); + } + + void insert(ValueType &&value) { + const Vec2crd *pt = m_point_accessor(value); + if (pt != nullptr) + m_map.emplace(std::make_pair(Vec2crd(pt->x()>>m_grid_log2, pt->y()>>m_grid_log2), std::move(value))); + } + + // Erase a data point equal to value. (ValueType has to declare the operator==). + // Returns true if the data point equal to value was found and removed. + bool erase(const ValueType &value) { + const Point *pt = m_point_accessor(value); + if (pt != nullptr) { + // Range of fragment starts around grid_corner, close to pt. + auto range = m_map.equal_range(Point((*pt).x()>>m_grid_log2, (*pt).y()>>m_grid_log2)); + // Remove the first item. + for (auto it = range.first; it != range.second; ++ it) { + if (it->second == value) { + m_map.erase(it); + return true; + } + } + } + return false; + } + + // Return a pair of + std::pair find(const Vec2crd &pt) { + // Iterate over 4 closest grid cells around pt, + // find the closest start point inside these cells to pt. + const ValueType *value_min = nullptr; + double dist_min = std::numeric_limits::max(); + // Round pt to a closest grid_cell corner. + Vec2crd grid_corner((pt.x()+(m_grid_resolution>>1))>>m_grid_log2, (pt.y()+(m_grid_resolution>>1))>>m_grid_log2); + // For four neighbors of grid_corner: + for (coord_t neighbor_y = -1; neighbor_y < 1; ++ neighbor_y) { + for (coord_t neighbor_x = -1; neighbor_x < 1; ++ neighbor_x) { + // Range of fragment starts around grid_corner, close to pt. + auto range = m_map.equal_range(Vec2crd(grid_corner.x() + neighbor_x, grid_corner.y() + neighbor_y)); + // Find the map entry closest to pt. + for (auto it = range.first; it != range.second; ++it) { + const ValueType &value = it->second; + const Vec2crd *pt2 = m_point_accessor(value); + if (pt2 != nullptr) { + const double d2 = (pt - *pt2).cast().squaredNorm(); + if (d2 < dist_min) { + dist_min = d2; + value_min = &value; + } + } + } + } + } + return (value_min != nullptr && dist_min < coordf_t(m_search_radius) * coordf_t(m_search_radius)) ? + std::make_pair(value_min, dist_min) : + std::make_pair(nullptr, std::numeric_limits::max()); + } + + // Returns all pairs of values and squared distances. + std::vector> find_all(const Vec2crd &pt) { + // Iterate over 4 closest grid cells around pt, + // Round pt to a closest grid_cell corner. + Vec2crd grid_corner((pt.x()+(m_grid_resolution>>1))>>m_grid_log2, (pt.y()+(m_grid_resolution>>1))>>m_grid_log2); + // For four neighbors of grid_corner: + std::vector> out; + const double r2 = double(m_search_radius) * m_search_radius; + for (coord_t neighbor_y = -1; neighbor_y < 1; ++ neighbor_y) { + for (coord_t neighbor_x = -1; neighbor_x < 1; ++ neighbor_x) { + // Range of fragment starts around grid_corner, close to pt. + auto range = m_map.equal_range(Vec2crd(grid_corner.x() + neighbor_x, grid_corner.y() + neighbor_y)); + // Find the map entry closest to pt. + for (auto it = range.first; it != range.second; ++it) { + const ValueType &value = it->second; + const Vec2crd *pt2 = m_point_accessor(value); + if (pt2 != nullptr) { + const double d2 = (pt - *pt2).cast().squaredNorm(); + if (d2 <= r2) + out.emplace_back(&value, d2); + } + } + } + } + return out; + } + +private: + using map_type = typename std::unordered_multimap; + PointAccessor m_point_accessor; + map_type m_map; + coord_t m_search_radius; + coord_t m_grid_resolution; + coord_t m_grid_log2; +}; + +std::ostream& operator<<(std::ostream &stm, const Vec2d &pointf); + + +// ///////////////////////////////////////////////////////////////////////////// +// Type safe conversions to and from scaled and unscaled coordinates +// ///////////////////////////////////////////////////////////////////////////// + +// Semantics are the following: +// Upscaling (scaled()): only from floating point types (or Vec) to either +// floating point or integer 'scaled coord' coordinates. +// Downscaling (unscaled()): from arithmetic (or Vec) to floating point only + +// Conversion definition from unscaled to floating point scaled +template> +inline constexpr FloatingOnly scaled(const Tin &v) noexcept +{ + return Tout(v / Tin(SCALING_FACTOR)); +} + +// Conversion definition from unscaled to integer 'scaled coord'. +// TODO: is the rounding necessary? Here it is commented out to show that +// it can be different for integers but it does not have to be. Using +// std::round means loosing noexcept and constexpr modifiers +template> +inline constexpr ScaledCoordOnly scaled(const Tin &v) noexcept +{ + //return static_cast(std::round(v / SCALING_FACTOR)); + return Tout(v / Tin(SCALING_FACTOR)); +} + +// Conversion for Eigen vectors (N dimensional points) +template, + int...EigenArgs> +inline Eigen::Matrix, N, EigenArgs...> +scaled(const Eigen::Matrix &v) +{ + return (v / SCALING_FACTOR).template cast(); +} + +// Conversion from arithmetic scaled type to floating point unscaled +template, + class = FloatingOnly> +inline constexpr Tout unscaled(const Tin &v) noexcept +{ + return Tout(v) * Tout(SCALING_FACTOR); +} + +// Unscaling for Eigen vectors. Input base type can be arithmetic, output base +// type can only be floating point. +template, + class = FloatingOnly, + int...EigenArgs> +inline constexpr Eigen::Matrix +unscaled(const Eigen::Matrix &v) noexcept +{ + return v.template cast() * Tout(SCALING_FACTOR); +} + +// Align a coordinate to a grid. The coordinate may be negative, +// the aligned value will never be bigger than the original one. +inline coord_t align_to_grid(const coord_t coord, const coord_t spacing) { + // Current C++ standard defines the result of integer division to be rounded to zero, + // for both positive and negative numbers. Here we want to round down for negative + // numbers as well. + coord_t aligned = (coord < 0) ? + ((coord - spacing + 1) / spacing) * spacing : + (coord / spacing) * spacing; + assert(aligned <= coord); + return aligned; +} +inline Point align_to_grid(Point coord, Point spacing) + { return Point(align_to_grid(coord.x(), spacing.x()), align_to_grid(coord.y(), spacing.y())); } +inline coord_t align_to_grid(coord_t coord, coord_t spacing, coord_t base) + { return base + align_to_grid(coord - base, spacing); } +inline Point align_to_grid(Point coord, Point spacing, Point base) + { return Point(align_to_grid(coord.x(), spacing.x(), base.x()), align_to_grid(coord.y(), spacing.y(), base.y())); } + +} // namespace Slic3r + +// start Boost +#include +#include +namespace boost { namespace polygon { + template <> + struct geometry_concept { using type = point_concept; }; + + template <> + struct point_traits { + using coordinate_type = coord_t; + + static inline coordinate_type get(const Slic3r::Point& point, orientation_2d orient) { + return static_cast(point((orient == HORIZONTAL) ? 0 : 1)); + } + }; + + template <> + struct point_mutable_traits { + using coordinate_type = coord_t; + static inline void set(Slic3r::Point& point, orientation_2d orient, coord_t value) { + point((orient == HORIZONTAL) ? 0 : 1) = value; + } + static inline Slic3r::Point construct(coord_t x_value, coord_t y_value) { + return Slic3r::Point(x_value, y_value); + } + }; +} } +// end Boost + +// Serialization through the Cereal library +namespace cereal { +// template void serialize(Archive& archive, Slic3r::Vec2crd &v) { archive(v.x(), v.y()); } +// template void serialize(Archive& archive, Slic3r::Vec3crd &v) { archive(v.x(), v.y(), v.z()); } + template void serialize(Archive& archive, Slic3r::Vec2i &v) { archive(v.x(), v.y()); } + template void serialize(Archive& archive, Slic3r::Vec3i &v) { archive(v.x(), v.y(), v.z()); } +// template void serialize(Archive& archive, Slic3r::Vec2i64 &v) { archive(v.x(), v.y()); } +// template void serialize(Archive& archive, Slic3r::Vec3i64 &v) { archive(v.x(), v.y(), v.z()); } + template void serialize(Archive& archive, Slic3r::Vec2f &v) { archive(v.x(), v.y()); } + template void serialize(Archive& archive, Slic3r::Vec3f &v) { archive(v.x(), v.y(), v.z()); } + template void serialize(Archive& archive, Slic3r::Vec2d &v) { archive(v.x(), v.y()); } + template void serialize(Archive& archive, Slic3r::Vec3d &v) { archive(v.x(), v.y(), v.z()); } + + template void load(Archive& archive, Slic3r::Matrix2f &m) { archive.loadBinary((char*)m.data(), sizeof(float) * 4); } + template void save(Archive& archive, Slic3r::Matrix2f &m) { archive.saveBinary((char*)m.data(), sizeof(float) * 4); } +#if ENABLE_WORLD_COORDINATE + template void load(Archive& archive, Slic3r::Transform3d& m) { archive.loadBinary((char*)m.data(), sizeof(double) * 16); } + template void save(Archive& archive, const Slic3r::Transform3d& m) { archive.saveBinary((char*)m.data(), sizeof(double) * 16); } +#endif // ENABLE_WORLD_COORDINATE +} + +// To be able to use Vec<> and Mat<> in range based for loops: +namespace Eigen { +template +T* begin(Slic3r::Mat &mat) { return mat.data(); } + +template +T* end(Slic3r::Mat &mat) { return mat.data() + N * M; } + +template +const T* begin(const Slic3r::Mat &mat) { return mat.data(); } + +template +const T* end(const Slic3r::Mat &mat) { return mat.data() + N * M; } +} // namespace Eigen + +#endif diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp index 85bcd69ba..e744d72ca 100644 --- a/src/libslic3r/PresetBundle.cpp +++ b/src/libslic3r/PresetBundle.cpp @@ -1,7 +1,7 @@ #include -#include "PresetBundle.hpp" #include "libslic3r.h" +#include "PresetBundle.hpp" #include "Utils.hpp" #include "Model.hpp" #include "format.hpp" @@ -453,6 +453,11 @@ void PresetBundle::save_changes_for_preset(const std::string& new_name, Preset:: presets.get_edited_preset().config.apply_only(presets.get_selected_preset().config, unselected_options); } +#if ENABLE_COPY_CUSTOM_BED_MODEL_AND_TEXTURE + if (type == Preset::TYPE_PRINTER) + copy_bed_model_and_texture_if_needed(presets.get_edited_preset().config); +#endif // ENABLE_COPY_CUSTOM_BED_MODEL_AND_TEXTURE + // Save the preset into Slic3r::data_dir / presets / section_name / preset_name.ini presets.save_current_preset(new_name); // Mark the print & filament enabled if they are compatible with the currently selected preset. @@ -1860,4 +1865,32 @@ void PresetBundle::set_default_suppressed(bool default_suppressed) printers.set_default_suppressed(default_suppressed); } +#if ENABLE_COPY_CUSTOM_BED_MODEL_AND_TEXTURE +void copy_bed_model_and_texture_if_needed(DynamicPrintConfig& config) +{ + const boost::filesystem::path user_dir = boost::filesystem::absolute(boost::filesystem::path(data_dir()) / "printer").make_preferred(); + const boost::filesystem::path res_dir = boost::filesystem::absolute(boost::filesystem::path(resources_dir()) / "profiles").make_preferred(); + + auto do_copy = [&user_dir, &res_dir](ConfigOptionString* cfg, const std::string& type) { + if (cfg == nullptr || cfg->value.empty()) + return; + + const boost::filesystem::path src_dir = boost::filesystem::absolute(boost::filesystem::path(cfg->value)).make_preferred().parent_path(); + if (src_dir != user_dir && src_dir.parent_path() != res_dir) { + const std::string dst_value = (user_dir / boost::filesystem::path(cfg->value).filename()).string(); + std::string error; + if (copy_file_inner(cfg->value, dst_value, error) == SUCCESS) + cfg->value = dst_value; + else { + BOOST_LOG_TRIVIAL(error) << "Copying from " << cfg->value << " to " << dst_value << " failed. Unable to set custom bed " << type << ". [" << error << "]"; + cfg->value = ""; + } + } + }; + + do_copy(config.option("bed_custom_texture"), "texture"); + do_copy(config.option("bed_custom_model"), "model"); +} +#endif // ENABLE_COPY_CUSTOM_BED_MODEL_AND_TEXTURE + } // namespace Slic3r diff --git a/src/libslic3r/PresetBundle.hpp b/src/libslic3r/PresetBundle.hpp index 2a5ce6839..b36f6ed6e 100644 --- a/src/libslic3r/PresetBundle.hpp +++ b/src/libslic3r/PresetBundle.hpp @@ -178,6 +178,12 @@ private: ENABLE_ENUM_BITMASK_OPERATORS(PresetBundle::LoadConfigBundleAttribute) +#if ENABLE_COPY_CUSTOM_BED_MODEL_AND_TEXTURE +// Copies bed texture and model files to 'data_dir()\printer' folder, if needed +// and updates the config accordingly +extern void copy_bed_model_and_texture_if_needed(DynamicPrintConfig& config); +#endif // ENABLE_COPY_CUSTOM_BED_MODEL_AND_TEXTURE + } // namespace Slic3r #endif /* slic3r_PresetBundle_hpp_ */ diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index e88f9093c..49fbec6b1 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -13,6 +13,7 @@ #include "GCode.hpp" #include "GCode/WipeTower.hpp" #include "Utils.hpp" +#include "BuildVolume.hpp" #include @@ -387,7 +388,7 @@ bool Print::sequential_print_horizontal_clearance_valid(const Print& print, Poly Geometry::assemble_transform({ 0.0, 0.0, model_instance0->get_offset().z() }, model_instance0->get_rotation(), model_instance0->get_scaling_factor(), model_instance0->get_mirror())), // Shrink the extruder_clearance_radius a tiny bit, so that if the object arrangement algorithm placed the objects // exactly by satisfying the extruder_clearance_radius, this test will not trigger collision. - float(scale_(0.5 * print.config().extruder_clearance_radius.value - EPSILON)), + float(scale_(0.5 * print.config().extruder_clearance_radius.value - BuildVolume::BedEpsilon)), jtRound, scale_(0.1)).front()); } // Make a copy, so it may be rotated for instances. diff --git a/src/libslic3r/PrintApply.cpp b/src/libslic3r/PrintApply.cpp index 34e3129d9..4c085728c 100644 --- a/src/libslic3r/PrintApply.cpp +++ b/src/libslic3r/PrintApply.cpp @@ -1,1444 +1,1453 @@ -#include "Model.hpp" -#include "Print.hpp" - -#include - -namespace Slic3r { - -// Add or remove support modifier ModelVolumes from model_object_dst to match the ModelVolumes of model_object_new -// in the exact order and with the same IDs. -// It is expected, that the model_object_dst already contains the non-support volumes of model_object_new in the correct order. -// Friend to ModelVolume to allow copying. -// static is not accepted by gcc if declared as a friend of ModelObject. -/* static */ void model_volume_list_update_supports(ModelObject &model_object_dst, const ModelObject &model_object_new) -{ - typedef std::pair ModelVolumeWithStatus; - std::vector old_volumes; - old_volumes.reserve(model_object_dst.volumes.size()); - for (const ModelVolume *model_volume : model_object_dst.volumes) - old_volumes.emplace_back(ModelVolumeWithStatus(model_volume, false)); - auto model_volume_lower = [](const ModelVolumeWithStatus &mv1, const ModelVolumeWithStatus &mv2){ return mv1.first->id() < mv2.first->id(); }; - auto model_volume_equal = [](const ModelVolumeWithStatus &mv1, const ModelVolumeWithStatus &mv2){ return mv1.first->id() == mv2.first->id(); }; - std::sort(old_volumes.begin(), old_volumes.end(), model_volume_lower); - model_object_dst.volumes.clear(); - model_object_dst.volumes.reserve(model_object_new.volumes.size()); - for (const ModelVolume *model_volume_src : model_object_new.volumes) { - ModelVolumeWithStatus key(model_volume_src, false); - auto it = std::lower_bound(old_volumes.begin(), old_volumes.end(), key, model_volume_lower); - if (it != old_volumes.end() && model_volume_equal(*it, key)) { - // The volume was found in the old list. Just copy it. - assert(! it->second); // not consumed yet - it->second = true; - ModelVolume *model_volume_dst = const_cast(it->first); - // For support modifiers, the type may have been switched from blocker to enforcer and vice versa. - assert((model_volume_dst->is_support_modifier() && model_volume_src->is_support_modifier()) || model_volume_dst->type() == model_volume_src->type()); - model_object_dst.volumes.emplace_back(model_volume_dst); - if (model_volume_dst->is_support_modifier()) { - // For support modifiers, the type may have been switched from blocker to enforcer and vice versa. - model_volume_dst->set_type(model_volume_src->type()); - model_volume_dst->set_transformation(model_volume_src->get_transformation()); - } - assert(model_volume_dst->get_matrix().isApprox(model_volume_src->get_matrix())); - } else { - // The volume was not found in the old list. Create a new copy. - assert(model_volume_src->is_support_modifier()); - model_object_dst.volumes.emplace_back(new ModelVolume(*model_volume_src)); - model_object_dst.volumes.back()->set_model_object(&model_object_dst); - } - } - // Release the non-consumed old volumes (those were deleted from the new list). - for (ModelVolumeWithStatus &mv_with_status : old_volumes) - if (! mv_with_status.second) - delete mv_with_status.first; -} - -static inline void model_volume_list_copy_configs(ModelObject &model_object_dst, const ModelObject &model_object_src, const ModelVolumeType type) -{ - size_t i_src, i_dst; - for (i_src = 0, i_dst = 0; i_src < model_object_src.volumes.size() && i_dst < model_object_dst.volumes.size();) { - const ModelVolume &mv_src = *model_object_src.volumes[i_src]; - ModelVolume &mv_dst = *model_object_dst.volumes[i_dst]; - if (mv_src.type() != type) { - ++ i_src; - continue; - } - if (mv_dst.type() != type) { - ++ i_dst; - continue; - } - assert(mv_src.id() == mv_dst.id()); - // Copy the ModelVolume data. - mv_dst.name = mv_src.name; - mv_dst.config.assign_config(mv_src.config); - assert(mv_dst.supported_facets.id() == mv_src.supported_facets.id()); - mv_dst.supported_facets.assign(mv_src.supported_facets); - assert(mv_dst.seam_facets.id() == mv_src.seam_facets.id()); - mv_dst.seam_facets.assign(mv_src.seam_facets); - assert(mv_dst.mmu_segmentation_facets.id() == mv_src.mmu_segmentation_facets.id()); - mv_dst.mmu_segmentation_facets.assign(mv_src.mmu_segmentation_facets); - //FIXME what to do with the materials? - // mv_dst.m_material_id = mv_src.m_material_id; - ++ i_src; - ++ i_dst; - } -} - -static inline void layer_height_ranges_copy_configs(t_layer_config_ranges &lr_dst, const t_layer_config_ranges &lr_src) -{ - assert(lr_dst.size() == lr_src.size()); - auto it_src = lr_src.cbegin(); - for (auto &kvp_dst : lr_dst) { - const auto &kvp_src = *it_src ++; - assert(std::abs(kvp_dst.first.first - kvp_src.first.first ) <= EPSILON); - assert(std::abs(kvp_dst.first.second - kvp_src.first.second) <= EPSILON); - // Layer heights are allowed do differ in case the layer height table is being overriden by the smooth profile. - // assert(std::abs(kvp_dst.second.option("layer_height")->getFloat() - kvp_src.second.option("layer_height")->getFloat()) <= EPSILON); - kvp_dst.second = kvp_src.second; - } -} - -static inline bool transform3d_lower(const Transform3d &lhs, const Transform3d &rhs) -{ - typedef Transform3d::Scalar T; - const T *lv = lhs.data(); - const T *rv = rhs.data(); - for (size_t i = 0; i < 16; ++ i, ++ lv, ++ rv) { - if (*lv < *rv) - return true; - else if (*lv > *rv) - return false; - } - return false; -} - -static inline bool transform3d_equal(const Transform3d &lhs, const Transform3d &rhs) -{ - typedef Transform3d::Scalar T; - const T *lv = lhs.data(); - const T *rv = rhs.data(); - for (size_t i = 0; i < 16; ++ i, ++ lv, ++ rv) - if (*lv != *rv) - return false; - return true; -} - -struct PrintObjectTrafoAndInstances -{ - Transform3d trafo; - PrintInstances instances; - bool operator<(const PrintObjectTrafoAndInstances &rhs) const { return transform3d_lower(this->trafo, rhs.trafo); } -}; - -// Generate a list of trafos and XY offsets for instances of a ModelObject -static std::vector print_objects_from_model_object(const ModelObject &model_object) -{ - std::set trafos; - PrintObjectTrafoAndInstances trafo; - for (ModelInstance *model_instance : model_object.instances) - if (model_instance->is_printable()) { - trafo.trafo = model_instance->get_matrix(); - auto shift = Point::new_scale(trafo.trafo.data()[12], trafo.trafo.data()[13]); - // Reset the XY axes of the transformation. - trafo.trafo.data()[12] = 0; - trafo.trafo.data()[13] = 0; - // Search or insert a trafo. - auto it = trafos.emplace(trafo).first; - const_cast(*it).instances.emplace_back(PrintInstance{ nullptr, model_instance, shift }); - } - return std::vector(trafos.begin(), trafos.end()); -} - -// Compare just the layer ranges and their layer heights, not the associated configs. -// Ignore the layer heights if check_layer_heights is false. -static bool layer_height_ranges_equal(const t_layer_config_ranges &lr1, const t_layer_config_ranges &lr2, bool check_layer_height) -{ - if (lr1.size() != lr2.size()) - return false; - auto it2 = lr2.begin(); - for (const auto &kvp1 : lr1) { - const auto &kvp2 = *it2 ++; - if (std::abs(kvp1.first.first - kvp2.first.first ) > EPSILON || - std::abs(kvp1.first.second - kvp2.first.second) > EPSILON || - (check_layer_height && std::abs(kvp1.second.option("layer_height")->getFloat() - kvp2.second.option("layer_height")->getFloat()) > EPSILON)) - return false; - } - return true; -} - -// Returns true if va == vb when all CustomGCode items that are not ToolChangeCode are ignored. -static bool custom_per_printz_gcodes_tool_changes_differ(const std::vector &va, const std::vector &vb) -{ - auto it_a = va.begin(); - auto it_b = vb.begin(); - while (it_a != va.end() || it_b != vb.end()) { - if (it_a != va.end() && it_a->type != CustomGCode::ToolChange) { - // Skip any CustomGCode items, which are not tool changes. - ++ it_a; - continue; - } - if (it_b != vb.end() && it_b->type != CustomGCode::ToolChange) { - // Skip any CustomGCode items, which are not tool changes. - ++ it_b; - continue; - } - if (it_a == va.end() || it_b == vb.end()) - // va or vb contains more Tool Changes than the other. - return true; - assert(it_a->type == CustomGCode::ToolChange); - assert(it_b->type == CustomGCode::ToolChange); - if (*it_a != *it_b) - // The two Tool Changes differ. - return true; - ++ it_a; - ++ it_b; - } - // There is no change in custom Tool Changes. - return false; -} - -// Collect changes to print config, account for overrides of extruder retract values by filament presets. -static t_config_option_keys print_config_diffs( - const PrintConfig ¤t_config, - const DynamicPrintConfig &new_full_config, - DynamicPrintConfig &filament_overrides) -{ - const std::vector &extruder_retract_keys = print_config_def.extruder_retract_keys(); - const std::string filament_prefix = "filament_"; - t_config_option_keys print_diff; - for (const t_config_option_key &opt_key : current_config.keys()) { - const ConfigOption *opt_old = current_config.option(opt_key); - assert(opt_old != nullptr); - const ConfigOption *opt_new = new_full_config.option(opt_key); - // assert(opt_new != nullptr); - if (opt_new == nullptr) - //FIXME This may happen when executing some test cases. - continue; - const ConfigOption *opt_new_filament = std::binary_search(extruder_retract_keys.begin(), extruder_retract_keys.end(), opt_key) ? new_full_config.option(filament_prefix + opt_key) : nullptr; - if (opt_new_filament != nullptr && ! opt_new_filament->is_nil()) { - // An extruder retract override is available at some of the filament presets. - bool overriden = opt_new->overriden_by(opt_new_filament); - if (overriden || *opt_old != *opt_new) { - auto opt_copy = opt_new->clone(); - opt_copy->apply_override(opt_new_filament); - bool changed = *opt_old != *opt_copy; - if (changed) - print_diff.emplace_back(opt_key); - if (changed || overriden) { - // filament_overrides will be applied to the placeholder parser, which layers these parameters over full_print_config. - filament_overrides.set_key_value(opt_key, opt_copy); - } else - delete opt_copy; - } - } else if (*opt_new != *opt_old) - print_diff.emplace_back(opt_key); - } - - return print_diff; -} - -// Prepare for storing of the full print config into new_full_config to be exported into the G-code and to be used by the PlaceholderParser. -static t_config_option_keys full_print_config_diffs(const DynamicPrintConfig ¤t_full_config, const DynamicPrintConfig &new_full_config) -{ - t_config_option_keys full_config_diff; - for (const t_config_option_key &opt_key : new_full_config.keys()) { - const ConfigOption *opt_old = current_full_config.option(opt_key); - const ConfigOption *opt_new = new_full_config.option(opt_key); - if (opt_old == nullptr || *opt_new != *opt_old) - full_config_diff.emplace_back(opt_key); - } - return full_config_diff; -} - -// Repository for solving partial overlaps of ModelObject::layer_config_ranges. -// Here the const DynamicPrintConfig* point to the config in ModelObject::layer_config_ranges. -class LayerRanges -{ -public: - struct LayerRange { - t_layer_height_range layer_height_range; - // Config is owned by the associated ModelObject. - const DynamicPrintConfig* config { nullptr }; - - bool operator<(const LayerRange &rhs) const throw() { return this->layer_height_range < rhs.layer_height_range; } - }; - - LayerRanges() = default; - LayerRanges(const t_layer_config_ranges &in) { this->assign(in); } - - // Convert input config ranges into continuous non-overlapping sorted vector of intervals and their configs. - void assign(const t_layer_config_ranges &in) { - m_ranges.clear(); - m_ranges.reserve(in.size()); - // Input ranges are sorted lexicographically. First range trims the other ranges. - coordf_t last_z = 0; - for (const std::pair &range : in) - if (range.first.second > last_z) { - coordf_t min_z = std::max(range.first.first, 0.); - if (min_z > last_z + EPSILON) { - m_ranges.push_back({ t_layer_height_range(last_z, min_z) }); - last_z = min_z; - } - if (range.first.second > last_z + EPSILON) { - const DynamicPrintConfig *cfg = &range.second.get(); - m_ranges.push_back({ t_layer_height_range(last_z, range.first.second), cfg }); - last_z = range.first.second; - } - } - if (m_ranges.empty()) - m_ranges.push_back({ t_layer_height_range(0, DBL_MAX) }); - else if (m_ranges.back().config == nullptr) - m_ranges.back().layer_height_range.second = DBL_MAX; - else - m_ranges.push_back({ t_layer_height_range(m_ranges.back().layer_height_range.second, DBL_MAX) }); - } - - const DynamicPrintConfig* config(const t_layer_height_range &range) const { - auto it = std::lower_bound(m_ranges.begin(), m_ranges.end(), LayerRange{ { range.first - EPSILON, range.second - EPSILON } }); - // #ys_FIXME_COLOR - // assert(it != m_ranges.end()); - // assert(it == m_ranges.end() || std::abs(it->first.first - range.first ) < EPSILON); - // assert(it == m_ranges.end() || std::abs(it->first.second - range.second) < EPSILON); - if (it == m_ranges.end() || - std::abs(it->layer_height_range.first - range.first) > EPSILON || - std::abs(it->layer_height_range.second - range.second) > EPSILON ) - return nullptr; // desired range doesn't found - return it == m_ranges.end() ? nullptr : it->config; - } - - std::vector::const_iterator begin() const { return m_ranges.cbegin(); } - std::vector::const_iterator end () const { return m_ranges.cend(); } - size_t size () const { return m_ranges.size(); } - -private: - // Layer ranges with their config overrides and list of volumes with their snug bounding boxes in a given layer range. - std::vector m_ranges; -}; - -// To track Model / ModelObject updates between the front end and back end, including layer height ranges, their configs, -// and snug bounding boxes of ModelVolumes. -struct ModelObjectStatus { - enum Status { - Unknown, - Old, - New, - Moved, - Deleted, - }; - - enum class PrintObjectRegionsStatus { - Invalid, - Valid, - PartiallyValid, - }; - - ModelObjectStatus(ObjectID id, Status status = Unknown) : id(id), status(status) {} - ~ModelObjectStatus() { if (print_object_regions) print_object_regions->ref_cnt_dec(); } - - // Key of the set. - ObjectID id; - // Status of this ModelObject with id on apply(). - Status status; - // PrintObjects to be generated for this ModelObject including their base transformation. - std::vector print_instances; - // Regions shared by the associated PrintObjects. - PrintObjectRegions *print_object_regions { nullptr }; - // Status of the above. - PrintObjectRegionsStatus print_object_regions_status { PrintObjectRegionsStatus::Invalid }; - - // Search by id. - bool operator<(const ModelObjectStatus &rhs) const { return id < rhs.id; } -}; - -struct ModelObjectStatusDB -{ - void add(const ModelObject &model_object, const ModelObjectStatus::Status status) { - assert(db.find(ModelObjectStatus(model_object.id())) == db.end()); - db.emplace(model_object.id(), status); - } - - bool add_if_new(const ModelObject &model_object, const ModelObjectStatus::Status status) { - auto it = db.find(ModelObjectStatus(model_object.id())); - if (it == db.end()) { - db.emplace_hint(it, model_object.id(), status); - return true; - } - return false; - } - - const ModelObjectStatus& get(const ModelObject &model_object) { - auto it = db.find(ModelObjectStatus(model_object.id())); - assert(it != db.end()); - return *it; - } - - const ModelObjectStatus& reuse(const ModelObject &model_object) { - const ModelObjectStatus &result = this->get(model_object); - assert(result.status != ModelObjectStatus::Deleted); - return result; - } - - std::set db; -}; - -struct PrintObjectStatus { - enum Status { - Unknown, - Deleted, - Reused, - New - }; - - PrintObjectStatus(PrintObject *print_object, Status status = Unknown) : - id(print_object->model_object()->id()), - print_object(print_object), - trafo(print_object->trafo()), - status(status) {} - PrintObjectStatus(ObjectID id) : id(id), print_object(nullptr), trafo(Transform3d::Identity()), status(Unknown) {} - - // ID of the ModelObject & PrintObject - ObjectID id; - // Pointer to the old PrintObject - PrintObject *print_object; - // Trafo generated with model_object->world_matrix(true) - Transform3d trafo; - Status status; - - // Search by id. - bool operator<(const PrintObjectStatus &rhs) const { return id < rhs.id; } -}; - -class PrintObjectStatusDB { -public: - using iterator = std::multiset::iterator; - using const_iterator = std::multiset::const_iterator; - - PrintObjectStatusDB(const PrintObjectPtrs &print_objects) { - for (PrintObject *print_object : print_objects) - m_db.emplace(PrintObjectStatus(print_object)); - } - - struct iterator_range : std::pair - { - using std::pair::pair; - iterator_range(const std::pair in) : std::pair(in) {} - - const_iterator begin() throw() { return this->first; } - const_iterator end() throw() { return this->second; } - }; - - iterator_range get_range(const ModelObject &model_object) const { - return m_db.equal_range(PrintObjectStatus(model_object.id())); - } - - iterator_range get_range(const ModelObjectStatus &model_object_status) const { - return m_db.equal_range(PrintObjectStatus(model_object_status.id)); - } - - size_t count(const ModelObject &model_object) { - return m_db.count(PrintObjectStatus(model_object.id())); - } - - std::multiset::iterator begin() { return m_db.begin(); } - std::multiset::iterator end() { return m_db.end(); } - - void clear() { - m_db.clear(); - } - -private: - std::multiset m_db; -}; - -static inline bool model_volume_solid_or_modifier(const ModelVolume &mv) -{ - ModelVolumeType type = mv.type(); - return type == ModelVolumeType::MODEL_PART || type == ModelVolumeType::NEGATIVE_VOLUME || type == ModelVolumeType::PARAMETER_MODIFIER; -} - -static inline Transform3f trafo_for_bbox(const Transform3d &object_trafo, const Transform3d &volume_trafo) -{ - Transform3d m = object_trafo * volume_trafo; - m.translation().x() = 0.; - m.translation().y() = 0.; - return m.cast(); -} - -static inline bool trafos_differ_in_rotation_by_z_and_mirroring_by_xy_only(const Transform3d &t1, const Transform3d &t2) -{ - if (std::abs(t1.translation().z() - t2.translation().z()) > EPSILON) - // One of the object is higher than the other above the build plate (or below the build plate). - return false; - Matrix3d m1 = t1.matrix().block<3, 3>(0, 0); - Matrix3d m2 = t2.matrix().block<3, 3>(0, 0); - Matrix3d m = m2.inverse() * m1; - Vec3d z = m.block<3, 1>(0, 2); - if (std::abs(z.x()) > EPSILON || std::abs(z.y()) > EPSILON || std::abs(z.z() - 1.) > EPSILON) - // Z direction or length changed. - return false; - // Z still points in the same direction and it has the same length. - Vec3d x = m.block<3, 1>(0, 0); - Vec3d y = m.block<3, 1>(0, 1); - if (std::abs(x.z()) > EPSILON || std::abs(y.z()) > EPSILON) - return false; - double lx2 = x.squaredNorm(); - double ly2 = y.squaredNorm(); - if (lx2 - 1. > EPSILON * EPSILON || ly2 - 1. > EPSILON * EPSILON) - return false; - // Verify whether the vectors x, y are still perpendicular. - double d = x.dot(y); - return std::abs(d * d) < EPSILON * lx2 * ly2; -} - -static PrintObjectRegions::BoundingBox transformed_its_bbox2d(const indexed_triangle_set &its, const Transform3f &m, float offset) -{ - assert(! its.indices.empty()); - - PrintObjectRegions::BoundingBox bbox(m * its.vertices[its.indices.front()(0)]); - for (const stl_triangle_vertex_indices &tri : its.indices) - for (int i = 0; i < 3; ++ i) - bbox.extend(m * its.vertices[tri(i)]); - bbox.min() -= Vec3f(offset, offset, float(EPSILON)); - bbox.max() += Vec3f(offset, offset, float(EPSILON)); - return bbox; -} - -static void transformed_its_bboxes_in_z_ranges( - const indexed_triangle_set &its, - const Transform3f &m, - const std::vector &z_ranges, - std::vector> &bboxes, - const float offset) -{ - bboxes.assign(z_ranges.size(), std::make_pair(PrintObjectRegions::BoundingBox(), false)); - for (const stl_triangle_vertex_indices &tri : its.indices) { - const Vec3f pts[3] = { m * its.vertices[tri(0)], m * its.vertices[tri(1)], m * its.vertices[tri(2)] }; - for (size_t irange = 0; irange < z_ranges.size(); ++ irange) { - const t_layer_height_range &z_range = z_ranges[irange]; - std::pair &bbox = bboxes[irange]; - auto bbox_extend = [&bbox](const Vec3f& p) { - if (bbox.second) { - bbox.first.extend(p); - } else { - bbox.first.min() = bbox.first.max() = p; - bbox.second = true; - } - }; - int iprev = 2; - for (int iedge = 0; iedge < 3; ++ iedge) { - const Vec3f *p1 = &pts[iprev]; - const Vec3f *p2 = &pts[iedge]; - // Sort the edge points by Z. - if (p1->z() > p2->z()) - std::swap(p1, p2); - if (p2->z() <= z_range.first || p1->z() >= z_range.second) { - // Out of this slab. - } else if (p1->z() < z_range.first) { - if (p1->z() > z_range.second) { - // Two intersections. - float zspan = p2->z() - p1->z(); - float t1 = (z_range.first - p1->z()) / zspan; - float t2 = (z_range.second - p1->z()) / zspan; - Vec2f p = to_2d(*p1); - Vec2f v(p2->x() - p1->x(), p2->y() - p1->y()); - bbox_extend(to_3d((p + v * t1).eval(), float(z_range.first))); - bbox_extend(to_3d((p + v * t2).eval(), float(z_range.second))); - } else { - // Single intersection with the lower limit. - float t = (z_range.first - p1->z()) / (p2->z() - p1->z()); - Vec2f v(p2->x() - p1->x(), p2->y() - p1->y()); - bbox_extend(to_3d((to_2d(*p1) + v * t).eval(), float(z_range.first))); - bbox_extend(*p2); - } - } else if (p2->z() > z_range.second) { - // Single intersection with the upper limit. - float t = (z_range.second - p1->z()) / (p2->z() - p1->z()); - Vec2f v(p2->x() - p1->x(), p2->y() - p1->y()); - bbox_extend(to_3d((to_2d(*p1) + v * t).eval(), float(z_range.second))); - bbox_extend(*p1); - } else { - // Both points are inside. - bbox_extend(*p1); - bbox_extend(*p2); - } - iprev = iedge; - } - } - } - - for (std::pair &bbox : bboxes) { - bbox.first.min() -= Vec3f(offset, offset, float(EPSILON)); - bbox.first.max() += Vec3f(offset, offset, float(EPSILON)); - } -} - -// Last PrintObject for this print_object_regions has been fully invalidated (deleted). -// Keep print_object_regions, but delete those volumes, which were either removed from new_volumes, or which rotated or scaled, so they need -// their bounding boxes to be recalculated. -void print_objects_regions_invalidate_keep_some_volumes(PrintObjectRegions &print_object_regions, ModelVolumePtrs old_volumes, ModelVolumePtrs new_volumes) -{ - print_object_regions.all_regions.clear(); - - model_volumes_sort_by_id(old_volumes); - model_volumes_sort_by_id(new_volumes); - - size_t i_cached_volume = 0; - size_t last_cached_volume = 0; - size_t i_old = 0; - for (size_t i_new = 0; i_new < new_volumes.size(); ++ i_new) - if (model_volume_solid_or_modifier(*new_volumes[i_new])) { - for (; i_old < old_volumes.size(); ++ i_old) - if (old_volumes[i_old]->id() >= new_volumes[i_new]->id()) - break; - if (i_old != old_volumes.size() && old_volumes[i_old]->id() == new_volumes[i_new]->id()) { - if (old_volumes[i_old]->get_matrix().isApprox(new_volumes[i_new]->get_matrix())) { - // Reuse the volume. - for (; print_object_regions.cached_volume_ids[i_cached_volume] < old_volumes[i_old]->id(); ++ i_cached_volume) - assert(i_cached_volume < print_object_regions.cached_volume_ids.size()); - assert(i_cached_volume < print_object_regions.cached_volume_ids.size() && print_object_regions.cached_volume_ids[i_cached_volume] == old_volumes[i_old]->id()); - print_object_regions.cached_volume_ids[last_cached_volume ++] = print_object_regions.cached_volume_ids[i_cached_volume ++]; - } else { - // Don't reuse the volume. - } - } - } - print_object_regions.cached_volume_ids.erase(print_object_regions.cached_volume_ids.begin() + last_cached_volume, print_object_regions.cached_volume_ids.end()); -} - -// Find a bounding box of a volume's part intersecting layer_range. Such a bounding box will likely be smaller in XY than the full bounding box, -// thus it will intersect with lower number of other volumes. -const PrintObjectRegions::BoundingBox* find_volume_extents(const PrintObjectRegions::LayerRangeRegions &layer_range, const ModelVolume &volume) -{ - auto it = lower_bound_by_predicate(layer_range.volumes.begin(), layer_range.volumes.end(), [&volume](const PrintObjectRegions::VolumeExtents &l){ return l.volume_id < volume.id(); }); - return it != layer_range.volumes.end() && it->volume_id == volume.id() ? &it->bbox : nullptr; -} - -// Find a bounding box of a topmost printable volume referenced by this modifier given this_region_id. -PrintObjectRegions::BoundingBox find_modifier_volume_extents(const PrintObjectRegions::LayerRangeRegions &layer_range, const int this_region_id) -{ - // Find the top-most printable volume of this modifier, or the printable volume itself. - const PrintObjectRegions::VolumeRegion &this_region = layer_range.volume_regions[this_region_id]; - const PrintObjectRegions::BoundingBox *this_extents = find_volume_extents(layer_range, *this_region.model_volume); - assert(this_extents); - PrintObjectRegions::BoundingBox out { *this_extents }; - if (! this_region.model_volume->is_model_part()) - for (int parent_region_id = this_region.parent;;) { - assert(parent_region_id >= 0); - const PrintObjectRegions::VolumeRegion &parent_region = layer_range.volume_regions[parent_region_id]; - const PrintObjectRegions::BoundingBox *parent_extents = find_volume_extents(layer_range, *parent_region.model_volume); - assert(parent_extents); - out.extend(*parent_extents); - if (parent_region.model_volume->is_model_part()) - break; - parent_region_id = parent_region.parent; - } - return out; -} - -PrintRegionConfig region_config_from_model_volume(const PrintRegionConfig &default_or_parent_region_config, const DynamicPrintConfig *layer_range_config, const ModelVolume &volume, size_t num_extruders); - -void print_region_ref_inc(PrintRegion &r) { ++ r.m_ref_cnt; } -void print_region_ref_reset(PrintRegion &r) { r.m_ref_cnt = 0; } -int print_region_ref_cnt(const PrintRegion &r) { return r.m_ref_cnt; } - -// Verify whether the PrintRegions of a PrintObject are still valid, possibly after updating the region configs. -// Before region configs are updated, callback_invalidate() is called to possibly stop background processing. -// Returns false if this object needs to be resliced because regions were merged or split. -bool verify_update_print_object_regions( - ModelVolumePtrs model_volumes, - const PrintRegionConfig &default_region_config, - size_t num_extruders, - const std::vector &painting_extruders, - PrintObjectRegions &print_object_regions, - const std::function &callback_invalidate) -{ - // Sort by ModelVolume ID. - model_volumes_sort_by_id(model_volumes); - - for (std::unique_ptr ®ion : print_object_regions.all_regions) - print_region_ref_reset(*region); - - // Verify and / or update PrintRegions produced by ModelVolumes, layer range modifiers, modifier volumes. - for (PrintObjectRegions::LayerRangeRegions &layer_range : print_object_regions.layer_ranges) { - // Each modifier ModelVolume intersecting this layer_range shall be referenced here at least once if it intersects some - // printable ModelVolume at this layer_range even if it does not modify its overlapping printable ModelVolume configuration yet. - // VolumeRegions reference ModelVolumes in layer_range.volume_regions the order they are stored in ModelObject volumes. - // Remember whether a given modifier ModelVolume was visited already. - auto it_model_volume_modifier_last = model_volumes.end(); - for (PrintObjectRegions::VolumeRegion ®ion : layer_range.volume_regions) - if (region.model_volume->is_model_part() || region.model_volume->is_modifier()) { - auto it_model_volume = lower_bound_by_predicate(model_volumes.begin(), model_volumes.end(), [®ion](const ModelVolume *l){ return l->id() < region.model_volume->id(); }); - assert(it_model_volume != model_volumes.end() && (*it_model_volume)->id() == region.model_volume->id()); - if (region.model_volume->is_modifier() && it_model_volume != it_model_volume_modifier_last) { - // A modifier ModelVolume is visited for the first time. - // A visited modifier may not have had parent volume_regions created overlapping with some model parts or modifiers, - // if the visited modifier did not modify their properties. Now the visited modifier's configuration may have changed, - // which may require new regions to be created. - it_model_volume_modifier_last = it_model_volume; - int next_region_id = int(®ion - layer_range.volume_regions.data()); - const PrintObjectRegions::BoundingBox *bbox = find_volume_extents(layer_range, *region.model_volume); - assert(bbox); - for (int parent_region_id = next_region_id - 1; parent_region_id >= 0; -- parent_region_id) { - const PrintObjectRegions::VolumeRegion &parent_region = layer_range.volume_regions[parent_region_id]; - assert(parent_region.model_volume != region.model_volume); - if (parent_region.model_volume->is_model_part() || parent_region.model_volume->is_modifier()) { - // volume_regions are produced in decreasing order of parent volume_regions ids. - // Some regions may not have been generated the last time by generate_print_object_regions(). - assert(next_region_id == int(layer_range.volume_regions.size()) || - layer_range.volume_regions[next_region_id].model_volume != region.model_volume || - layer_range.volume_regions[next_region_id].parent <= parent_region_id); - if (next_region_id < int(layer_range.volume_regions.size()) && - layer_range.volume_regions[next_region_id].model_volume == region.model_volume && - layer_range.volume_regions[next_region_id].parent == parent_region_id) { - // A parent region is already overridden. - ++ next_region_id; - } else if (PrintObjectRegions::BoundingBox parent_bbox = find_modifier_volume_extents(layer_range, parent_region_id); parent_bbox.intersects(*bbox)) - // Such parent region does not exist. If it is needed, then we need to reslice. - // Only create new region for a modifier, which actually modifies config of it's parent. - if (PrintRegionConfig config = region_config_from_model_volume(parent_region.region->config(), nullptr, **it_model_volume, num_extruders); - config != parent_region.region->config()) - // This modifier newly overrides a region, which it did not before. We need to reslice. - return false; - } - } - } - PrintRegionConfig cfg = region.parent == -1 ? - region_config_from_model_volume(default_region_config, layer_range.config, **it_model_volume, num_extruders) : - region_config_from_model_volume(layer_range.volume_regions[region.parent].region->config(), nullptr, **it_model_volume, num_extruders); - if (cfg != region.region->config()) { - // Region configuration changed. - if (print_region_ref_cnt(*region.region) == 0) { - // Region is referenced for the first time. Just change its parameters. - // Stop the background process before assigning new configuration to the regions. - t_config_option_keys diff = region.region->config().diff(cfg); - callback_invalidate(region.region->config(), cfg, diff); - region.region->config_apply_only(cfg, diff, false); - } else { - // Region is referenced multiple times, thus the region is being split. We need to reslice. - return false; - } - } - print_region_ref_inc(*region.region); - } - } - - // Verify and / or update PrintRegions produced by color painting. - for (const PrintObjectRegions::LayerRangeRegions &layer_range : print_object_regions.layer_ranges) - for (const PrintObjectRegions::PaintedRegion ®ion : layer_range.painted_regions) { - const PrintObjectRegions::VolumeRegion &parent_region = layer_range.volume_regions[region.parent]; - PrintRegionConfig cfg = parent_region.region->config(); - cfg.perimeter_extruder.value = region.extruder_id; - cfg.solid_infill_extruder.value = region.extruder_id; - cfg.infill_extruder.value = region.extruder_id; - if (cfg != region.region->config()) { - // Region configuration changed. - if (print_region_ref_cnt(*region.region) == 0) { - // Region is referenced for the first time. Just change its parameters. - // Stop the background process before assigning new configuration to the regions. - t_config_option_keys diff = region.region->config().diff(cfg); - callback_invalidate(region.region->config(), cfg, diff); - region.region->config_apply_only(cfg, diff, false); - } else { - // Region is referenced multiple times, thus the region is being split. We need to reslice. - return false; - } - } - print_region_ref_inc(*region.region); - } - - // Lastly verify, whether some regions were not merged. - { - std::vector regions; - regions.reserve(print_object_regions.all_regions.size()); - for (std::unique_ptr ®ion : print_object_regions.all_regions) { - assert(print_region_ref_cnt(*region) > 0); - regions.emplace_back(&(*region.get())); - } - std::sort(regions.begin(), regions.end(), [](const PrintRegion *l, const PrintRegion *r){ return l->config_hash() < r->config_hash(); }); - for (size_t i = 0; i < regions.size(); ++ i) { - size_t hash = regions[i]->config_hash(); - size_t j = i; - for (++ j; j < regions.size() && regions[j]->config_hash() == hash; ++ j) - if (regions[i]->config() == regions[j]->config()) { - // Regions were merged. We need to reslice. - return false; - } - } - } - - return true; -} - -// Update caches of volume bounding boxes. -void update_volume_bboxes( - std::vector &layer_ranges, - std::vector &cached_volume_ids, - ModelVolumePtrs model_volumes, - const Transform3d &object_trafo, - const float offset) -{ - // output will be sorted by the order of model_volumes sorted by their ObjectIDs. - model_volumes_sort_by_id(model_volumes); - - if (layer_ranges.size() == 1) { - PrintObjectRegions::LayerRangeRegions &layer_range = layer_ranges.front(); - std::vector volumes_old(std::move(layer_range.volumes)); - layer_range.volumes.reserve(model_volumes.size()); - for (const ModelVolume *model_volume : model_volumes) - if (model_volume_solid_or_modifier(*model_volume)) { - if (std::binary_search(cached_volume_ids.begin(), cached_volume_ids.end(), model_volume->id())) { - auto it = lower_bound_by_predicate(volumes_old.begin(), volumes_old.end(), [model_volume](PrintObjectRegions::VolumeExtents &l) { return l.volume_id < model_volume->id(); }); - if (it != volumes_old.end() && it->volume_id == model_volume->id()) - layer_range.volumes.emplace_back(*it); - } else - layer_range.volumes.push_back({ model_volume->id(), - transformed_its_bbox2d(model_volume->mesh().its, trafo_for_bbox(object_trafo, model_volume->get_matrix(false)), offset) }); - } - } else { - std::vector> volumes_old; - if (cached_volume_ids.empty()) - for (PrintObjectRegions::LayerRangeRegions &layer_range : layer_ranges) - layer_range.volumes.clear(); - else { - volumes_old.reserve(layer_ranges.size()); - for (PrintObjectRegions::LayerRangeRegions &layer_range : layer_ranges) - volumes_old.emplace_back(std::move(layer_range.volumes)); - } - - std::vector> bboxes; - std::vector ranges; - ranges.reserve(layer_ranges.size()); - for (const PrintObjectRegions::LayerRangeRegions &layer_range : layer_ranges) { - t_layer_height_range r = layer_range.layer_height_range; - r.first -= EPSILON; - r.second += EPSILON; - ranges.emplace_back(r); - } - for (const ModelVolume *model_volume : model_volumes) - if (model_volume_solid_or_modifier(*model_volume)) { - if (std::binary_search(cached_volume_ids.begin(), cached_volume_ids.end(), model_volume->id())) { - for (PrintObjectRegions::LayerRangeRegions &layer_range : layer_ranges) { - const auto &vold = volumes_old[&layer_range - layer_ranges.data()]; - auto it = lower_bound_by_predicate(vold.begin(), vold.end(), [model_volume](const PrintObjectRegions::VolumeExtents &l) { return l.volume_id < model_volume->id(); }); - if (it != vold.end() && it->volume_id == model_volume->id()) - layer_range.volumes.emplace_back(*it); - } - } else { - transformed_its_bboxes_in_z_ranges(model_volume->mesh().its, trafo_for_bbox(object_trafo, model_volume->get_matrix(false)), ranges, bboxes, offset); - for (PrintObjectRegions::LayerRangeRegions &layer_range : layer_ranges) - if (auto &bbox = bboxes[&layer_range - layer_ranges.data()]; bbox.second) - layer_range.volumes.push_back({ model_volume->id(), bbox.first }); - } - } - } - - cached_volume_ids.clear(); - cached_volume_ids.reserve(model_volumes.size()); - for (const ModelVolume *v : model_volumes) - if (model_volume_solid_or_modifier(*v)) - cached_volume_ids.emplace_back(v->id()); -} - -// Either a fresh PrintObject, or PrintObject regions were invalidated (merged, split). -// Generate PrintRegions from scratch. -static PrintObjectRegions* generate_print_object_regions( - PrintObjectRegions *print_object_regions_old, - const ModelVolumePtrs &model_volumes, - const LayerRanges &model_layer_ranges, - const PrintRegionConfig &default_region_config, - const Transform3d &trafo, - size_t num_extruders, - const float xy_size_compensation, - const std::vector &painting_extruders) -{ - // Reuse the old object or generate a new one. - auto out = print_object_regions_old ? std::unique_ptr(print_object_regions_old) : std::make_unique(); - auto &all_regions = out->all_regions; - auto &layer_ranges_regions = out->layer_ranges; - - all_regions.clear(); - - bool reuse_old = print_object_regions_old && !print_object_regions_old->layer_ranges.empty(); - - if (reuse_old) { - // Reuse old bounding boxes of some ModelVolumes and their ranges. - // Verify that the old ranges match the new ranges. - assert(model_layer_ranges.size() == layer_ranges_regions.size()); - for (const auto &range : model_layer_ranges) { - PrintObjectRegions::LayerRangeRegions &r = layer_ranges_regions[&range - &*model_layer_ranges.begin()]; - assert(range.layer_height_range == r.layer_height_range); - // If model::assign_copy() is called, layer_ranges_regions is copied thus the pointers to configs are lost. - r.config = range.config; - r.volume_regions.clear(); - r.painted_regions.clear(); - } - } else { - out->trafo_bboxes = trafo; - layer_ranges_regions.reserve(model_layer_ranges.size()); - for (const auto &range : model_layer_ranges) - layer_ranges_regions.push_back({ range.layer_height_range, range.config }); - } - - const bool is_mm_painted = num_extruders > 1 && std::any_of(model_volumes.cbegin(), model_volumes.cend(), [](const ModelVolume *mv) { return mv->is_mm_painted(); }); - update_volume_bboxes(layer_ranges_regions, out->cached_volume_ids, model_volumes, out->trafo_bboxes, is_mm_painted ? 0.f : std::max(0.f, xy_size_compensation)); - - std::vector region_set; - auto get_create_region = [®ion_set, &all_regions](PrintRegionConfig &&config) -> PrintRegion* { - size_t hash = config.hash(); - auto it = Slic3r::lower_bound_by_predicate(region_set.begin(), region_set.end(), [&config, hash](const PrintRegion* l) { - return l->config_hash() < hash || (l->config_hash() == hash && l->config() < config); }); - if (it != region_set.end() && (*it)->config_hash() == hash && (*it)->config() == config) - return *it; - // Insert into a sorted array, it has O(n) complexity, but the calling algorithm has an O(n^2*log(n)) complexity anyways. - all_regions.emplace_back(std::make_unique(std::move(config), hash, int(all_regions.size()))); - PrintRegion *region = all_regions.back().get(); - region_set.emplace(it, region); - return region; - }; - - // Chain the regions in the order they are stored in the volumes list. - for (int volume_id = 0; volume_id < int(model_volumes.size()); ++ volume_id) { - const ModelVolume &volume = *model_volumes[volume_id]; - if (model_volume_solid_or_modifier(volume)) { - for (PrintObjectRegions::LayerRangeRegions &layer_range : layer_ranges_regions) - if (const PrintObjectRegions::BoundingBox *bbox = find_volume_extents(layer_range, volume); bbox) { - if (volume.is_model_part()) { - // Add a model volume, assign an existing region or generate a new one. - layer_range.volume_regions.push_back({ - &volume, -1, - get_create_region(region_config_from_model_volume(default_region_config, layer_range.config, volume, num_extruders)), - bbox - }); - } else if (volume.is_negative_volume()) { - // Add a negative (subtractor) volume. Such volume has neither region nor parent volume assigned. - layer_range.volume_regions.push_back({ &volume, -1, nullptr, bbox }); - } else { - assert(volume.is_modifier()); - // Modifiers may be chained one over the other. Check for overlap, merge DynamicPrintConfigs. - bool added = false; - int parent_model_part_id = -1; - for (int parent_region_id = int(layer_range.volume_regions.size()) - 1; parent_region_id >= 0; -- parent_region_id) { - const PrintObjectRegions::VolumeRegion &parent_region = layer_range.volume_regions[parent_region_id]; - const ModelVolume &parent_volume = *parent_region.model_volume; - if (parent_volume.is_model_part() || parent_volume.is_modifier()) - if (PrintObjectRegions::BoundingBox parent_bbox = find_modifier_volume_extents(layer_range, parent_region_id); parent_bbox.intersects(*bbox)) { - // Only create new region for a modifier, which actually modifies config of it's parent. - if (PrintRegionConfig config = region_config_from_model_volume(parent_region.region->config(), nullptr, volume, num_extruders); - config != parent_region.region->config()) { - added = true; - layer_range.volume_regions.push_back({ &volume, parent_region_id, get_create_region(std::move(config)), bbox }); - } else if (parent_model_part_id == -1 && parent_volume.is_model_part()) - parent_model_part_id = parent_region_id; - } - } - if (! added && parent_model_part_id >= 0) - // This modifier does not override any printable volume's configuration, however it may in the future. - // Store it so that verify_update_print_object_regions() will handle this modifier correctly if its configuration changes. - layer_range.volume_regions.push_back({ &volume, parent_model_part_id, layer_range.volume_regions[parent_model_part_id].region, bbox }); - } - } - } - } - - // Finally add painting regions. - for (PrintObjectRegions::LayerRangeRegions &layer_range : layer_ranges_regions) { - for (unsigned int painted_extruder_id : painting_extruders) - for (int parent_region_id = 0; parent_region_id < int(layer_range.volume_regions.size()); ++ parent_region_id) - if (const PrintObjectRegions::VolumeRegion &parent_region = layer_range.volume_regions[parent_region_id]; - parent_region.model_volume->is_model_part() || parent_region.model_volume->is_modifier()) { - PrintRegionConfig cfg = parent_region.region->config(); - cfg.perimeter_extruder.value = painted_extruder_id; - cfg.solid_infill_extruder.value = painted_extruder_id; - cfg.infill_extruder.value = painted_extruder_id; - layer_range.painted_regions.push_back({ painted_extruder_id, parent_region_id, get_create_region(std::move(cfg))}); - } - // Sort the regions by parent region::print_object_region_id() and extruder_id to help the slicing algorithm when applying MMU segmentation. - std::sort(layer_range.painted_regions.begin(), layer_range.painted_regions.end(), [&layer_range](auto &l, auto &r) { - int lid = layer_range.volume_regions[l.parent].region->print_object_region_id(); - int rid = layer_range.volume_regions[r.parent].region->print_object_region_id(); - return lid < rid || (lid == rid && l.extruder_id < r.extruder_id); }); - } - - return out.release(); -} - -Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_config) -{ -#ifdef _DEBUG - check_model_ids_validity(model); -#endif /* _DEBUG */ - - // Normalize the config. - new_full_config.option("print_settings_id", true); - new_full_config.option("filament_settings_id", true); - new_full_config.option("printer_settings_id", true); - new_full_config.option("physical_printer_settings_id", true); - new_full_config.normalize_fdm(); - - // Find modified keys of the various configs. Resolve overrides extruder retract values by filament profiles. - DynamicPrintConfig filament_overrides; - t_config_option_keys print_diff = print_config_diffs(m_config, new_full_config, filament_overrides); - t_config_option_keys full_config_diff = full_print_config_diffs(m_full_print_config, new_full_config); - // Collect changes to object and region configs. - t_config_option_keys object_diff = m_default_object_config.diff(new_full_config); - t_config_option_keys region_diff = m_default_region_config.diff(new_full_config); - - // Do not use the ApplyStatus as we will use the max function when updating apply_status. - unsigned int apply_status = APPLY_STATUS_UNCHANGED; - auto update_apply_status = [&apply_status](bool invalidated) - { apply_status = std::max(apply_status, invalidated ? APPLY_STATUS_INVALIDATED : APPLY_STATUS_CHANGED); }; - if (! (print_diff.empty() && object_diff.empty() && region_diff.empty())) - update_apply_status(false); - - // Grab the lock for the Print / PrintObject milestones. - std::scoped_lock lock(this->state_mutex()); - - // The following call may stop the background processing. - if (! print_diff.empty()) - update_apply_status(this->invalidate_state_by_config_options(new_full_config, print_diff)); - - // Apply variables to placeholder parser. The placeholder parser is used by G-code export, - // which should be stopped if print_diff is not empty. - size_t num_extruders = m_config.nozzle_diameter.size(); - bool num_extruders_changed = false; - if (! full_config_diff.empty()) { - update_apply_status(this->invalidate_step(psGCodeExport)); - m_placeholder_parser.clear_config(); - // Set the profile aliases for the PrintBase::output_filename() - m_placeholder_parser.set("print_preset", new_full_config.option("print_settings_id")->clone()); - m_placeholder_parser.set("filament_preset", new_full_config.option("filament_settings_id")->clone()); - m_placeholder_parser.set("printer_preset", new_full_config.option("printer_settings_id")->clone()); - m_placeholder_parser.set("physical_printer_preset", new_full_config.option("physical_printer_settings_id")->clone()); - // We want the filament overrides to be applied over their respective extruder parameters by the PlaceholderParser. - // see "Placeholders do not respect filament overrides." GH issue #3649 - m_placeholder_parser.apply_config(filament_overrides); - // It is also safe to change m_config now after this->invalidate_state_by_config_options() call. - m_config.apply_only(new_full_config, print_diff, true); - //FIXME use move semantics once ConfigBase supports it. - // Some filament_overrides may contain values different from new_full_config, but equal to m_config. - // As long as these config options don't reallocate memory when copying, we are safe overriding a value, which is in use by a worker thread. - m_config.apply(filament_overrides); - // Handle changes to object config defaults - m_default_object_config.apply_only(new_full_config, object_diff, true); - // Handle changes to regions config defaults - m_default_region_config.apply_only(new_full_config, region_diff, true); - m_full_print_config = std::move(new_full_config); - if (num_extruders != m_config.nozzle_diameter.size()) { - num_extruders = m_config.nozzle_diameter.size(); - num_extruders_changed = true; - } - } - - ModelObjectStatusDB model_object_status_db; - - // 1) Synchronize model objects. - bool print_regions_reshuffled = false; - if (model.id() != m_model.id()) { - // Kill everything, initialize from scratch. - // Stop background processing. - this->call_cancel_callback(); - update_apply_status(this->invalidate_all_steps()); - for (PrintObject *object : m_objects) { - model_object_status_db.add(*object->model_object(), ModelObjectStatus::Deleted); - update_apply_status(object->invalidate_all_steps()); - delete object; - } - m_objects.clear(); - print_regions_reshuffled = true; - m_model.assign_copy(model); - for (const ModelObject *model_object : m_model.objects) - model_object_status_db.add(*model_object, ModelObjectStatus::New); - } else { - if (m_model.custom_gcode_per_print_z != model.custom_gcode_per_print_z) { - update_apply_status(num_extruders_changed || - // Tool change G-codes are applied as color changes for a single extruder printer, no need to invalidate tool ordering. - //FIXME The tool ordering may be invalidated unnecessarily if the custom_gcode_per_print_z.mode is not applicable - // to the active print / model state, and then it is reset, so it is being applicable, but empty, thus the effect is the same. - (num_extruders > 1 && custom_per_printz_gcodes_tool_changes_differ(m_model.custom_gcode_per_print_z.gcodes, model.custom_gcode_per_print_z.gcodes)) ? - // The Tool Ordering and the Wipe Tower are no more valid. - this->invalidate_steps({ psWipeTower, psGCodeExport }) : - // There is no change in Tool Changes stored in custom_gcode_per_print_z, therefore there is no need to update Tool Ordering. - this->invalidate_step(psGCodeExport)); - m_model.custom_gcode_per_print_z = model.custom_gcode_per_print_z; - } - if (model_object_list_equal(m_model, model)) { - // The object list did not change. - for (const ModelObject *model_object : m_model.objects) - model_object_status_db.add(*model_object, ModelObjectStatus::Old); - } else if (model_object_list_extended(m_model, model)) { - // Add new objects. Their volumes and configs will be synchronized later. - update_apply_status(this->invalidate_step(psGCodeExport)); - for (const ModelObject *model_object : m_model.objects) - model_object_status_db.add(*model_object, ModelObjectStatus::Old); - for (size_t i = m_model.objects.size(); i < model.objects.size(); ++ i) { - model_object_status_db.add(*model.objects[i], ModelObjectStatus::New); - m_model.objects.emplace_back(ModelObject::new_copy(*model.objects[i])); - m_model.objects.back()->set_model(&m_model); - } - } else { - // Reorder the objects, add new objects. - // First stop background processing before shuffling or deleting the PrintObjects in the object list. - this->call_cancel_callback(); - update_apply_status(this->invalidate_step(psGCodeExport)); - // Second create a new list of objects. - std::vector model_objects_old(std::move(m_model.objects)); - m_model.objects.clear(); - m_model.objects.reserve(model.objects.size()); - auto by_id_lower = [](const ModelObject *lhs, const ModelObject *rhs){ return lhs->id() < rhs->id(); }; - std::sort(model_objects_old.begin(), model_objects_old.end(), by_id_lower); - for (const ModelObject *mobj : model.objects) { - auto it = std::lower_bound(model_objects_old.begin(), model_objects_old.end(), mobj, by_id_lower); - if (it == model_objects_old.end() || (*it)->id() != mobj->id()) { - // New ModelObject added. - m_model.objects.emplace_back(ModelObject::new_copy(*mobj)); - m_model.objects.back()->set_model(&m_model); - model_object_status_db.add(*mobj, ModelObjectStatus::New); - } else { - // Existing ModelObject re-added (possibly moved in the list). - m_model.objects.emplace_back(*it); - model_object_status_db.add(*mobj, ModelObjectStatus::Moved); - } - } - bool deleted_any = false; - for (ModelObject *&model_object : model_objects_old) - if (model_object_status_db.add_if_new(*model_object, ModelObjectStatus::Deleted)) - deleted_any = true; - else - // Do not delete this ModelObject instance. - model_object = nullptr; - if (deleted_any) { - // Delete PrintObjects of the deleted ModelObjects. - PrintObjectPtrs print_objects_old = std::move(m_objects); - m_objects.clear(); - m_objects.reserve(print_objects_old.size()); - for (PrintObject *print_object : print_objects_old) { - const ModelObjectStatus &status = model_object_status_db.get(*print_object->model_object()); - if (status.status == ModelObjectStatus::Deleted) { - update_apply_status(print_object->invalidate_all_steps()); - delete print_object; - } else - m_objects.emplace_back(print_object); - } - for (ModelObject *model_object : model_objects_old) - delete model_object; - print_regions_reshuffled = true; - } - } - } - - // 2) Map print objects including their transformation matrices. - PrintObjectStatusDB print_object_status_db(m_objects); - - // 3) Synchronize ModelObjects & PrintObjects. - const std::initializer_list solid_or_modifier_types { ModelVolumeType::MODEL_PART, ModelVolumeType::NEGATIVE_VOLUME, ModelVolumeType::PARAMETER_MODIFIER }; - for (size_t idx_model_object = 0; idx_model_object < model.objects.size(); ++ idx_model_object) { - ModelObject &model_object = *m_model.objects[idx_model_object]; - ModelObjectStatus &model_object_status = const_cast(model_object_status_db.reuse(model_object)); - const ModelObject &model_object_new = *model.objects[idx_model_object]; - if (model_object_status.status == ModelObjectStatus::New) - // PrintObject instances will be added in the next loop. - continue; - // Update the ModelObject instance, possibly invalidate the linked PrintObjects. - assert(model_object_status.status == ModelObjectStatus::Old || model_object_status.status == ModelObjectStatus::Moved); - // Check whether a model part volume was added or removed, their transformations or order changed. - // Only volume IDs, volume types, transformation matrices and their order are checked, configuration and other parameters are NOT checked. - bool solid_or_modifier_differ = model_volume_list_changed(model_object, model_object_new, solid_or_modifier_types) || - model_mmu_segmentation_data_changed(model_object, model_object_new) || - (model_object_new.is_mm_painted() && num_extruders_changed); - bool supports_differ = model_volume_list_changed(model_object, model_object_new, ModelVolumeType::SUPPORT_BLOCKER) || - model_volume_list_changed(model_object, model_object_new, ModelVolumeType::SUPPORT_ENFORCER); - bool layer_height_ranges_differ = ! layer_height_ranges_equal(model_object.layer_config_ranges, model_object_new.layer_config_ranges, model_object_new.layer_height_profile.empty()); - bool model_origin_translation_differ = model_object.origin_translation != model_object_new.origin_translation; - auto print_objects_range = print_object_status_db.get_range(model_object); - // The list actually can be empty if all instances are out of the print bed. - //assert(print_objects_range.begin() != print_objects_range.end()); - // All PrintObjects in print_objects_range shall point to the same prints_objects_regions - if (print_objects_range.begin() != print_objects_range.end()) { - model_object_status.print_object_regions = print_objects_range.begin()->print_object->m_shared_regions; - model_object_status.print_object_regions->ref_cnt_inc(); - } - if (solid_or_modifier_differ || model_origin_translation_differ || layer_height_ranges_differ || - ! model_object.layer_height_profile.timestamp_matches(model_object_new.layer_height_profile)) { - // The very first step (the slicing step) is invalidated. One may freely remove all associated PrintObjects. - model_object_status.print_object_regions_status = - model_object_status.print_object_regions == nullptr || model_origin_translation_differ || layer_height_ranges_differ ? - // Drop print_objects_regions. - ModelObjectStatus::PrintObjectRegionsStatus::Invalid : - // Reuse bounding boxes of print_objects_regions for ModelVolumes with unmodified transformation. - ModelObjectStatus::PrintObjectRegionsStatus::PartiallyValid; - for (const PrintObjectStatus &print_object_status : print_objects_range) { - update_apply_status(print_object_status.print_object->invalidate_all_steps()); - const_cast(print_object_status).status = PrintObjectStatus::Deleted; - } - if (model_object_status.print_object_regions_status == ModelObjectStatus::PrintObjectRegionsStatus::PartiallyValid) - // Drop everything from PrintObjectRegions but those VolumeExtents (of their particular ModelVolumes) that are still valid. - print_objects_regions_invalidate_keep_some_volumes(*model_object_status.print_object_regions, model_object.volumes, model_object_new.volumes); - else if (model_object_status.print_object_regions != nullptr) - model_object_status.print_object_regions->clear(); - // Copy content of the ModelObject including its ID, do not change the parent. - model_object.assign_copy(model_object_new); - } else { - model_object_status.print_object_regions_status = ModelObjectStatus::PrintObjectRegionsStatus::Valid; - if (supports_differ || model_custom_supports_data_changed(model_object, model_object_new)) { - // First stop background processing before shuffling or deleting the ModelVolumes in the ModelObject's list. - if (supports_differ) { - this->call_cancel_callback(); - update_apply_status(false); - } - // Invalidate just the supports step. - for (const PrintObjectStatus &print_object_status : print_objects_range) - update_apply_status(print_object_status.print_object->invalidate_step(posSupportMaterial)); - if (supports_differ) { - // Copy just the support volumes. - model_volume_list_update_supports(model_object, model_object_new); - } - } else if (model_custom_seam_data_changed(model_object, model_object_new)) { - update_apply_status(this->invalidate_step(psGCodeExport)); - } - } - if (! solid_or_modifier_differ) { - // Synchronize Object's config. - bool object_config_changed = ! model_object.config.timestamp_matches(model_object_new.config); - if (object_config_changed) - model_object.config.assign_config(model_object_new.config); - if (! object_diff.empty() || object_config_changed || num_extruders_changed) { - PrintObjectConfig new_config = PrintObject::object_config_from_model_object(m_default_object_config, model_object, num_extruders); - for (const PrintObjectStatus &print_object_status : print_object_status_db.get_range(model_object)) { - t_config_option_keys diff = print_object_status.print_object->config().diff(new_config); - if (! diff.empty()) { - update_apply_status(print_object_status.print_object->invalidate_state_by_config_options(print_object_status.print_object->config(), new_config, diff)); - print_object_status.print_object->config_apply_only(new_config, diff, true); - } - } - } - // Synchronize (just copy) the remaining data of ModelVolumes (name, config, custom supports data). - //FIXME What to do with m_material_id? - model_volume_list_copy_configs(model_object /* dst */, model_object_new /* src */, ModelVolumeType::MODEL_PART); - model_volume_list_copy_configs(model_object /* dst */, model_object_new /* src */, ModelVolumeType::PARAMETER_MODIFIER); - layer_height_ranges_copy_configs(model_object.layer_config_ranges /* dst */, model_object_new.layer_config_ranges /* src */); - // Copy the ModelObject name, input_file and instances. The instances will be compared against PrintObject instances in the next step. - model_object.name = model_object_new.name; - model_object.input_file = model_object_new.input_file; - // Only refresh ModelInstances if there is any change. - if (model_object.instances.size() != model_object_new.instances.size() || - ! std::equal(model_object.instances.begin(), model_object.instances.end(), model_object_new.instances.begin(), [](auto l, auto r){ return l->id() == r->id(); })) { - // G-code generator accesses model_object.instances to generate sequential print ordering matching the Plater object list. - update_apply_status(this->invalidate_step(psGCodeExport)); - model_object.clear_instances(); - model_object.instances.reserve(model_object_new.instances.size()); - for (const ModelInstance *model_instance : model_object_new.instances) { - model_object.instances.emplace_back(new ModelInstance(*model_instance)); - model_object.instances.back()->set_model_object(&model_object); - } - } else if (! std::equal(model_object.instances.begin(), model_object.instances.end(), model_object_new.instances.begin(), - [](auto l, auto r){ return l->print_volume_state == r->print_volume_state && l->printable == r->printable && - l->get_transformation().get_matrix().isApprox(r->get_transformation().get_matrix()); })) { - // If some of the instances changed, the bounding box of the updated ModelObject is likely no more valid. - // This is safe as the ModelObject's bounding box is only accessed from this function, which is called from the main thread only. - model_object.invalidate_bounding_box(); - // Synchronize the content of instances. - auto new_instance = model_object_new.instances.begin(); - for (auto old_instance = model_object.instances.begin(); old_instance != model_object.instances.end(); ++ old_instance, ++ new_instance) { - (*old_instance)->set_transformation((*new_instance)->get_transformation()); - (*old_instance)->print_volume_state = (*new_instance)->print_volume_state; - (*old_instance)->printable = (*new_instance)->printable; - } - } - } - } - - // 4) Generate PrintObjects from ModelObjects and their instances. - { - PrintObjectPtrs print_objects_new; - print_objects_new.reserve(std::max(m_objects.size(), m_model.objects.size())); - bool new_objects = false; - // Walk over all new model objects and check, whether there are matching PrintObjects. - for (ModelObject *model_object : m_model.objects) { - ModelObjectStatus &model_object_status = const_cast(model_object_status_db.reuse(*model_object)); - model_object_status.print_instances = print_objects_from_model_object(*model_object); - std::vector old; - old.reserve(print_object_status_db.count(*model_object)); - for (const PrintObjectStatus &print_object_status : print_object_status_db.get_range(*model_object)) - if (print_object_status.status != PrintObjectStatus::Deleted) - old.emplace_back(&print_object_status); - // Generate a list of trafos and XY offsets for instances of a ModelObject - // Producing the config for PrintObject on demand, caching it at print_object_last. - const PrintObject *print_object_last = nullptr; - auto print_object_apply_config = [this, &print_object_last, model_object, num_extruders](PrintObject *print_object) { - print_object->config_apply(print_object_last ? - print_object_last->config() : - PrintObject::object_config_from_model_object(m_default_object_config, *model_object, num_extruders)); - print_object_last = print_object; - }; - if (old.empty()) { - // Simple case, just generate new instances. - for (PrintObjectTrafoAndInstances &print_instances : model_object_status.print_instances) { - PrintObject *print_object = new PrintObject(this, model_object, print_instances.trafo, std::move(print_instances.instances)); - print_object_apply_config(print_object); - print_objects_new.emplace_back(print_object); - // print_object_status.emplace(PrintObjectStatus(print_object, PrintObjectStatus::New)); - new_objects = true; - } - continue; - } - // Complex case, try to merge the two lists. - // Sort the old lexicographically by their trafos. - std::sort(old.begin(), old.end(), [](const PrintObjectStatus *lhs, const PrintObjectStatus *rhs){ return transform3d_lower(lhs->trafo, rhs->trafo); }); - // Merge the old / new lists. - auto it_old = old.begin(); - for (PrintObjectTrafoAndInstances &new_instances : model_object_status.print_instances) { - for (; it_old != old.end() && transform3d_lower((*it_old)->trafo, new_instances.trafo); ++ it_old); - if (it_old == old.end() || ! transform3d_equal((*it_old)->trafo, new_instances.trafo)) { - // This is a new instance (or a set of instances with the same trafo). Just add it. - PrintObject *print_object = new PrintObject(this, model_object, new_instances.trafo, std::move(new_instances.instances)); - print_object_apply_config(print_object); - print_objects_new.emplace_back(print_object); - // print_object_status.emplace(PrintObjectStatus(print_object, PrintObjectStatus::New)); - new_objects = true; - if (it_old != old.end()) - const_cast(*it_old)->status = PrintObjectStatus::Deleted; - } else { - // The PrintObject already exists and the copies differ. - PrintBase::ApplyStatus status = (*it_old)->print_object->set_instances(std::move(new_instances.instances)); - if (status != PrintBase::APPLY_STATUS_UNCHANGED) - update_apply_status(status == PrintBase::APPLY_STATUS_INVALIDATED); - print_objects_new.emplace_back((*it_old)->print_object); - const_cast(*it_old)->status = PrintObjectStatus::Reused; - } - } - } - if (m_objects != print_objects_new) { - this->call_cancel_callback(); - update_apply_status(this->invalidate_all_steps()); - m_objects = print_objects_new; - // Delete the PrintObjects marked as Unknown or Deleted. - bool deleted_objects = false; - for (const PrintObjectStatus &pos : print_object_status_db) - if (pos.status == PrintObjectStatus::Unknown || pos.status == PrintObjectStatus::Deleted) { - update_apply_status(pos.print_object->invalidate_all_steps()); - delete pos.print_object; - deleted_objects = true; - } - if (new_objects || deleted_objects) - update_apply_status(this->invalidate_steps({ psSkirtBrim, psWipeTower, psGCodeExport })); - if (new_objects) - update_apply_status(false); - print_regions_reshuffled = true; - } - print_object_status_db.clear(); - } - - // All regions now have distinct settings. - // Check whether applying the new region config defaults we would get different regions, - // update regions or create regions from scratch. - for (auto it_print_object = m_objects.begin(); it_print_object != m_objects.end();) { - // Find the range of PrintObjects sharing the same associated ModelObject. - auto it_print_object_end = it_print_object; - PrintObject &print_object = *(*it_print_object); - const ModelObject &model_object = *print_object.model_object(); - ModelObjectStatus &model_object_status = const_cast(model_object_status_db.reuse(model_object)); - PrintObjectRegions *print_object_regions = model_object_status.print_object_regions; - for (++ it_print_object_end; it_print_object_end != m_objects.end() && (*it_print_object)->model_object() == (*it_print_object_end)->model_object(); ++ it_print_object_end) - assert((*it_print_object_end)->m_shared_regions == nullptr || (*it_print_object_end)->m_shared_regions == print_object_regions); - if (print_object_regions == nullptr) { - print_object_regions = new PrintObjectRegions{}; - model_object_status.print_object_regions = print_object_regions; - print_object_regions->ref_cnt_inc(); - } - std::vector painting_extruders; - if (const auto &volumes = print_object.model_object()->volumes; - num_extruders > 1 && - std::find_if(volumes.begin(), volumes.end(), [](const ModelVolume *v) { return ! v->mmu_segmentation_facets.empty(); }) != volumes.end()) { - //FIXME be more specific! Don't enumerate extruders that are not used for painting! - painting_extruders.assign(num_extruders, 0); - std::iota(painting_extruders.begin(), painting_extruders.end(), 1); - } - if (model_object_status.print_object_regions_status == ModelObjectStatus::PrintObjectRegionsStatus::Valid) { - // Verify that the trafo for regions & volume bounding boxes thus for regions is still applicable. - auto invalidate = [it_print_object, it_print_object_end, update_apply_status]() { - for (auto it = it_print_object; it != it_print_object_end; ++ it) - if ((*it)->m_shared_regions != nullptr) - update_apply_status((*it)->invalidate_all_steps()); - }; - if (print_object_regions && ! trafos_differ_in_rotation_by_z_and_mirroring_by_xy_only(print_object_regions->trafo_bboxes, model_object_status.print_instances.front().trafo)) { - invalidate(); - print_object_regions->clear(); - model_object_status.print_object_regions_status = ModelObjectStatus::PrintObjectRegionsStatus::Invalid; - print_regions_reshuffled = true; - } else if (print_object_regions && - verify_update_print_object_regions( - print_object.model_object()->volumes, - m_default_region_config, - num_extruders, - painting_extruders, - *print_object_regions, - [it_print_object, it_print_object_end, &update_apply_status](const PrintRegionConfig &old_config, const PrintRegionConfig &new_config, const t_config_option_keys &diff_keys) { - for (auto it = it_print_object; it != it_print_object_end; ++it) - if ((*it)->m_shared_regions != nullptr) - update_apply_status((*it)->invalidate_state_by_config_options(old_config, new_config, diff_keys)); - })) { - // Regions are valid, just keep them. - } else { - // Regions were reshuffled. - invalidate(); - // At least reuse layer ranges and bounding boxes of ModelVolumes. - model_object_status.print_object_regions_status = ModelObjectStatus::PrintObjectRegionsStatus::PartiallyValid; - print_regions_reshuffled = true; - } - } - if (print_object_regions == nullptr || model_object_status.print_object_regions_status != ModelObjectStatus::PrintObjectRegionsStatus::Valid) { - // Layer ranges with their associated configurations. Remove overlaps between the ranges - // and create the regions from scratch. - print_object_regions = generate_print_object_regions( - print_object_regions, - print_object.model_object()->volumes, - LayerRanges(print_object.model_object()->layer_config_ranges), - m_default_region_config, - model_object_status.print_instances.front().trafo, - num_extruders, - print_object.is_mm_painted() ? 0.f : float(print_object.config().xy_size_compensation.value), - painting_extruders); - } - for (auto it = it_print_object; it != it_print_object_end; ++it) - if ((*it)->m_shared_regions) { - assert((*it)->m_shared_regions == print_object_regions); - } else { - (*it)->m_shared_regions = print_object_regions; - print_object_regions->ref_cnt_inc(); - } - it_print_object = it_print_object_end; - } - - if (print_regions_reshuffled) { - // Update Print::m_print_regions from objects. - struct cmp { bool operator() (const PrintRegion *l, const PrintRegion *r) const { return l->config_hash() == r->config_hash() && l->config() == r->config(); } }; - std::set region_set; - m_print_regions.clear(); - PrintObjectRegions *print_object_regions = nullptr; - for (PrintObject *print_object : m_objects) - if (print_object_regions != print_object->m_shared_regions) { - print_object_regions = print_object->m_shared_regions; - for (std::unique_ptr &print_region : print_object_regions->all_regions) - if (auto it = region_set.find(print_region.get()); it == region_set.end()) { - int print_region_id = int(m_print_regions.size()); - m_print_regions.emplace_back(print_region.get()); - print_region->m_print_region_id = print_region_id; - } else { - print_region->m_print_region_id = (*it)->print_region_id(); - } - } - } - - // Update SlicingParameters for each object where the SlicingParameters is not valid. - // If it is not valid, then it is ensured that PrintObject.m_slicing_params is not in use - // (posSlicing and posSupportMaterial was invalidated). - for (PrintObject *object : m_objects) - object->update_slicing_parameters(); - -#ifdef _DEBUG - check_model_ids_equal(m_model, model); -#endif /* _DEBUG */ - - return static_cast(apply_status); -} - -} // namespace Slic3r +#include "Model.hpp" +#include "Print.hpp" + +#include + +namespace Slic3r { + +// Add or remove support modifier ModelVolumes from model_object_dst to match the ModelVolumes of model_object_new +// in the exact order and with the same IDs. +// It is expected, that the model_object_dst already contains the non-support volumes of model_object_new in the correct order. +// Friend to ModelVolume to allow copying. +// static is not accepted by gcc if declared as a friend of ModelObject. +/* static */ void model_volume_list_update_supports(ModelObject &model_object_dst, const ModelObject &model_object_new) +{ + typedef std::pair ModelVolumeWithStatus; + std::vector old_volumes; + old_volumes.reserve(model_object_dst.volumes.size()); + for (const ModelVolume *model_volume : model_object_dst.volumes) + old_volumes.emplace_back(ModelVolumeWithStatus(model_volume, false)); + auto model_volume_lower = [](const ModelVolumeWithStatus &mv1, const ModelVolumeWithStatus &mv2){ return mv1.first->id() < mv2.first->id(); }; + auto model_volume_equal = [](const ModelVolumeWithStatus &mv1, const ModelVolumeWithStatus &mv2){ return mv1.first->id() == mv2.first->id(); }; + std::sort(old_volumes.begin(), old_volumes.end(), model_volume_lower); + model_object_dst.volumes.clear(); + model_object_dst.volumes.reserve(model_object_new.volumes.size()); + for (const ModelVolume *model_volume_src : model_object_new.volumes) { + ModelVolumeWithStatus key(model_volume_src, false); + auto it = std::lower_bound(old_volumes.begin(), old_volumes.end(), key, model_volume_lower); + if (it != old_volumes.end() && model_volume_equal(*it, key)) { + // The volume was found in the old list. Just copy it. + assert(! it->second); // not consumed yet + it->second = true; + ModelVolume *model_volume_dst = const_cast(it->first); + // For support modifiers, the type may have been switched from blocker to enforcer and vice versa. + assert((model_volume_dst->is_support_modifier() && model_volume_src->is_support_modifier()) || model_volume_dst->type() == model_volume_src->type()); + model_object_dst.volumes.emplace_back(model_volume_dst); + if (model_volume_dst->is_support_modifier()) { + // For support modifiers, the type may have been switched from blocker to enforcer and vice versa. + model_volume_dst->set_type(model_volume_src->type()); + model_volume_dst->set_transformation(model_volume_src->get_transformation()); + } + assert(model_volume_dst->get_matrix().isApprox(model_volume_src->get_matrix())); + } else { + // The volume was not found in the old list. Create a new copy. + assert(model_volume_src->is_support_modifier()); + model_object_dst.volumes.emplace_back(new ModelVolume(*model_volume_src)); + model_object_dst.volumes.back()->set_model_object(&model_object_dst); + } + } + // Release the non-consumed old volumes (those were deleted from the new list). + for (ModelVolumeWithStatus &mv_with_status : old_volumes) + if (! mv_with_status.second) + delete mv_with_status.first; +} + +static inline void model_volume_list_copy_configs(ModelObject &model_object_dst, const ModelObject &model_object_src, const ModelVolumeType type) +{ + size_t i_src, i_dst; + for (i_src = 0, i_dst = 0; i_src < model_object_src.volumes.size() && i_dst < model_object_dst.volumes.size();) { + const ModelVolume &mv_src = *model_object_src.volumes[i_src]; + ModelVolume &mv_dst = *model_object_dst.volumes[i_dst]; + if (mv_src.type() != type) { + ++ i_src; + continue; + } + if (mv_dst.type() != type) { + ++ i_dst; + continue; + } + assert(mv_src.id() == mv_dst.id()); + // Copy the ModelVolume data. + mv_dst.name = mv_src.name; + mv_dst.config.assign_config(mv_src.config); + assert(mv_dst.supported_facets.id() == mv_src.supported_facets.id()); + mv_dst.supported_facets.assign(mv_src.supported_facets); + assert(mv_dst.seam_facets.id() == mv_src.seam_facets.id()); + mv_dst.seam_facets.assign(mv_src.seam_facets); + assert(mv_dst.mmu_segmentation_facets.id() == mv_src.mmu_segmentation_facets.id()); + mv_dst.mmu_segmentation_facets.assign(mv_src.mmu_segmentation_facets); + //FIXME what to do with the materials? + // mv_dst.m_material_id = mv_src.m_material_id; + ++ i_src; + ++ i_dst; + } +} + +static inline void layer_height_ranges_copy_configs(t_layer_config_ranges &lr_dst, const t_layer_config_ranges &lr_src) +{ + assert(lr_dst.size() == lr_src.size()); + auto it_src = lr_src.cbegin(); + for (auto &kvp_dst : lr_dst) { + const auto &kvp_src = *it_src ++; + assert(std::abs(kvp_dst.first.first - kvp_src.first.first ) <= EPSILON); + assert(std::abs(kvp_dst.first.second - kvp_src.first.second) <= EPSILON); + // Layer heights are allowed do differ in case the layer height table is being overriden by the smooth profile. + // assert(std::abs(kvp_dst.second.option("layer_height")->getFloat() - kvp_src.second.option("layer_height")->getFloat()) <= EPSILON); + kvp_dst.second = kvp_src.second; + } +} + +static inline bool transform3d_lower(const Transform3d &lhs, const Transform3d &rhs) +{ + typedef Transform3d::Scalar T; + const T *lv = lhs.data(); + const T *rv = rhs.data(); + for (size_t i = 0; i < 16; ++ i, ++ lv, ++ rv) { + if (*lv < *rv) + return true; + else if (*lv > *rv) + return false; + } + return false; +} + +static inline bool transform3d_equal(const Transform3d &lhs, const Transform3d &rhs) +{ + typedef Transform3d::Scalar T; + const T *lv = lhs.data(); + const T *rv = rhs.data(); + for (size_t i = 0; i < 16; ++ i, ++ lv, ++ rv) + if (*lv != *rv) + return false; + return true; +} + +struct PrintObjectTrafoAndInstances +{ + Transform3d trafo; + PrintInstances instances; + bool operator<(const PrintObjectTrafoAndInstances &rhs) const { return transform3d_lower(this->trafo, rhs.trafo); } +}; + +// Generate a list of trafos and XY offsets for instances of a ModelObject +static std::vector print_objects_from_model_object(const ModelObject &model_object) +{ + std::set trafos; + PrintObjectTrafoAndInstances trafo; + for (ModelInstance *model_instance : model_object.instances) + if (model_instance->is_printable()) { + trafo.trafo = model_instance->get_matrix(); + auto shift = Point::new_scale(trafo.trafo.data()[12], trafo.trafo.data()[13]); + // Reset the XY axes of the transformation. + trafo.trafo.data()[12] = 0; + trafo.trafo.data()[13] = 0; + // Search or insert a trafo. + auto it = trafos.emplace(trafo).first; + const_cast(*it).instances.emplace_back(PrintInstance{ nullptr, model_instance, shift }); + } + return std::vector(trafos.begin(), trafos.end()); +} + +// Compare just the layer ranges and their layer heights, not the associated configs. +// Ignore the layer heights if check_layer_heights is false. +static bool layer_height_ranges_equal(const t_layer_config_ranges &lr1, const t_layer_config_ranges &lr2, bool check_layer_height) +{ + if (lr1.size() != lr2.size()) + return false; + auto it2 = lr2.begin(); + for (const auto &kvp1 : lr1) { + const auto &kvp2 = *it2 ++; + if (std::abs(kvp1.first.first - kvp2.first.first ) > EPSILON || + std::abs(kvp1.first.second - kvp2.first.second) > EPSILON || + (check_layer_height && std::abs(kvp1.second.option("layer_height")->getFloat() - kvp2.second.option("layer_height")->getFloat()) > EPSILON)) + return false; + } + return true; +} + +// Returns true if va == vb when all CustomGCode items that are not ToolChangeCode are ignored. +static bool custom_per_printz_gcodes_tool_changes_differ(const std::vector &va, const std::vector &vb) +{ + auto it_a = va.begin(); + auto it_b = vb.begin(); + while (it_a != va.end() || it_b != vb.end()) { + if (it_a != va.end() && it_a->type != CustomGCode::ToolChange) { + // Skip any CustomGCode items, which are not tool changes. + ++ it_a; + continue; + } + if (it_b != vb.end() && it_b->type != CustomGCode::ToolChange) { + // Skip any CustomGCode items, which are not tool changes. + ++ it_b; + continue; + } + if (it_a == va.end() || it_b == vb.end()) + // va or vb contains more Tool Changes than the other. + return true; + assert(it_a->type == CustomGCode::ToolChange); + assert(it_b->type == CustomGCode::ToolChange); + if (*it_a != *it_b) + // The two Tool Changes differ. + return true; + ++ it_a; + ++ it_b; + } + // There is no change in custom Tool Changes. + return false; +} + +// Collect changes to print config, account for overrides of extruder retract values by filament presets. +static t_config_option_keys print_config_diffs( + const PrintConfig ¤t_config, + const DynamicPrintConfig &new_full_config, + DynamicPrintConfig &filament_overrides) +{ + const std::vector &extruder_retract_keys = print_config_def.extruder_retract_keys(); + const std::string filament_prefix = "filament_"; + t_config_option_keys print_diff; + for (const t_config_option_key &opt_key : current_config.keys()) { + const ConfigOption *opt_old = current_config.option(opt_key); + assert(opt_old != nullptr); + const ConfigOption *opt_new = new_full_config.option(opt_key); + // assert(opt_new != nullptr); + if (opt_new == nullptr) + //FIXME This may happen when executing some test cases. + continue; + const ConfigOption *opt_new_filament = std::binary_search(extruder_retract_keys.begin(), extruder_retract_keys.end(), opt_key) ? new_full_config.option(filament_prefix + opt_key) : nullptr; + if (opt_new_filament != nullptr && ! opt_new_filament->is_nil()) { + // An extruder retract override is available at some of the filament presets. + bool overriden = opt_new->overriden_by(opt_new_filament); + if (overriden || *opt_old != *opt_new) { + auto opt_copy = opt_new->clone(); + opt_copy->apply_override(opt_new_filament); + bool changed = *opt_old != *opt_copy; + if (changed) + print_diff.emplace_back(opt_key); + if (changed || overriden) { + // filament_overrides will be applied to the placeholder parser, which layers these parameters over full_print_config. + filament_overrides.set_key_value(opt_key, opt_copy); + } else + delete opt_copy; + } + } else if (*opt_new != *opt_old) + print_diff.emplace_back(opt_key); + } + + return print_diff; +} + +// Prepare for storing of the full print config into new_full_config to be exported into the G-code and to be used by the PlaceholderParser. +static t_config_option_keys full_print_config_diffs(const DynamicPrintConfig ¤t_full_config, const DynamicPrintConfig &new_full_config) +{ + t_config_option_keys full_config_diff; + for (const t_config_option_key &opt_key : new_full_config.keys()) { + const ConfigOption *opt_old = current_full_config.option(opt_key); + const ConfigOption *opt_new = new_full_config.option(opt_key); + if (opt_old == nullptr || *opt_new != *opt_old) + full_config_diff.emplace_back(opt_key); + } + return full_config_diff; +} + +// Repository for solving partial overlaps of ModelObject::layer_config_ranges. +// Here the const DynamicPrintConfig* point to the config in ModelObject::layer_config_ranges. +class LayerRanges +{ +public: + struct LayerRange { + t_layer_height_range layer_height_range; + // Config is owned by the associated ModelObject. + const DynamicPrintConfig* config { nullptr }; + + bool operator<(const LayerRange &rhs) const throw() { return this->layer_height_range < rhs.layer_height_range; } + }; + + LayerRanges() = default; + LayerRanges(const t_layer_config_ranges &in) { this->assign(in); } + + // Convert input config ranges into continuous non-overlapping sorted vector of intervals and their configs. + void assign(const t_layer_config_ranges &in) { + m_ranges.clear(); + m_ranges.reserve(in.size()); + // Input ranges are sorted lexicographically. First range trims the other ranges. + coordf_t last_z = 0; + for (const std::pair &range : in) + if (range.first.second > last_z) { + coordf_t min_z = std::max(range.first.first, 0.); + if (min_z > last_z + EPSILON) { + m_ranges.push_back({ t_layer_height_range(last_z, min_z) }); + last_z = min_z; + } + if (range.first.second > last_z + EPSILON) { + const DynamicPrintConfig *cfg = &range.second.get(); + m_ranges.push_back({ t_layer_height_range(last_z, range.first.second), cfg }); + last_z = range.first.second; + } + } + if (m_ranges.empty()) + m_ranges.push_back({ t_layer_height_range(0, DBL_MAX) }); + else if (m_ranges.back().config == nullptr) + m_ranges.back().layer_height_range.second = DBL_MAX; + else + m_ranges.push_back({ t_layer_height_range(m_ranges.back().layer_height_range.second, DBL_MAX) }); + } + + const DynamicPrintConfig* config(const t_layer_height_range &range) const { + auto it = std::lower_bound(m_ranges.begin(), m_ranges.end(), LayerRange{ { range.first - EPSILON, range.second - EPSILON } }); + // #ys_FIXME_COLOR + // assert(it != m_ranges.end()); + // assert(it == m_ranges.end() || std::abs(it->first.first - range.first ) < EPSILON); + // assert(it == m_ranges.end() || std::abs(it->first.second - range.second) < EPSILON); + if (it == m_ranges.end() || + std::abs(it->layer_height_range.first - range.first) > EPSILON || + std::abs(it->layer_height_range.second - range.second) > EPSILON ) + return nullptr; // desired range doesn't found + return it == m_ranges.end() ? nullptr : it->config; + } + + std::vector::const_iterator begin() const { return m_ranges.cbegin(); } + std::vector::const_iterator end () const { return m_ranges.cend(); } + size_t size () const { return m_ranges.size(); } + +private: + // Layer ranges with their config overrides and list of volumes with their snug bounding boxes in a given layer range. + std::vector m_ranges; +}; + +// To track Model / ModelObject updates between the front end and back end, including layer height ranges, their configs, +// and snug bounding boxes of ModelVolumes. +struct ModelObjectStatus { + enum Status { + Unknown, + Old, + New, + Moved, + Deleted, + }; + + enum class PrintObjectRegionsStatus { + Invalid, + Valid, + PartiallyValid, + }; + + ModelObjectStatus(ObjectID id, Status status = Unknown) : id(id), status(status) {} + ~ModelObjectStatus() { if (print_object_regions) print_object_regions->ref_cnt_dec(); } + + // Key of the set. + ObjectID id; + // Status of this ModelObject with id on apply(). + Status status; + // PrintObjects to be generated for this ModelObject including their base transformation. + std::vector print_instances; + // Regions shared by the associated PrintObjects. + PrintObjectRegions *print_object_regions { nullptr }; + // Status of the above. + PrintObjectRegionsStatus print_object_regions_status { PrintObjectRegionsStatus::Invalid }; + + // Search by id. + bool operator<(const ModelObjectStatus &rhs) const { return id < rhs.id; } +}; + +struct ModelObjectStatusDB +{ + void add(const ModelObject &model_object, const ModelObjectStatus::Status status) { + assert(db.find(ModelObjectStatus(model_object.id())) == db.end()); + db.emplace(model_object.id(), status); + } + + bool add_if_new(const ModelObject &model_object, const ModelObjectStatus::Status status) { + auto it = db.find(ModelObjectStatus(model_object.id())); + if (it == db.end()) { + db.emplace_hint(it, model_object.id(), status); + return true; + } + return false; + } + + const ModelObjectStatus& get(const ModelObject &model_object) { + auto it = db.find(ModelObjectStatus(model_object.id())); + assert(it != db.end()); + return *it; + } + + const ModelObjectStatus& reuse(const ModelObject &model_object) { + const ModelObjectStatus &result = this->get(model_object); + assert(result.status != ModelObjectStatus::Deleted); + return result; + } + + std::set db; +}; + +struct PrintObjectStatus { + enum Status { + Unknown, + Deleted, + Reused, + New + }; + + PrintObjectStatus(PrintObject *print_object, Status status = Unknown) : + id(print_object->model_object()->id()), + print_object(print_object), + trafo(print_object->trafo()), + status(status) {} + PrintObjectStatus(ObjectID id) : id(id), print_object(nullptr), trafo(Transform3d::Identity()), status(Unknown) {} + + // ID of the ModelObject & PrintObject + ObjectID id; + // Pointer to the old PrintObject + PrintObject *print_object; + // Trafo generated with model_object->world_matrix(true) + Transform3d trafo; + Status status; + + // Search by id. + bool operator<(const PrintObjectStatus &rhs) const { return id < rhs.id; } +}; + +class PrintObjectStatusDB { +public: + using iterator = std::multiset::iterator; + using const_iterator = std::multiset::const_iterator; + + PrintObjectStatusDB(const PrintObjectPtrs &print_objects) { + for (PrintObject *print_object : print_objects) + m_db.emplace(PrintObjectStatus(print_object)); + } + + struct iterator_range : std::pair + { + using std::pair::pair; + iterator_range(const std::pair in) : std::pair(in) {} + + const_iterator begin() throw() { return this->first; } + const_iterator end() throw() { return this->second; } + }; + + iterator_range get_range(const ModelObject &model_object) const { + return m_db.equal_range(PrintObjectStatus(model_object.id())); + } + + iterator_range get_range(const ModelObjectStatus &model_object_status) const { + return m_db.equal_range(PrintObjectStatus(model_object_status.id)); + } + + size_t count(const ModelObject &model_object) { + return m_db.count(PrintObjectStatus(model_object.id())); + } + + std::multiset::iterator begin() { return m_db.begin(); } + std::multiset::iterator end() { return m_db.end(); } + + void clear() { + m_db.clear(); + } + +private: + std::multiset m_db; +}; + +static inline bool model_volume_solid_or_modifier(const ModelVolume &mv) +{ + ModelVolumeType type = mv.type(); + return type == ModelVolumeType::MODEL_PART || type == ModelVolumeType::NEGATIVE_VOLUME || type == ModelVolumeType::PARAMETER_MODIFIER; +} + +static inline Transform3f trafo_for_bbox(const Transform3d &object_trafo, const Transform3d &volume_trafo) +{ + Transform3d m = object_trafo * volume_trafo; + m.translation().x() = 0.; + m.translation().y() = 0.; + return m.cast(); +} + +static inline bool trafos_differ_in_rotation_by_z_and_mirroring_by_xy_only(const Transform3d &t1, const Transform3d &t2) +{ + if (std::abs(t1.translation().z() - t2.translation().z()) > EPSILON) + // One of the object is higher than the other above the build plate (or below the build plate). + return false; + Matrix3d m1 = t1.matrix().block<3, 3>(0, 0); + Matrix3d m2 = t2.matrix().block<3, 3>(0, 0); + Matrix3d m = m2.inverse() * m1; + Vec3d z = m.block<3, 1>(0, 2); + if (std::abs(z.x()) > EPSILON || std::abs(z.y()) > EPSILON || std::abs(z.z() - 1.) > EPSILON) + // Z direction or length changed. + return false; + // Z still points in the same direction and it has the same length. + Vec3d x = m.block<3, 1>(0, 0); + Vec3d y = m.block<3, 1>(0, 1); + if (std::abs(x.z()) > EPSILON || std::abs(y.z()) > EPSILON) + return false; + double lx2 = x.squaredNorm(); + double ly2 = y.squaredNorm(); + if (lx2 - 1. > EPSILON * EPSILON || ly2 - 1. > EPSILON * EPSILON) + return false; + // Verify whether the vectors x, y are still perpendicular. + double d = x.dot(y); + return std::abs(d * d) < EPSILON * lx2 * ly2; +} + +static PrintObjectRegions::BoundingBox transformed_its_bbox2d(const indexed_triangle_set &its, const Transform3f &m, float offset) +{ + assert(! its.indices.empty()); + + PrintObjectRegions::BoundingBox bbox(m * its.vertices[its.indices.front()(0)]); + for (const stl_triangle_vertex_indices &tri : its.indices) + for (int i = 0; i < 3; ++ i) + bbox.extend(m * its.vertices[tri(i)]); + bbox.min() -= Vec3f(offset, offset, float(EPSILON)); + bbox.max() += Vec3f(offset, offset, float(EPSILON)); + return bbox; +} + +static void transformed_its_bboxes_in_z_ranges( + const indexed_triangle_set &its, + const Transform3f &m, + const std::vector &z_ranges, + std::vector> &bboxes, + const float offset) +{ + bboxes.assign(z_ranges.size(), std::make_pair(PrintObjectRegions::BoundingBox(), false)); + for (const stl_triangle_vertex_indices &tri : its.indices) { + const Vec3f pts[3] = { m * its.vertices[tri(0)], m * its.vertices[tri(1)], m * its.vertices[tri(2)] }; + for (size_t irange = 0; irange < z_ranges.size(); ++ irange) { + const t_layer_height_range &z_range = z_ranges[irange]; + std::pair &bbox = bboxes[irange]; + auto bbox_extend = [&bbox](const Vec3f& p) { + if (bbox.second) { + bbox.first.extend(p); + } else { + bbox.first.min() = bbox.first.max() = p; + bbox.second = true; + } + }; + int iprev = 2; + for (int iedge = 0; iedge < 3; ++ iedge) { + const Vec3f *p1 = &pts[iprev]; + const Vec3f *p2 = &pts[iedge]; + // Sort the edge points by Z. + if (p1->z() > p2->z()) + std::swap(p1, p2); + if (p2->z() <= z_range.first || p1->z() >= z_range.second) { + // Out of this slab. + } else if (p1->z() < z_range.first) { + if (p1->z() > z_range.second) { + // Two intersections. + float zspan = p2->z() - p1->z(); + float t1 = (z_range.first - p1->z()) / zspan; + float t2 = (z_range.second - p1->z()) / zspan; + Vec2f p = to_2d(*p1); + Vec2f v(p2->x() - p1->x(), p2->y() - p1->y()); + bbox_extend(to_3d((p + v * t1).eval(), float(z_range.first))); + bbox_extend(to_3d((p + v * t2).eval(), float(z_range.second))); + } else { + // Single intersection with the lower limit. + float t = (z_range.first - p1->z()) / (p2->z() - p1->z()); + Vec2f v(p2->x() - p1->x(), p2->y() - p1->y()); + bbox_extend(to_3d((to_2d(*p1) + v * t).eval(), float(z_range.first))); + bbox_extend(*p2); + } + } else if (p2->z() > z_range.second) { + // Single intersection with the upper limit. + float t = (z_range.second - p1->z()) / (p2->z() - p1->z()); + Vec2f v(p2->x() - p1->x(), p2->y() - p1->y()); + bbox_extend(to_3d((to_2d(*p1) + v * t).eval(), float(z_range.second))); + bbox_extend(*p1); + } else { + // Both points are inside. + bbox_extend(*p1); + bbox_extend(*p2); + } + iprev = iedge; + } + } + } + + for (std::pair &bbox : bboxes) { + bbox.first.min() -= Vec3f(offset, offset, float(EPSILON)); + bbox.first.max() += Vec3f(offset, offset, float(EPSILON)); + } +} + +// Last PrintObject for this print_object_regions has been fully invalidated (deleted). +// Keep print_object_regions, but delete those volumes, which were either removed from new_volumes, or which rotated or scaled, so they need +// their bounding boxes to be recalculated. +void print_objects_regions_invalidate_keep_some_volumes(PrintObjectRegions &print_object_regions, ModelVolumePtrs old_volumes, ModelVolumePtrs new_volumes) +{ + print_object_regions.all_regions.clear(); + + model_volumes_sort_by_id(old_volumes); + model_volumes_sort_by_id(new_volumes); + + size_t i_cached_volume = 0; + size_t last_cached_volume = 0; + size_t i_old = 0; + for (size_t i_new = 0; i_new < new_volumes.size(); ++ i_new) + if (model_volume_solid_or_modifier(*new_volumes[i_new])) { + for (; i_old < old_volumes.size(); ++ i_old) + if (old_volumes[i_old]->id() >= new_volumes[i_new]->id()) + break; + if (i_old != old_volumes.size() && old_volumes[i_old]->id() == new_volumes[i_new]->id()) { + if (old_volumes[i_old]->get_matrix().isApprox(new_volumes[i_new]->get_matrix())) { + // Reuse the volume. + for (; print_object_regions.cached_volume_ids[i_cached_volume] < old_volumes[i_old]->id(); ++ i_cached_volume) + assert(i_cached_volume < print_object_regions.cached_volume_ids.size()); + assert(i_cached_volume < print_object_regions.cached_volume_ids.size() && print_object_regions.cached_volume_ids[i_cached_volume] == old_volumes[i_old]->id()); + print_object_regions.cached_volume_ids[last_cached_volume ++] = print_object_regions.cached_volume_ids[i_cached_volume ++]; + } else { + // Don't reuse the volume. + } + } + } + print_object_regions.cached_volume_ids.erase(print_object_regions.cached_volume_ids.begin() + last_cached_volume, print_object_regions.cached_volume_ids.end()); +} + +// Find a bounding box of a volume's part intersecting layer_range. Such a bounding box will likely be smaller in XY than the full bounding box, +// thus it will intersect with lower number of other volumes. +const PrintObjectRegions::BoundingBox* find_volume_extents(const PrintObjectRegions::LayerRangeRegions &layer_range, const ModelVolume &volume) +{ + auto it = lower_bound_by_predicate(layer_range.volumes.begin(), layer_range.volumes.end(), [&volume](const PrintObjectRegions::VolumeExtents &l){ return l.volume_id < volume.id(); }); + return it != layer_range.volumes.end() && it->volume_id == volume.id() ? &it->bbox : nullptr; +} + +// Find a bounding box of a topmost printable volume referenced by this modifier given this_region_id. +PrintObjectRegions::BoundingBox find_modifier_volume_extents(const PrintObjectRegions::LayerRangeRegions &layer_range, const int this_region_id) +{ + // Find the top-most printable volume of this modifier, or the printable volume itself. + const PrintObjectRegions::VolumeRegion &this_region = layer_range.volume_regions[this_region_id]; + const PrintObjectRegions::BoundingBox *this_extents = find_volume_extents(layer_range, *this_region.model_volume); + assert(this_extents); + PrintObjectRegions::BoundingBox out { *this_extents }; + if (! this_region.model_volume->is_model_part()) + for (int parent_region_id = this_region.parent;;) { + assert(parent_region_id >= 0); + const PrintObjectRegions::VolumeRegion &parent_region = layer_range.volume_regions[parent_region_id]; + const PrintObjectRegions::BoundingBox *parent_extents = find_volume_extents(layer_range, *parent_region.model_volume); + assert(parent_extents); + out.extend(*parent_extents); + if (parent_region.model_volume->is_model_part()) + break; + parent_region_id = parent_region.parent; + } + return out; +} + +PrintRegionConfig region_config_from_model_volume(const PrintRegionConfig &default_or_parent_region_config, const DynamicPrintConfig *layer_range_config, const ModelVolume &volume, size_t num_extruders); + +void print_region_ref_inc(PrintRegion &r) { ++ r.m_ref_cnt; } +void print_region_ref_reset(PrintRegion &r) { r.m_ref_cnt = 0; } +int print_region_ref_cnt(const PrintRegion &r) { return r.m_ref_cnt; } + +// Verify whether the PrintRegions of a PrintObject are still valid, possibly after updating the region configs. +// Before region configs are updated, callback_invalidate() is called to possibly stop background processing. +// Returns false if this object needs to be resliced because regions were merged or split. +bool verify_update_print_object_regions( + ModelVolumePtrs model_volumes, + const PrintRegionConfig &default_region_config, + size_t num_extruders, + const std::vector &painting_extruders, + PrintObjectRegions &print_object_regions, + const std::function &callback_invalidate) +{ + // Sort by ModelVolume ID. + model_volumes_sort_by_id(model_volumes); + + for (std::unique_ptr ®ion : print_object_regions.all_regions) + print_region_ref_reset(*region); + + // Verify and / or update PrintRegions produced by ModelVolumes, layer range modifiers, modifier volumes. + for (PrintObjectRegions::LayerRangeRegions &layer_range : print_object_regions.layer_ranges) { + // Each modifier ModelVolume intersecting this layer_range shall be referenced here at least once if it intersects some + // printable ModelVolume at this layer_range even if it does not modify its overlapping printable ModelVolume configuration yet. + // VolumeRegions reference ModelVolumes in layer_range.volume_regions the order they are stored in ModelObject volumes. + // Remember whether a given modifier ModelVolume was visited already. + auto it_model_volume_modifier_last = model_volumes.end(); + for (PrintObjectRegions::VolumeRegion ®ion : layer_range.volume_regions) + if (region.model_volume->is_model_part() || region.model_volume->is_modifier()) { + auto it_model_volume = lower_bound_by_predicate(model_volumes.begin(), model_volumes.end(), [®ion](const ModelVolume *l){ return l->id() < region.model_volume->id(); }); + assert(it_model_volume != model_volumes.end() && (*it_model_volume)->id() == region.model_volume->id()); + if (region.model_volume->is_modifier() && it_model_volume != it_model_volume_modifier_last) { + // A modifier ModelVolume is visited for the first time. + // A visited modifier may not have had parent volume_regions created overlapping with some model parts or modifiers, + // if the visited modifier did not modify their properties. Now the visited modifier's configuration may have changed, + // which may require new regions to be created. + it_model_volume_modifier_last = it_model_volume; + int next_region_id = int(®ion - layer_range.volume_regions.data()); + const PrintObjectRegions::BoundingBox *bbox = find_volume_extents(layer_range, *region.model_volume); + assert(bbox); + for (int parent_region_id = next_region_id - 1; parent_region_id >= 0; -- parent_region_id) { + const PrintObjectRegions::VolumeRegion &parent_region = layer_range.volume_regions[parent_region_id]; + assert(parent_region.model_volume != region.model_volume); + if (parent_region.model_volume->is_model_part() || parent_region.model_volume->is_modifier()) { + // volume_regions are produced in decreasing order of parent volume_regions ids. + // Some regions may not have been generated the last time by generate_print_object_regions(). + assert(next_region_id == int(layer_range.volume_regions.size()) || + layer_range.volume_regions[next_region_id].model_volume != region.model_volume || + layer_range.volume_regions[next_region_id].parent <= parent_region_id); + if (next_region_id < int(layer_range.volume_regions.size()) && + layer_range.volume_regions[next_region_id].model_volume == region.model_volume && + layer_range.volume_regions[next_region_id].parent == parent_region_id) { + // A parent region is already overridden. + ++ next_region_id; + } else if (PrintObjectRegions::BoundingBox parent_bbox = find_modifier_volume_extents(layer_range, parent_region_id); parent_bbox.intersects(*bbox)) + // Such parent region does not exist. If it is needed, then we need to reslice. + // Only create new region for a modifier, which actually modifies config of it's parent. + if (PrintRegionConfig config = region_config_from_model_volume(parent_region.region->config(), nullptr, **it_model_volume, num_extruders); + config != parent_region.region->config()) + // This modifier newly overrides a region, which it did not before. We need to reslice. + return false; + } + } + } + PrintRegionConfig cfg = region.parent == -1 ? + region_config_from_model_volume(default_region_config, layer_range.config, **it_model_volume, num_extruders) : + region_config_from_model_volume(layer_range.volume_regions[region.parent].region->config(), nullptr, **it_model_volume, num_extruders); + if (cfg != region.region->config()) { + // Region configuration changed. + if (print_region_ref_cnt(*region.region) == 0) { + // Region is referenced for the first time. Just change its parameters. + // Stop the background process before assigning new configuration to the regions. + t_config_option_keys diff = region.region->config().diff(cfg); + callback_invalidate(region.region->config(), cfg, diff); + region.region->config_apply_only(cfg, diff, false); + } else { + // Region is referenced multiple times, thus the region is being split. We need to reslice. + return false; + } + } + print_region_ref_inc(*region.region); + } + } + + // Verify and / or update PrintRegions produced by color painting. + for (const PrintObjectRegions::LayerRangeRegions &layer_range : print_object_regions.layer_ranges) + for (const PrintObjectRegions::PaintedRegion ®ion : layer_range.painted_regions) { + const PrintObjectRegions::VolumeRegion &parent_region = layer_range.volume_regions[region.parent]; + PrintRegionConfig cfg = parent_region.region->config(); + cfg.perimeter_extruder.value = region.extruder_id; + cfg.solid_infill_extruder.value = region.extruder_id; + cfg.infill_extruder.value = region.extruder_id; + if (cfg != region.region->config()) { + // Region configuration changed. + if (print_region_ref_cnt(*region.region) == 0) { + // Region is referenced for the first time. Just change its parameters. + // Stop the background process before assigning new configuration to the regions. + t_config_option_keys diff = region.region->config().diff(cfg); + callback_invalidate(region.region->config(), cfg, diff); + region.region->config_apply_only(cfg, diff, false); + } else { + // Region is referenced multiple times, thus the region is being split. We need to reslice. + return false; + } + } + print_region_ref_inc(*region.region); + } + + // Lastly verify, whether some regions were not merged. + { + std::vector regions; + regions.reserve(print_object_regions.all_regions.size()); + for (std::unique_ptr ®ion : print_object_regions.all_regions) { + assert(print_region_ref_cnt(*region) > 0); + regions.emplace_back(&(*region.get())); + } + std::sort(regions.begin(), regions.end(), [](const PrintRegion *l, const PrintRegion *r){ return l->config_hash() < r->config_hash(); }); + for (size_t i = 0; i < regions.size(); ++ i) { + size_t hash = regions[i]->config_hash(); + size_t j = i; + for (++ j; j < regions.size() && regions[j]->config_hash() == hash; ++ j) + if (regions[i]->config() == regions[j]->config()) { + // Regions were merged. We need to reslice. + return false; + } + } + } + + return true; +} + +// Update caches of volume bounding boxes. +void update_volume_bboxes( + std::vector &layer_ranges, + std::vector &cached_volume_ids, + ModelVolumePtrs model_volumes, + const Transform3d &object_trafo, + const float offset) +{ + // output will be sorted by the order of model_volumes sorted by their ObjectIDs. + model_volumes_sort_by_id(model_volumes); + + if (layer_ranges.size() == 1) { + PrintObjectRegions::LayerRangeRegions &layer_range = layer_ranges.front(); + std::vector volumes_old(std::move(layer_range.volumes)); + layer_range.volumes.reserve(model_volumes.size()); + for (const ModelVolume *model_volume : model_volumes) + if (model_volume_solid_or_modifier(*model_volume)) { + if (std::binary_search(cached_volume_ids.begin(), cached_volume_ids.end(), model_volume->id())) { + auto it = lower_bound_by_predicate(volumes_old.begin(), volumes_old.end(), [model_volume](PrintObjectRegions::VolumeExtents &l) { return l.volume_id < model_volume->id(); }); + if (it != volumes_old.end() && it->volume_id == model_volume->id()) + layer_range.volumes.emplace_back(*it); + } else +#if ENABLE_WORLD_COORDINATE + layer_range.volumes.push_back({ model_volume->id(), + transformed_its_bbox2d(model_volume->mesh().its, trafo_for_bbox(object_trafo, model_volume->get_matrix()), offset) }); +#else + layer_range.volumes.push_back({ model_volume->id(), + transformed_its_bbox2d(model_volume->mesh().its, trafo_for_bbox(object_trafo, model_volume->get_matrix(false)), offset) }); +#endif // ENABLE_WORLD_COORDINATE + } + } else { + std::vector> volumes_old; + if (cached_volume_ids.empty()) + for (PrintObjectRegions::LayerRangeRegions &layer_range : layer_ranges) + layer_range.volumes.clear(); + else { + volumes_old.reserve(layer_ranges.size()); + for (PrintObjectRegions::LayerRangeRegions &layer_range : layer_ranges) + volumes_old.emplace_back(std::move(layer_range.volumes)); + } + + std::vector> bboxes; + std::vector ranges; + ranges.reserve(layer_ranges.size()); + for (const PrintObjectRegions::LayerRangeRegions &layer_range : layer_ranges) { + t_layer_height_range r = layer_range.layer_height_range; + r.first -= EPSILON; + r.second += EPSILON; + ranges.emplace_back(r); + } + for (const ModelVolume *model_volume : model_volumes) + if (model_volume_solid_or_modifier(*model_volume)) { + if (std::binary_search(cached_volume_ids.begin(), cached_volume_ids.end(), model_volume->id())) { + for (PrintObjectRegions::LayerRangeRegions &layer_range : layer_ranges) { + const auto &vold = volumes_old[&layer_range - layer_ranges.data()]; + auto it = lower_bound_by_predicate(vold.begin(), vold.end(), [model_volume](const PrintObjectRegions::VolumeExtents &l) { return l.volume_id < model_volume->id(); }); + if (it != vold.end() && it->volume_id == model_volume->id()) + layer_range.volumes.emplace_back(*it); + } + } else { +#if ENABLE_WORLD_COORDINATE + transformed_its_bboxes_in_z_ranges(model_volume->mesh().its, trafo_for_bbox(object_trafo, model_volume->get_matrix()), ranges, bboxes, offset); +#else + transformed_its_bboxes_in_z_ranges(model_volume->mesh().its, trafo_for_bbox(object_trafo, model_volume->get_matrix(false)), ranges, bboxes, offset); +#endif // ENABLE_WORLD_COORDINATE + for (PrintObjectRegions::LayerRangeRegions &layer_range : layer_ranges) + if (auto &bbox = bboxes[&layer_range - layer_ranges.data()]; bbox.second) + layer_range.volumes.push_back({ model_volume->id(), bbox.first }); + } + } + } + + cached_volume_ids.clear(); + cached_volume_ids.reserve(model_volumes.size()); + for (const ModelVolume *v : model_volumes) + if (model_volume_solid_or_modifier(*v)) + cached_volume_ids.emplace_back(v->id()); +} + +// Either a fresh PrintObject, or PrintObject regions were invalidated (merged, split). +// Generate PrintRegions from scratch. +static PrintObjectRegions* generate_print_object_regions( + PrintObjectRegions *print_object_regions_old, + const ModelVolumePtrs &model_volumes, + const LayerRanges &model_layer_ranges, + const PrintRegionConfig &default_region_config, + const Transform3d &trafo, + size_t num_extruders, + const float xy_size_compensation, + const std::vector &painting_extruders) +{ + // Reuse the old object or generate a new one. + auto out = print_object_regions_old ? std::unique_ptr(print_object_regions_old) : std::make_unique(); + auto &all_regions = out->all_regions; + auto &layer_ranges_regions = out->layer_ranges; + + all_regions.clear(); + + bool reuse_old = print_object_regions_old && !print_object_regions_old->layer_ranges.empty(); + + if (reuse_old) { + // Reuse old bounding boxes of some ModelVolumes and their ranges. + // Verify that the old ranges match the new ranges. + assert(model_layer_ranges.size() == layer_ranges_regions.size()); + for (const auto &range : model_layer_ranges) { + PrintObjectRegions::LayerRangeRegions &r = layer_ranges_regions[&range - &*model_layer_ranges.begin()]; + assert(range.layer_height_range == r.layer_height_range); + // If model::assign_copy() is called, layer_ranges_regions is copied thus the pointers to configs are lost. + r.config = range.config; + r.volume_regions.clear(); + r.painted_regions.clear(); + } + } else { + out->trafo_bboxes = trafo; + layer_ranges_regions.reserve(model_layer_ranges.size()); + for (const auto &range : model_layer_ranges) + layer_ranges_regions.push_back({ range.layer_height_range, range.config }); + } + + const bool is_mm_painted = num_extruders > 1 && std::any_of(model_volumes.cbegin(), model_volumes.cend(), [](const ModelVolume *mv) { return mv->is_mm_painted(); }); + update_volume_bboxes(layer_ranges_regions, out->cached_volume_ids, model_volumes, out->trafo_bboxes, is_mm_painted ? 0.f : std::max(0.f, xy_size_compensation)); + + std::vector region_set; + auto get_create_region = [®ion_set, &all_regions](PrintRegionConfig &&config) -> PrintRegion* { + size_t hash = config.hash(); + auto it = Slic3r::lower_bound_by_predicate(region_set.begin(), region_set.end(), [&config, hash](const PrintRegion* l) { + return l->config_hash() < hash || (l->config_hash() == hash && l->config() < config); }); + if (it != region_set.end() && (*it)->config_hash() == hash && (*it)->config() == config) + return *it; + // Insert into a sorted array, it has O(n) complexity, but the calling algorithm has an O(n^2*log(n)) complexity anyways. + all_regions.emplace_back(std::make_unique(std::move(config), hash, int(all_regions.size()))); + PrintRegion *region = all_regions.back().get(); + region_set.emplace(it, region); + return region; + }; + + // Chain the regions in the order they are stored in the volumes list. + for (int volume_id = 0; volume_id < int(model_volumes.size()); ++ volume_id) { + const ModelVolume &volume = *model_volumes[volume_id]; + if (model_volume_solid_or_modifier(volume)) { + for (PrintObjectRegions::LayerRangeRegions &layer_range : layer_ranges_regions) + if (const PrintObjectRegions::BoundingBox *bbox = find_volume_extents(layer_range, volume); bbox) { + if (volume.is_model_part()) { + // Add a model volume, assign an existing region or generate a new one. + layer_range.volume_regions.push_back({ + &volume, -1, + get_create_region(region_config_from_model_volume(default_region_config, layer_range.config, volume, num_extruders)), + bbox + }); + } else if (volume.is_negative_volume()) { + // Add a negative (subtractor) volume. Such volume has neither region nor parent volume assigned. + layer_range.volume_regions.push_back({ &volume, -1, nullptr, bbox }); + } else { + assert(volume.is_modifier()); + // Modifiers may be chained one over the other. Check for overlap, merge DynamicPrintConfigs. + bool added = false; + int parent_model_part_id = -1; + for (int parent_region_id = int(layer_range.volume_regions.size()) - 1; parent_region_id >= 0; -- parent_region_id) { + const PrintObjectRegions::VolumeRegion &parent_region = layer_range.volume_regions[parent_region_id]; + const ModelVolume &parent_volume = *parent_region.model_volume; + if (parent_volume.is_model_part() || parent_volume.is_modifier()) + if (PrintObjectRegions::BoundingBox parent_bbox = find_modifier_volume_extents(layer_range, parent_region_id); parent_bbox.intersects(*bbox)) { + // Only create new region for a modifier, which actually modifies config of it's parent. + if (PrintRegionConfig config = region_config_from_model_volume(parent_region.region->config(), nullptr, volume, num_extruders); + config != parent_region.region->config()) { + added = true; + layer_range.volume_regions.push_back({ &volume, parent_region_id, get_create_region(std::move(config)), bbox }); + } else if (parent_model_part_id == -1 && parent_volume.is_model_part()) + parent_model_part_id = parent_region_id; + } + } + if (! added && parent_model_part_id >= 0) + // This modifier does not override any printable volume's configuration, however it may in the future. + // Store it so that verify_update_print_object_regions() will handle this modifier correctly if its configuration changes. + layer_range.volume_regions.push_back({ &volume, parent_model_part_id, layer_range.volume_regions[parent_model_part_id].region, bbox }); + } + } + } + } + + // Finally add painting regions. + for (PrintObjectRegions::LayerRangeRegions &layer_range : layer_ranges_regions) { + for (unsigned int painted_extruder_id : painting_extruders) + for (int parent_region_id = 0; parent_region_id < int(layer_range.volume_regions.size()); ++ parent_region_id) + if (const PrintObjectRegions::VolumeRegion &parent_region = layer_range.volume_regions[parent_region_id]; + parent_region.model_volume->is_model_part() || parent_region.model_volume->is_modifier()) { + PrintRegionConfig cfg = parent_region.region->config(); + cfg.perimeter_extruder.value = painted_extruder_id; + cfg.solid_infill_extruder.value = painted_extruder_id; + cfg.infill_extruder.value = painted_extruder_id; + layer_range.painted_regions.push_back({ painted_extruder_id, parent_region_id, get_create_region(std::move(cfg))}); + } + // Sort the regions by parent region::print_object_region_id() and extruder_id to help the slicing algorithm when applying MMU segmentation. + std::sort(layer_range.painted_regions.begin(), layer_range.painted_regions.end(), [&layer_range](auto &l, auto &r) { + int lid = layer_range.volume_regions[l.parent].region->print_object_region_id(); + int rid = layer_range.volume_regions[r.parent].region->print_object_region_id(); + return lid < rid || (lid == rid && l.extruder_id < r.extruder_id); }); + } + + return out.release(); +} + +Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_config) +{ +#ifdef _DEBUG + check_model_ids_validity(model); +#endif /* _DEBUG */ + + // Normalize the config. + new_full_config.option("print_settings_id", true); + new_full_config.option("filament_settings_id", true); + new_full_config.option("printer_settings_id", true); + new_full_config.option("physical_printer_settings_id", true); + new_full_config.normalize_fdm(); + + // Find modified keys of the various configs. Resolve overrides extruder retract values by filament profiles. + DynamicPrintConfig filament_overrides; + t_config_option_keys print_diff = print_config_diffs(m_config, new_full_config, filament_overrides); + t_config_option_keys full_config_diff = full_print_config_diffs(m_full_print_config, new_full_config); + // Collect changes to object and region configs. + t_config_option_keys object_diff = m_default_object_config.diff(new_full_config); + t_config_option_keys region_diff = m_default_region_config.diff(new_full_config); + + // Do not use the ApplyStatus as we will use the max function when updating apply_status. + unsigned int apply_status = APPLY_STATUS_UNCHANGED; + auto update_apply_status = [&apply_status](bool invalidated) + { apply_status = std::max(apply_status, invalidated ? APPLY_STATUS_INVALIDATED : APPLY_STATUS_CHANGED); }; + if (! (print_diff.empty() && object_diff.empty() && region_diff.empty())) + update_apply_status(false); + + // Grab the lock for the Print / PrintObject milestones. + std::scoped_lock lock(this->state_mutex()); + + // The following call may stop the background processing. + if (! print_diff.empty()) + update_apply_status(this->invalidate_state_by_config_options(new_full_config, print_diff)); + + // Apply variables to placeholder parser. The placeholder parser is used by G-code export, + // which should be stopped if print_diff is not empty. + size_t num_extruders = m_config.nozzle_diameter.size(); + bool num_extruders_changed = false; + if (! full_config_diff.empty()) { + update_apply_status(this->invalidate_step(psGCodeExport)); + m_placeholder_parser.clear_config(); + // Set the profile aliases for the PrintBase::output_filename() + m_placeholder_parser.set("print_preset", new_full_config.option("print_settings_id")->clone()); + m_placeholder_parser.set("filament_preset", new_full_config.option("filament_settings_id")->clone()); + m_placeholder_parser.set("printer_preset", new_full_config.option("printer_settings_id")->clone()); + m_placeholder_parser.set("physical_printer_preset", new_full_config.option("physical_printer_settings_id")->clone()); + // We want the filament overrides to be applied over their respective extruder parameters by the PlaceholderParser. + // see "Placeholders do not respect filament overrides." GH issue #3649 + m_placeholder_parser.apply_config(filament_overrides); + // It is also safe to change m_config now after this->invalidate_state_by_config_options() call. + m_config.apply_only(new_full_config, print_diff, true); + //FIXME use move semantics once ConfigBase supports it. + // Some filament_overrides may contain values different from new_full_config, but equal to m_config. + // As long as these config options don't reallocate memory when copying, we are safe overriding a value, which is in use by a worker thread. + m_config.apply(filament_overrides); + // Handle changes to object config defaults + m_default_object_config.apply_only(new_full_config, object_diff, true); + // Handle changes to regions config defaults + m_default_region_config.apply_only(new_full_config, region_diff, true); + m_full_print_config = std::move(new_full_config); + if (num_extruders != m_config.nozzle_diameter.size()) { + num_extruders = m_config.nozzle_diameter.size(); + num_extruders_changed = true; + } + } + + ModelObjectStatusDB model_object_status_db; + + // 1) Synchronize model objects. + bool print_regions_reshuffled = false; + if (model.id() != m_model.id()) { + // Kill everything, initialize from scratch. + // Stop background processing. + this->call_cancel_callback(); + update_apply_status(this->invalidate_all_steps()); + for (PrintObject *object : m_objects) { + model_object_status_db.add(*object->model_object(), ModelObjectStatus::Deleted); + update_apply_status(object->invalidate_all_steps()); + delete object; + } + m_objects.clear(); + print_regions_reshuffled = true; + m_model.assign_copy(model); + for (const ModelObject *model_object : m_model.objects) + model_object_status_db.add(*model_object, ModelObjectStatus::New); + } else { + if (m_model.custom_gcode_per_print_z != model.custom_gcode_per_print_z) { + update_apply_status(num_extruders_changed || + // Tool change G-codes are applied as color changes for a single extruder printer, no need to invalidate tool ordering. + //FIXME The tool ordering may be invalidated unnecessarily if the custom_gcode_per_print_z.mode is not applicable + // to the active print / model state, and then it is reset, so it is being applicable, but empty, thus the effect is the same. + (num_extruders > 1 && custom_per_printz_gcodes_tool_changes_differ(m_model.custom_gcode_per_print_z.gcodes, model.custom_gcode_per_print_z.gcodes)) ? + // The Tool Ordering and the Wipe Tower are no more valid. + this->invalidate_steps({ psWipeTower, psGCodeExport }) : + // There is no change in Tool Changes stored in custom_gcode_per_print_z, therefore there is no need to update Tool Ordering. + this->invalidate_step(psGCodeExport)); + m_model.custom_gcode_per_print_z = model.custom_gcode_per_print_z; + } + if (model_object_list_equal(m_model, model)) { + // The object list did not change. + for (const ModelObject *model_object : m_model.objects) + model_object_status_db.add(*model_object, ModelObjectStatus::Old); + } else if (model_object_list_extended(m_model, model)) { + // Add new objects. Their volumes and configs will be synchronized later. + update_apply_status(this->invalidate_step(psGCodeExport)); + for (const ModelObject *model_object : m_model.objects) + model_object_status_db.add(*model_object, ModelObjectStatus::Old); + for (size_t i = m_model.objects.size(); i < model.objects.size(); ++ i) { + model_object_status_db.add(*model.objects[i], ModelObjectStatus::New); + m_model.objects.emplace_back(ModelObject::new_copy(*model.objects[i])); + m_model.objects.back()->set_model(&m_model); + } + } else { + // Reorder the objects, add new objects. + // First stop background processing before shuffling or deleting the PrintObjects in the object list. + this->call_cancel_callback(); + update_apply_status(this->invalidate_step(psGCodeExport)); + // Second create a new list of objects. + std::vector model_objects_old(std::move(m_model.objects)); + m_model.objects.clear(); + m_model.objects.reserve(model.objects.size()); + auto by_id_lower = [](const ModelObject *lhs, const ModelObject *rhs){ return lhs->id() < rhs->id(); }; + std::sort(model_objects_old.begin(), model_objects_old.end(), by_id_lower); + for (const ModelObject *mobj : model.objects) { + auto it = std::lower_bound(model_objects_old.begin(), model_objects_old.end(), mobj, by_id_lower); + if (it == model_objects_old.end() || (*it)->id() != mobj->id()) { + // New ModelObject added. + m_model.objects.emplace_back(ModelObject::new_copy(*mobj)); + m_model.objects.back()->set_model(&m_model); + model_object_status_db.add(*mobj, ModelObjectStatus::New); + } else { + // Existing ModelObject re-added (possibly moved in the list). + m_model.objects.emplace_back(*it); + model_object_status_db.add(*mobj, ModelObjectStatus::Moved); + } + } + bool deleted_any = false; + for (ModelObject *&model_object : model_objects_old) + if (model_object_status_db.add_if_new(*model_object, ModelObjectStatus::Deleted)) + deleted_any = true; + else + // Do not delete this ModelObject instance. + model_object = nullptr; + if (deleted_any) { + // Delete PrintObjects of the deleted ModelObjects. + PrintObjectPtrs print_objects_old = std::move(m_objects); + m_objects.clear(); + m_objects.reserve(print_objects_old.size()); + for (PrintObject *print_object : print_objects_old) { + const ModelObjectStatus &status = model_object_status_db.get(*print_object->model_object()); + if (status.status == ModelObjectStatus::Deleted) { + update_apply_status(print_object->invalidate_all_steps()); + delete print_object; + } else + m_objects.emplace_back(print_object); + } + for (ModelObject *model_object : model_objects_old) + delete model_object; + print_regions_reshuffled = true; + } + } + } + + // 2) Map print objects including their transformation matrices. + PrintObjectStatusDB print_object_status_db(m_objects); + + // 3) Synchronize ModelObjects & PrintObjects. + const std::initializer_list solid_or_modifier_types { ModelVolumeType::MODEL_PART, ModelVolumeType::NEGATIVE_VOLUME, ModelVolumeType::PARAMETER_MODIFIER }; + for (size_t idx_model_object = 0; idx_model_object < model.objects.size(); ++ idx_model_object) { + ModelObject &model_object = *m_model.objects[idx_model_object]; + ModelObjectStatus &model_object_status = const_cast(model_object_status_db.reuse(model_object)); + const ModelObject &model_object_new = *model.objects[idx_model_object]; + if (model_object_status.status == ModelObjectStatus::New) + // PrintObject instances will be added in the next loop. + continue; + // Update the ModelObject instance, possibly invalidate the linked PrintObjects. + assert(model_object_status.status == ModelObjectStatus::Old || model_object_status.status == ModelObjectStatus::Moved); + // Check whether a model part volume was added or removed, their transformations or order changed. + // Only volume IDs, volume types, transformation matrices and their order are checked, configuration and other parameters are NOT checked. + bool solid_or_modifier_differ = model_volume_list_changed(model_object, model_object_new, solid_or_modifier_types) || + model_mmu_segmentation_data_changed(model_object, model_object_new) || + (model_object_new.is_mm_painted() && num_extruders_changed); + bool supports_differ = model_volume_list_changed(model_object, model_object_new, ModelVolumeType::SUPPORT_BLOCKER) || + model_volume_list_changed(model_object, model_object_new, ModelVolumeType::SUPPORT_ENFORCER); + bool layer_height_ranges_differ = ! layer_height_ranges_equal(model_object.layer_config_ranges, model_object_new.layer_config_ranges, model_object_new.layer_height_profile.empty()); + bool model_origin_translation_differ = model_object.origin_translation != model_object_new.origin_translation; + auto print_objects_range = print_object_status_db.get_range(model_object); + // The list actually can be empty if all instances are out of the print bed. + //assert(print_objects_range.begin() != print_objects_range.end()); + // All PrintObjects in print_objects_range shall point to the same prints_objects_regions + if (print_objects_range.begin() != print_objects_range.end()) { + model_object_status.print_object_regions = print_objects_range.begin()->print_object->m_shared_regions; + model_object_status.print_object_regions->ref_cnt_inc(); + } + if (solid_or_modifier_differ || model_origin_translation_differ || layer_height_ranges_differ || + ! model_object.layer_height_profile.timestamp_matches(model_object_new.layer_height_profile)) { + // The very first step (the slicing step) is invalidated. One may freely remove all associated PrintObjects. + model_object_status.print_object_regions_status = + model_object_status.print_object_regions == nullptr || model_origin_translation_differ || layer_height_ranges_differ ? + // Drop print_objects_regions. + ModelObjectStatus::PrintObjectRegionsStatus::Invalid : + // Reuse bounding boxes of print_objects_regions for ModelVolumes with unmodified transformation. + ModelObjectStatus::PrintObjectRegionsStatus::PartiallyValid; + for (const PrintObjectStatus &print_object_status : print_objects_range) { + update_apply_status(print_object_status.print_object->invalidate_all_steps()); + const_cast(print_object_status).status = PrintObjectStatus::Deleted; + } + if (model_object_status.print_object_regions_status == ModelObjectStatus::PrintObjectRegionsStatus::PartiallyValid) + // Drop everything from PrintObjectRegions but those VolumeExtents (of their particular ModelVolumes) that are still valid. + print_objects_regions_invalidate_keep_some_volumes(*model_object_status.print_object_regions, model_object.volumes, model_object_new.volumes); + else if (model_object_status.print_object_regions != nullptr) + model_object_status.print_object_regions->clear(); + // Copy content of the ModelObject including its ID, do not change the parent. + model_object.assign_copy(model_object_new); + } else { + model_object_status.print_object_regions_status = ModelObjectStatus::PrintObjectRegionsStatus::Valid; + if (supports_differ || model_custom_supports_data_changed(model_object, model_object_new)) { + // First stop background processing before shuffling or deleting the ModelVolumes in the ModelObject's list. + if (supports_differ) { + this->call_cancel_callback(); + update_apply_status(false); + } + // Invalidate just the supports step. + for (const PrintObjectStatus &print_object_status : print_objects_range) + update_apply_status(print_object_status.print_object->invalidate_step(posSupportMaterial)); + if (supports_differ) { + // Copy just the support volumes. + model_volume_list_update_supports(model_object, model_object_new); + } + } else if (model_custom_seam_data_changed(model_object, model_object_new)) { + update_apply_status(this->invalidate_step(psGCodeExport)); + } + } + if (! solid_or_modifier_differ) { + // Synchronize Object's config. + bool object_config_changed = ! model_object.config.timestamp_matches(model_object_new.config); + if (object_config_changed) + model_object.config.assign_config(model_object_new.config); + if (! object_diff.empty() || object_config_changed || num_extruders_changed) { + PrintObjectConfig new_config = PrintObject::object_config_from_model_object(m_default_object_config, model_object, num_extruders); + for (const PrintObjectStatus &print_object_status : print_object_status_db.get_range(model_object)) { + t_config_option_keys diff = print_object_status.print_object->config().diff(new_config); + if (! diff.empty()) { + update_apply_status(print_object_status.print_object->invalidate_state_by_config_options(print_object_status.print_object->config(), new_config, diff)); + print_object_status.print_object->config_apply_only(new_config, diff, true); + } + } + } + // Synchronize (just copy) the remaining data of ModelVolumes (name, config, custom supports data). + //FIXME What to do with m_material_id? + model_volume_list_copy_configs(model_object /* dst */, model_object_new /* src */, ModelVolumeType::MODEL_PART); + model_volume_list_copy_configs(model_object /* dst */, model_object_new /* src */, ModelVolumeType::PARAMETER_MODIFIER); + layer_height_ranges_copy_configs(model_object.layer_config_ranges /* dst */, model_object_new.layer_config_ranges /* src */); + // Copy the ModelObject name, input_file and instances. The instances will be compared against PrintObject instances in the next step. + model_object.name = model_object_new.name; + model_object.input_file = model_object_new.input_file; + // Only refresh ModelInstances if there is any change. + if (model_object.instances.size() != model_object_new.instances.size() || + ! std::equal(model_object.instances.begin(), model_object.instances.end(), model_object_new.instances.begin(), [](auto l, auto r){ return l->id() == r->id(); })) { + // G-code generator accesses model_object.instances to generate sequential print ordering matching the Plater object list. + update_apply_status(this->invalidate_step(psGCodeExport)); + model_object.clear_instances(); + model_object.instances.reserve(model_object_new.instances.size()); + for (const ModelInstance *model_instance : model_object_new.instances) { + model_object.instances.emplace_back(new ModelInstance(*model_instance)); + model_object.instances.back()->set_model_object(&model_object); + } + } else if (! std::equal(model_object.instances.begin(), model_object.instances.end(), model_object_new.instances.begin(), + [](auto l, auto r){ return l->print_volume_state == r->print_volume_state && l->printable == r->printable && + l->get_transformation().get_matrix().isApprox(r->get_transformation().get_matrix()); })) { + // If some of the instances changed, the bounding box of the updated ModelObject is likely no more valid. + // This is safe as the ModelObject's bounding box is only accessed from this function, which is called from the main thread only. + model_object.invalidate_bounding_box(); + // Synchronize the content of instances. + auto new_instance = model_object_new.instances.begin(); + for (auto old_instance = model_object.instances.begin(); old_instance != model_object.instances.end(); ++ old_instance, ++ new_instance) { + (*old_instance)->set_transformation((*new_instance)->get_transformation()); + (*old_instance)->print_volume_state = (*new_instance)->print_volume_state; + (*old_instance)->printable = (*new_instance)->printable; + } + } + } + } + + // 4) Generate PrintObjects from ModelObjects and their instances. + { + PrintObjectPtrs print_objects_new; + print_objects_new.reserve(std::max(m_objects.size(), m_model.objects.size())); + bool new_objects = false; + // Walk over all new model objects and check, whether there are matching PrintObjects. + for (ModelObject *model_object : m_model.objects) { + ModelObjectStatus &model_object_status = const_cast(model_object_status_db.reuse(*model_object)); + model_object_status.print_instances = print_objects_from_model_object(*model_object); + std::vector old; + old.reserve(print_object_status_db.count(*model_object)); + for (const PrintObjectStatus &print_object_status : print_object_status_db.get_range(*model_object)) + if (print_object_status.status != PrintObjectStatus::Deleted) + old.emplace_back(&print_object_status); + // Generate a list of trafos and XY offsets for instances of a ModelObject + // Producing the config for PrintObject on demand, caching it at print_object_last. + const PrintObject *print_object_last = nullptr; + auto print_object_apply_config = [this, &print_object_last, model_object, num_extruders](PrintObject *print_object) { + print_object->config_apply(print_object_last ? + print_object_last->config() : + PrintObject::object_config_from_model_object(m_default_object_config, *model_object, num_extruders)); + print_object_last = print_object; + }; + if (old.empty()) { + // Simple case, just generate new instances. + for (PrintObjectTrafoAndInstances &print_instances : model_object_status.print_instances) { + PrintObject *print_object = new PrintObject(this, model_object, print_instances.trafo, std::move(print_instances.instances)); + print_object_apply_config(print_object); + print_objects_new.emplace_back(print_object); + // print_object_status.emplace(PrintObjectStatus(print_object, PrintObjectStatus::New)); + new_objects = true; + } + continue; + } + // Complex case, try to merge the two lists. + // Sort the old lexicographically by their trafos. + std::sort(old.begin(), old.end(), [](const PrintObjectStatus *lhs, const PrintObjectStatus *rhs){ return transform3d_lower(lhs->trafo, rhs->trafo); }); + // Merge the old / new lists. + auto it_old = old.begin(); + for (PrintObjectTrafoAndInstances &new_instances : model_object_status.print_instances) { + for (; it_old != old.end() && transform3d_lower((*it_old)->trafo, new_instances.trafo); ++ it_old); + if (it_old == old.end() || ! transform3d_equal((*it_old)->trafo, new_instances.trafo)) { + // This is a new instance (or a set of instances with the same trafo). Just add it. + PrintObject *print_object = new PrintObject(this, model_object, new_instances.trafo, std::move(new_instances.instances)); + print_object_apply_config(print_object); + print_objects_new.emplace_back(print_object); + // print_object_status.emplace(PrintObjectStatus(print_object, PrintObjectStatus::New)); + new_objects = true; + if (it_old != old.end()) + const_cast(*it_old)->status = PrintObjectStatus::Deleted; + } else { + // The PrintObject already exists and the copies differ. + PrintBase::ApplyStatus status = (*it_old)->print_object->set_instances(std::move(new_instances.instances)); + if (status != PrintBase::APPLY_STATUS_UNCHANGED) + update_apply_status(status == PrintBase::APPLY_STATUS_INVALIDATED); + print_objects_new.emplace_back((*it_old)->print_object); + const_cast(*it_old)->status = PrintObjectStatus::Reused; + } + } + } + if (m_objects != print_objects_new) { + this->call_cancel_callback(); + update_apply_status(this->invalidate_all_steps()); + m_objects = print_objects_new; + // Delete the PrintObjects marked as Unknown or Deleted. + bool deleted_objects = false; + for (const PrintObjectStatus &pos : print_object_status_db) + if (pos.status == PrintObjectStatus::Unknown || pos.status == PrintObjectStatus::Deleted) { + update_apply_status(pos.print_object->invalidate_all_steps()); + delete pos.print_object; + deleted_objects = true; + } + if (new_objects || deleted_objects) + update_apply_status(this->invalidate_steps({ psSkirtBrim, psWipeTower, psGCodeExport })); + if (new_objects) + update_apply_status(false); + print_regions_reshuffled = true; + } + print_object_status_db.clear(); + } + + // All regions now have distinct settings. + // Check whether applying the new region config defaults we would get different regions, + // update regions or create regions from scratch. + for (auto it_print_object = m_objects.begin(); it_print_object != m_objects.end();) { + // Find the range of PrintObjects sharing the same associated ModelObject. + auto it_print_object_end = it_print_object; + PrintObject &print_object = *(*it_print_object); + const ModelObject &model_object = *print_object.model_object(); + ModelObjectStatus &model_object_status = const_cast(model_object_status_db.reuse(model_object)); + PrintObjectRegions *print_object_regions = model_object_status.print_object_regions; + for (++ it_print_object_end; it_print_object_end != m_objects.end() && (*it_print_object)->model_object() == (*it_print_object_end)->model_object(); ++ it_print_object_end) + assert((*it_print_object_end)->m_shared_regions == nullptr || (*it_print_object_end)->m_shared_regions == print_object_regions); + if (print_object_regions == nullptr) { + print_object_regions = new PrintObjectRegions{}; + model_object_status.print_object_regions = print_object_regions; + print_object_regions->ref_cnt_inc(); + } + std::vector painting_extruders; + if (const auto &volumes = print_object.model_object()->volumes; + num_extruders > 1 && + std::find_if(volumes.begin(), volumes.end(), [](const ModelVolume *v) { return ! v->mmu_segmentation_facets.empty(); }) != volumes.end()) { + //FIXME be more specific! Don't enumerate extruders that are not used for painting! + painting_extruders.assign(num_extruders, 0); + std::iota(painting_extruders.begin(), painting_extruders.end(), 1); + } + if (model_object_status.print_object_regions_status == ModelObjectStatus::PrintObjectRegionsStatus::Valid) { + // Verify that the trafo for regions & volume bounding boxes thus for regions is still applicable. + auto invalidate = [it_print_object, it_print_object_end, update_apply_status]() { + for (auto it = it_print_object; it != it_print_object_end; ++ it) + if ((*it)->m_shared_regions != nullptr) + update_apply_status((*it)->invalidate_all_steps()); + }; + if (print_object_regions && ! trafos_differ_in_rotation_by_z_and_mirroring_by_xy_only(print_object_regions->trafo_bboxes, model_object_status.print_instances.front().trafo)) { + invalidate(); + print_object_regions->clear(); + model_object_status.print_object_regions_status = ModelObjectStatus::PrintObjectRegionsStatus::Invalid; + print_regions_reshuffled = true; + } else if (print_object_regions && + verify_update_print_object_regions( + print_object.model_object()->volumes, + m_default_region_config, + num_extruders, + painting_extruders, + *print_object_regions, + [it_print_object, it_print_object_end, &update_apply_status](const PrintRegionConfig &old_config, const PrintRegionConfig &new_config, const t_config_option_keys &diff_keys) { + for (auto it = it_print_object; it != it_print_object_end; ++it) + if ((*it)->m_shared_regions != nullptr) + update_apply_status((*it)->invalidate_state_by_config_options(old_config, new_config, diff_keys)); + })) { + // Regions are valid, just keep them. + } else { + // Regions were reshuffled. + invalidate(); + // At least reuse layer ranges and bounding boxes of ModelVolumes. + model_object_status.print_object_regions_status = ModelObjectStatus::PrintObjectRegionsStatus::PartiallyValid; + print_regions_reshuffled = true; + } + } + if (print_object_regions == nullptr || model_object_status.print_object_regions_status != ModelObjectStatus::PrintObjectRegionsStatus::Valid) { + // Layer ranges with their associated configurations. Remove overlaps between the ranges + // and create the regions from scratch. + print_object_regions = generate_print_object_regions( + print_object_regions, + print_object.model_object()->volumes, + LayerRanges(print_object.model_object()->layer_config_ranges), + m_default_region_config, + model_object_status.print_instances.front().trafo, + num_extruders, + print_object.is_mm_painted() ? 0.f : float(print_object.config().xy_size_compensation.value), + painting_extruders); + } + for (auto it = it_print_object; it != it_print_object_end; ++it) + if ((*it)->m_shared_regions) { + assert((*it)->m_shared_regions == print_object_regions); + } else { + (*it)->m_shared_regions = print_object_regions; + print_object_regions->ref_cnt_inc(); + } + it_print_object = it_print_object_end; + } + + if (print_regions_reshuffled) { + // Update Print::m_print_regions from objects. + struct cmp { bool operator() (const PrintRegion *l, const PrintRegion *r) const { return l->config_hash() == r->config_hash() && l->config() == r->config(); } }; + std::set region_set; + m_print_regions.clear(); + PrintObjectRegions *print_object_regions = nullptr; + for (PrintObject *print_object : m_objects) + if (print_object_regions != print_object->m_shared_regions) { + print_object_regions = print_object->m_shared_regions; + for (std::unique_ptr &print_region : print_object_regions->all_regions) + if (auto it = region_set.find(print_region.get()); it == region_set.end()) { + int print_region_id = int(m_print_regions.size()); + m_print_regions.emplace_back(print_region.get()); + print_region->m_print_region_id = print_region_id; + } else { + print_region->m_print_region_id = (*it)->print_region_id(); + } + } + } + + // Update SlicingParameters for each object where the SlicingParameters is not valid. + // If it is not valid, then it is ensured that PrintObject.m_slicing_params is not in use + // (posSlicing and posSupportMaterial was invalidated). + for (PrintObject *object : m_objects) + object->update_slicing_parameters(); + +#ifdef _DEBUG + check_model_ids_equal(m_model, model); +#endif /* _DEBUG */ + + return static_cast(apply_status); +} + +} // namespace Slic3r diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index 9c6178f8b..eeaf1b13c 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -464,7 +464,7 @@ FillLightning::GeneratorPtr PrintObject::prepare_lightning_infill_data() break; } - return has_lightning_infill ? FillLightning::build_generator(std::as_const(*this)) : FillLightning::GeneratorPtr(); + return has_lightning_infill ? FillLightning::build_generator(std::as_const(*this), [this]() -> void { this->throw_if_canceled(); }) : FillLightning::GeneratorPtr(); } void PrintObject::clear_layers() diff --git a/src/libslic3r/SLA/Rotfinder.cpp b/src/libslic3r/SLA/Rotfinder.cpp index 847e638e6..16cd28130 100644 --- a/src/libslic3r/SLA/Rotfinder.cpp +++ b/src/libslic3r/SLA/Rotfinder.cpp @@ -288,7 +288,7 @@ template struct RotfinderBoilerplate { static constexpr unsigned MAX_TRIES = MAX_ITER; - int status = 0; + int status = 0, prev_status = 0; TriangleMesh mesh; unsigned max_tries; const RotOptimizeParams ¶ms; @@ -314,13 +314,20 @@ struct RotfinderBoilerplate { RotfinderBoilerplate(const ModelObject &mo, const RotOptimizeParams &p) : mesh{get_mesh_to_rotate(mo)} - , params{p} , max_tries(p.accuracy() * MAX_TRIES) - { + , params{p} + {} + void statusfn() { + int s = status * 100 / max_tries; + if (s != prev_status) { + params.statuscb()(s); + prev_status = s; + } + + ++status; } - void statusfn() { params.statuscb()(++status * 100.0 / max_tries); } bool stopcond() { return ! params.statuscb()(-1); } }; diff --git a/src/libslic3r/ShortestPath.cpp b/src/libslic3r/ShortestPath.cpp index 449ff45b5..72bfe1f30 100644 --- a/src/libslic3r/ShortestPath.cpp +++ b/src/libslic3r/ShortestPath.cpp @@ -195,7 +195,15 @@ std::vector> chain_segments_greedy_constrained_reversals } // Initialize a heap of end points sorted by the lowest distance to the next valid point of a path. - auto queue = make_mutable_priority_queue( + auto queue = make_mutable_priority_queue( [](EndPoint *ep, size_t idx){ ep->heap_idx = idx; }, [](EndPoint *l, EndPoint *r){ return l->distance_out < r->distance_out; }); queue.reserve(end_points.size() * 2 - 1); @@ -213,7 +221,7 @@ std::vector> chain_segments_greedy_constrained_reversals assert(ep.chain_id == 0); } else { // End point is NOT on the heap, therefore it is part of the output path. - assert(ep.heap_idx == std::numeric_limits::max()); + assert(ep.heap_idx == queue.invalid_id()); assert(ep.chain_id != 0); if (&ep == first_point) { assert(ep.edge_out == nullptr); @@ -222,7 +230,7 @@ std::vector> chain_segments_greedy_constrained_reversals // Detect loops. for (EndPoint *pt = &ep; pt != nullptr;) { // Out of queue. It is a final point. - assert(pt->heap_idx == std::numeric_limits::max()); + assert(pt->heap_idx == queue.invalid_id()); EndPoint *pt_other = &end_points[(pt - &end_points.front()) ^ 1]; if (pt_other->heap_idx < queue.size()) // The other side of this segment is undecided yet. diff --git a/src/libslic3r/Technologies.hpp b/src/libslic3r/Technologies.hpp index a89a7c8b0..302c62879 100644 --- a/src/libslic3r/Technologies.hpp +++ b/src/libslic3r/Technologies.hpp @@ -50,7 +50,7 @@ // Enable showing time estimate for travel moves in legend #define ENABLE_TRAVEL_TIME (1 && ENABLE_2_5_0_ALPHA1) // Enable not killing focus in object manipulator fields when hovering over 3D scene -#define ENABLE_OBJECT_MANIPULATOR_FOCUS (1 && ENABLE_2_5_0_ALPHA1) +#define ENABLE_OBJECT_MANIPULATOR_FOCUS (0 && ENABLE_2_5_0_ALPHA1) // Enable removal of wipe tower magic object_id equal to 1000 #define ENABLE_WIPETOWER_OBJECTID_1000_REMOVAL (1 && ENABLE_2_5_0_ALPHA1) // Enable removal of legacy OpenGL calls @@ -69,6 +69,8 @@ #define ENABLE_SHOW_TOOLPATHS_COG (1 && ENABLE_2_5_0_ALPHA1) // Enable recalculating toolpaths when switching to/from volumetric rate visualization #define ENABLE_VOLUMETRIC_RATE_TOOLPATHS_RECALC (1 && ENABLE_2_5_0_ALPHA1) +// Enable editing volumes transformation in world coordinates and instances in local coordinates +#define ENABLE_WORLD_COORDINATE (1 && ENABLE_2_5_0_ALPHA1) // Enable modified camera control using mouse #define ENABLE_NEW_CAMERA_MOVEMENTS (1 && ENABLE_2_5_0_ALPHA1) // Enable modified rectangle selection @@ -81,6 +83,7 @@ #define ENABLE_USED_FILAMENT_POST_PROCESS (1 && ENABLE_2_5_0_ALPHA1) // Enable gizmo grabbers to share common models #define ENABLE_GIZMO_GRABBER_REFACTOR (1 && ENABLE_2_5_0_ALPHA1) - +// Enable copy of custom bed model and texture +#define ENABLE_COPY_CUSTOM_BED_MODEL_AND_TEXTURE (1 && ENABLE_2_5_0_ALPHA1) #endif // _prusaslicer_technologies_h_ diff --git a/src/libslic3r/Thread.cpp b/src/libslic3r/Thread.cpp index 4e7bd073a..c099f8de6 100644 --- a/src/libslic3r/Thread.cpp +++ b/src/libslic3r/Thread.cpp @@ -15,6 +15,7 @@ #include "Thread.hpp" #include "Utils.hpp" +#include "LocalesUtils.hpp" namespace Slic3r { @@ -234,21 +235,8 @@ void name_tbb_thread_pool_threads_set_locale() std::ostringstream name; name << "slic3r_tbb_" << range.begin(); set_current_thread_name(name.str().c_str()); - // Set locales of the worker thread to "C". -#ifdef _WIN32 - _configthreadlocale(_ENABLE_PER_THREAD_LOCALE); - std::setlocale(LC_ALL, "C"); -#else - // We are leaking some memory here, because the newlocale() produced memory will never be released. - // This is not a problem though, as there will be a maximum one worker thread created per physical thread. - uselocale(newlocale( -#ifdef __APPLE__ - LC_ALL_MASK -#else // some Unix / Linux / BSD - LC_ALL -#endif - , "C", nullptr)); -#endif + // Set locales of the worker thread to "C". + set_c_locales(); } }); } diff --git a/src/libslic3r/libslic3r.h b/src/libslic3r/libslic3r.h index 06cad840e..3ee8acfc3 100644 --- a/src/libslic3r/libslic3r.h +++ b/src/libslic3r/libslic3r.h @@ -4,7 +4,6 @@ #include "libslic3r_version.h" #define GCODEVIEWER_APP_NAME "PrusaSlicer G-code Viewer" #define GCODEVIEWER_APP_KEY "PrusaSlicerGcodeViewer" -#define GCODEVIEWER_BUILD_ID std::string("PrusaSlicer G-code Viewer-") + std::string(SLIC3R_VERSION) + std::string("-UNKNOWN") // this needs to be included early for MSVC (listing it in Build.PL is not enough) #include diff --git a/src/nanosvg/README-prusa.txt b/src/nanosvg/README-prusa.txt deleted file mode 100644 index 8388aa8ef..000000000 --- a/src/nanosvg/README-prusa.txt +++ /dev/null @@ -1 +0,0 @@ -Upstream source: https://github.com/memononen/nanosvg/tree/c1f6e209c16b18b46aa9f45d7e619acf42c29726 \ No newline at end of file diff --git a/src/nanosvg/nanosvg.h b/src/nanosvg/nanosvg.h deleted file mode 100644 index 57bcb7c2c..000000000 --- a/src/nanosvg/nanosvg.h +++ /dev/null @@ -1,2979 +0,0 @@ -/* - * Copyright (c) 2013-14 Mikko Mononen memon@inside.org - * - * This software is provided 'as-is', without any express or implied - * warranty. In no event will the authors be held liable for any damages - * arising from the use of this software. - * - * Permission is granted to anyone to use this software for any purpose, - * including commercial applications, and to alter it and redistribute it - * freely, subject to the following restrictions: - * - * 1. The origin of this software must not be misrepresented; you must not - * claim that you wrote the original software. If you use this software - * in a product, an acknowledgment in the product documentation would be - * appreciated but is not required. - * 2. Altered source versions must be plainly marked as such, and must not be - * misrepresented as being the original software. - * 3. This notice may not be removed or altered from any source distribution. - * - * The SVG parser is based on Anti-Grain Geometry 2.4 SVG example - * Copyright (C) 2002-2004 Maxim Shemanarev (McSeem) (http://www.antigrain.com/) - * - * Arc calculation code based on canvg (https://code.google.com/p/canvg/) - * - * Bounding box calculation based on http://blog.hackers-cafe.net/2009/06/how-to-calculate-bezier-curves-bounding.html - * - */ - -#ifndef NANOSVG_H -#define NANOSVG_H - -#ifndef NANOSVG_CPLUSPLUS -#ifdef __cplusplus -extern "C" { -#endif -#endif - -// NanoSVG is a simple stupid single-header-file SVG parse. The output of the parser is a list of cubic bezier shapes. -// -// The library suits well for anything from rendering scalable icons in your editor application to prototyping a game. -// -// NanoSVG supports a wide range of SVG features, but something may be missing, feel free to create a pull request! -// -// The shapes in the SVG images are transformed by the viewBox and converted to specified units. -// That is, you should get the same looking data as your designed in your favorite app. -// -// NanoSVG can return the paths in few different units. For example if you want to render an image, you may choose -// to get the paths in pixels, or if you are feeding the data into a CNC-cutter, you may want to use millimeters. -// -// The units passed to NanoSVG should be one of: 'px', 'pt', 'pc' 'mm', 'cm', or 'in'. -// DPI (dots-per-inch) controls how the unit conversion is done. -// -// If you don't know or care about the units stuff, "px" and 96 should get you going. - - -/* Example Usage: - // Load SVG - NSVGimage* image; - image = nsvgParseFromFile("test.svg", "px", 96); - printf("size: %f x %f\n", image->width, image->height); - // Use... - for (NSVGshape *shape = image->shapes; shape != NULL; shape = shape->next) { - for (NSVGpath *path = shape->paths; path != NULL; path = path->next) { - for (int i = 0; i < path->npts-1; i += 3) { - float* p = &path->pts[i*2]; - drawCubicBez(p[0],p[1], p[2],p[3], p[4],p[5], p[6],p[7]); - } - } - } - // Delete - nsvgDelete(image); -*/ - -enum NSVGpaintType { - NSVG_PAINT_NONE = 0, - NSVG_PAINT_COLOR = 1, - NSVG_PAINT_LINEAR_GRADIENT = 2, - NSVG_PAINT_RADIAL_GRADIENT = 3 -}; - -enum NSVGspreadType { - NSVG_SPREAD_PAD = 0, - NSVG_SPREAD_REFLECT = 1, - NSVG_SPREAD_REPEAT = 2 -}; - -enum NSVGlineJoin { - NSVG_JOIN_MITER = 0, - NSVG_JOIN_ROUND = 1, - NSVG_JOIN_BEVEL = 2 -}; - -enum NSVGlineCap { - NSVG_CAP_BUTT = 0, - NSVG_CAP_ROUND = 1, - NSVG_CAP_SQUARE = 2 -}; - -enum NSVGfillRule { - NSVG_FILLRULE_NONZERO = 0, - NSVG_FILLRULE_EVENODD = 1 -}; - -enum NSVGflags { - NSVG_FLAGS_VISIBLE = 0x01 -}; - -typedef struct NSVGgradientStop { - unsigned int color; - float offset; -} NSVGgradientStop; - -typedef struct NSVGgradient { - float xform[6]; - char spread; - float fx, fy; - int nstops; - NSVGgradientStop stops[1]; -} NSVGgradient; - -typedef struct NSVGpaint { - char type; - union { - unsigned int color; - NSVGgradient* gradient; - }; -} NSVGpaint; - -typedef struct NSVGpath -{ - float* pts; // Cubic bezier points: x0,y0, [cpx1,cpx1,cpx2,cpy2,x1,y1], ... - int npts; // Total number of bezier points. - char closed; // Flag indicating if shapes should be treated as closed. - float bounds[4]; // Tight bounding box of the shape [minx,miny,maxx,maxy]. - struct NSVGpath* next; // Pointer to next path, or NULL if last element. -} NSVGpath; - -typedef struct NSVGshape -{ - char id[64]; // Optional 'id' attr of the shape or its group - NSVGpaint fill; // Fill paint - NSVGpaint stroke; // Stroke paint - float opacity; // Opacity of the shape. - float strokeWidth; // Stroke width (scaled). - float strokeDashOffset; // Stroke dash offset (scaled). - float strokeDashArray[8]; // Stroke dash array (scaled). - char strokeDashCount; // Number of dash values in dash array. - char strokeLineJoin; // Stroke join type. - char strokeLineCap; // Stroke cap type. - float miterLimit; // Miter limit - char fillRule; // Fill rule, see NSVGfillRule. - unsigned char flags; // Logical or of NSVG_FLAGS_* flags - float bounds[4]; // Tight bounding box of the shape [minx,miny,maxx,maxy]. - NSVGpath* paths; // Linked list of paths in the image. - struct NSVGshape* next; // Pointer to next shape, or NULL if last element. -} NSVGshape; - -typedef struct NSVGimage -{ - float width; // Width of the image. - float height; // Height of the image. - NSVGshape* shapes; // Linked list of shapes in the image. -} NSVGimage; - -// Parses SVG file from a file, returns SVG image as paths. -NSVGimage* nsvgParseFromFile(const char* filename, const char* units, float dpi); - -// Parses SVG file from a null terminated string, returns SVG image as paths. -// Important note: changes the string. -NSVGimage* nsvgParse(char* input, const char* units, float dpi); - -// Duplicates a path. -NSVGpath* nsvgDuplicatePath(NSVGpath* p); - -// Deletes an image. -void nsvgDelete(NSVGimage* image); - -#ifndef NANOSVG_CPLUSPLUS -#ifdef __cplusplus -} -#endif -#endif - -#endif // NANOSVG_H - -#ifdef NANOSVG_IMPLEMENTATION - -#include -#include -#include - -#include - -#define NSVG_PI (3.14159265358979323846264338327f) -#define NSVG_KAPPA90 (0.5522847493f) // Length proportional to radius of a cubic bezier handle for 90deg arcs. - -#define NSVG_ALIGN_MIN 0 -#define NSVG_ALIGN_MID 1 -#define NSVG_ALIGN_MAX 2 -#define NSVG_ALIGN_NONE 0 -#define NSVG_ALIGN_MEET 1 -#define NSVG_ALIGN_SLICE 2 - -#define NSVG_NOTUSED(v) do { (void)(1 ? (void)0 : ( (void)(v) ) ); } while(0) -#define NSVG_RGB(r, g, b) (((unsigned int)r) | ((unsigned int)g << 8) | ((unsigned int)b << 16)) - -#ifdef _MSC_VER - #pragma warning (disable: 4996) // Switch off security warnings - #pragma warning (disable: 4100) // Switch off unreferenced formal parameter warnings - #ifdef __cplusplus - #define NSVG_INLINE inline - #else - #define NSVG_INLINE - #endif -#else - #define NSVG_INLINE inline -#endif - - -static int nsvg__isspace(char c) -{ - return strchr(" \t\n\v\f\r", c) != 0; -} - -static int nsvg__isdigit(char c) -{ - return c >= '0' && c <= '9'; -} - -static int nsvg__isnum(char c) -{ - return strchr("0123456789+-.eE", c) != 0; -} - -static NSVG_INLINE float nsvg__minf(float a, float b) { return a < b ? a : b; } -static NSVG_INLINE float nsvg__maxf(float a, float b) { return a > b ? a : b; } - - -// Simple XML parser - -#define NSVG_XML_TAG 1 -#define NSVG_XML_CONTENT 2 -#define NSVG_XML_MAX_ATTRIBS 256 - -static void nsvg__parseContent(char* s, - void (*contentCb)(void* ud, const char* s), - void* ud) -{ - // Trim start white spaces - while (*s && nsvg__isspace(*s)) s++; - if (!*s) return; - - if (contentCb) - (*contentCb)(ud, s); -} - -static void nsvg__parseElement(char* s, - void (*startelCb)(void* ud, const char* el, const char** attr), - void (*endelCb)(void* ud, const char* el), - void* ud) -{ - const char* attr[NSVG_XML_MAX_ATTRIBS]; - int nattr = 0; - char* name; - int start = 0; - int end = 0; - char quote; - - // Skip white space after the '<' - while (*s && nsvg__isspace(*s)) s++; - - // Check if the tag is end tag - if (*s == '/') { - s++; - end = 1; - } else { - start = 1; - } - - // Skip comments, data and preprocessor stuff. - if (!*s || *s == '?' || *s == '!') - return; - - // Get tag name - name = s; - while (*s && !nsvg__isspace(*s)) s++; - if (*s) { *s++ = '\0'; } - - // Get attribs - while (!end && *s && nattr < NSVG_XML_MAX_ATTRIBS-3) { - char* name = NULL; - char* value = NULL; - - // Skip white space before the attrib name - while (*s && nsvg__isspace(*s)) s++; - if (!*s) break; - if (*s == '/') { - end = 1; - break; - } - name = s; - // Find end of the attrib name. - while (*s && !nsvg__isspace(*s) && *s != '=') s++; - if (*s) { *s++ = '\0'; } - // Skip until the beginning of the value. - while (*s && *s != '\"' && *s != '\'') s++; - if (!*s) break; - quote = *s; - s++; - // Store value and find the end of it. - value = s; - while (*s && *s != quote) s++; - if (*s) { *s++ = '\0'; } - - // Store only well formed attributes - if (name && value) { - attr[nattr++] = name; - attr[nattr++] = value; - } - } - - // List terminator - attr[nattr++] = 0; - attr[nattr++] = 0; - - // Call callbacks. - if (start && startelCb) - (*startelCb)(ud, name, attr); - if (end && endelCb) - (*endelCb)(ud, name); -} - -int nsvg__parseXML(char* input, - void (*startelCb)(void* ud, const char* el, const char** attr), - void (*endelCb)(void* ud, const char* el), - void (*contentCb)(void* ud, const char* s), - void* ud) -{ - char* s = input; - char* mark = s; - int state = NSVG_XML_CONTENT; - while (*s) { - if (*s == '<' && state == NSVG_XML_CONTENT) { - // Start of a tag - *s++ = '\0'; - nsvg__parseContent(mark, contentCb, ud); - mark = s; - state = NSVG_XML_TAG; - } else if (*s == '>' && state == NSVG_XML_TAG) { - // Start of a content or new tag. - *s++ = '\0'; - nsvg__parseElement(mark, startelCb, endelCb, ud); - mark = s; - state = NSVG_XML_CONTENT; - } else { - s++; - } - } - - return 1; -} - - -/* Simple SVG parser. */ - -#define NSVG_MAX_ATTR 128 - -enum NSVGgradientUnits { - NSVG_USER_SPACE = 0, - NSVG_OBJECT_SPACE = 1 -}; - -#define NSVG_MAX_DASHES 8 - -enum NSVGunits { - NSVG_UNITS_USER, - NSVG_UNITS_PX, - NSVG_UNITS_PT, - NSVG_UNITS_PC, - NSVG_UNITS_MM, - NSVG_UNITS_CM, - NSVG_UNITS_IN, - NSVG_UNITS_PERCENT, - NSVG_UNITS_EM, - NSVG_UNITS_EX -}; - -typedef struct NSVGcoordinate { - float value; - int units; -} NSVGcoordinate; - -typedef struct NSVGlinearData { - NSVGcoordinate x1, y1, x2, y2; -} NSVGlinearData; - -typedef struct NSVGradialData { - NSVGcoordinate cx, cy, r, fx, fy; -} NSVGradialData; - -typedef struct NSVGgradientData -{ - char id[64]; - char ref[64]; - char type; - union { - NSVGlinearData linear; - NSVGradialData radial; - }; - char spread; - char units; - float xform[6]; - int nstops; - NSVGgradientStop* stops; - struct NSVGgradientData* next; -} NSVGgradientData; - -typedef struct NSVGattrib -{ - char id[64]; - float xform[6]; - unsigned int fillColor; - unsigned int strokeColor; - float opacity; - float fillOpacity; - float strokeOpacity; - char fillGradient[64]; - char strokeGradient[64]; - float strokeWidth; - float strokeDashOffset; - float strokeDashArray[NSVG_MAX_DASHES]; - int strokeDashCount; - char strokeLineJoin; - char strokeLineCap; - float miterLimit; - char fillRule; - float fontSize; - unsigned int stopColor; - float stopOpacity; - float stopOffset; - char hasFill; - char hasStroke; - char visible; -} NSVGattrib; - -typedef struct NSVGparser -{ - NSVGattrib attr[NSVG_MAX_ATTR]; - int attrHead; - float* pts; - int npts; - int cpts; - NSVGpath* plist; - NSVGimage* image; - NSVGgradientData* gradients; - NSVGshape* shapesTail; - float viewMinx, viewMiny, viewWidth, viewHeight; - int alignX, alignY, alignType; - float dpi; - char pathFlag; - char defsFlag; -} NSVGparser; - -static void nsvg__xformIdentity(float* t) -{ - t[0] = 1.0f; t[1] = 0.0f; - t[2] = 0.0f; t[3] = 1.0f; - t[4] = 0.0f; t[5] = 0.0f; -} - -static void nsvg__xformSetTranslation(float* t, float tx, float ty) -{ - t[0] = 1.0f; t[1] = 0.0f; - t[2] = 0.0f; t[3] = 1.0f; - t[4] = tx; t[5] = ty; -} - -static void nsvg__xformSetScale(float* t, float sx, float sy) -{ - t[0] = sx; t[1] = 0.0f; - t[2] = 0.0f; t[3] = sy; - t[4] = 0.0f; t[5] = 0.0f; -} - -static void nsvg__xformSetSkewX(float* t, float a) -{ - t[0] = 1.0f; t[1] = 0.0f; - t[2] = tanf(a); t[3] = 1.0f; - t[4] = 0.0f; t[5] = 0.0f; -} - -static void nsvg__xformSetSkewY(float* t, float a) -{ - t[0] = 1.0f; t[1] = tanf(a); - t[2] = 0.0f; t[3] = 1.0f; - t[4] = 0.0f; t[5] = 0.0f; -} - -static void nsvg__xformSetRotation(float* t, float a) -{ - float cs = cosf(a), sn = sinf(a); - t[0] = cs; t[1] = sn; - t[2] = -sn; t[3] = cs; - t[4] = 0.0f; t[5] = 0.0f; -} - -static void nsvg__xformMultiply(float* t, float* s) -{ - float t0 = t[0] * s[0] + t[1] * s[2]; - float t2 = t[2] * s[0] + t[3] * s[2]; - float t4 = t[4] * s[0] + t[5] * s[2] + s[4]; - t[1] = t[0] * s[1] + t[1] * s[3]; - t[3] = t[2] * s[1] + t[3] * s[3]; - t[5] = t[4] * s[1] + t[5] * s[3] + s[5]; - t[0] = t0; - t[2] = t2; - t[4] = t4; -} - -static void nsvg__xformInverse(float* inv, float* t) -{ - double invdet, det = (double)t[0] * t[3] - (double)t[2] * t[1]; - if (det > -1e-6 && det < 1e-6) { - nsvg__xformIdentity(t); - return; - } - invdet = 1.0 / det; - inv[0] = (float)(t[3] * invdet); - inv[2] = (float)(-t[2] * invdet); - inv[4] = (float)(((double)t[2] * t[5] - (double)t[3] * t[4]) * invdet); - inv[1] = (float)(-t[1] * invdet); - inv[3] = (float)(t[0] * invdet); - inv[5] = (float)(((double)t[1] * t[4] - (double)t[0] * t[5]) * invdet); -} - -static void nsvg__xformPremultiply(float* t, float* s) -{ - float s2[6]; - memcpy(s2, s, sizeof(float)*6); - nsvg__xformMultiply(s2, t); - memcpy(t, s2, sizeof(float)*6); -} - -static void nsvg__xformPoint(float* dx, float* dy, float x, float y, float* t) -{ - *dx = x*t[0] + y*t[2] + t[4]; - *dy = x*t[1] + y*t[3] + t[5]; -} - -static void nsvg__xformVec(float* dx, float* dy, float x, float y, float* t) -{ - *dx = x*t[0] + y*t[2]; - *dy = x*t[1] + y*t[3]; -} - -#define NSVG_EPSILON (1e-12) - -static int nsvg__ptInBounds(float* pt, float* bounds) -{ - return pt[0] >= bounds[0] && pt[0] <= bounds[2] && pt[1] >= bounds[1] && pt[1] <= bounds[3]; -} - - -static double nsvg__evalBezier(double t, double p0, double p1, double p2, double p3) -{ - double it = 1.0-t; - return it*it*it*p0 + 3.0*it*it*t*p1 + 3.0*it*t*t*p2 + t*t*t*p3; -} - -static void nsvg__curveBounds(float* bounds, float* curve) -{ - int i, j, count; - double roots[2], a, b, c, b2ac, t, v; - float* v0 = &curve[0]; - float* v1 = &curve[2]; - float* v2 = &curve[4]; - float* v3 = &curve[6]; - - // Start the bounding box by end points - bounds[0] = nsvg__minf(v0[0], v3[0]); - bounds[1] = nsvg__minf(v0[1], v3[1]); - bounds[2] = nsvg__maxf(v0[0], v3[0]); - bounds[3] = nsvg__maxf(v0[1], v3[1]); - - // Bezier curve fits inside the convex hull of it's control points. - // If control points are inside the bounds, we're done. - if (nsvg__ptInBounds(v1, bounds) && nsvg__ptInBounds(v2, bounds)) - return; - - // Add bezier curve inflection points in X and Y. - for (i = 0; i < 2; i++) { - a = -3.0 * v0[i] + 9.0 * v1[i] - 9.0 * v2[i] + 3.0 * v3[i]; - b = 6.0 * v0[i] - 12.0 * v1[i] + 6.0 * v2[i]; - c = 3.0 * v1[i] - 3.0 * v0[i]; - count = 0; - if (fabs(a) < NSVG_EPSILON) { - if (fabs(b) > NSVG_EPSILON) { - t = -c / b; - if (t > NSVG_EPSILON && t < 1.0-NSVG_EPSILON) - roots[count++] = t; - } - } else { - b2ac = b*b - 4.0*c*a; - if (b2ac > NSVG_EPSILON) { - t = (-b + sqrt(b2ac)) / (2.0 * a); - if (t > NSVG_EPSILON && t < 1.0-NSVG_EPSILON) - roots[count++] = t; - t = (-b - sqrt(b2ac)) / (2.0 * a); - if (t > NSVG_EPSILON && t < 1.0-NSVG_EPSILON) - roots[count++] = t; - } - } - for (j = 0; j < count; j++) { - v = nsvg__evalBezier(roots[j], v0[i], v1[i], v2[i], v3[i]); - bounds[0+i] = nsvg__minf(bounds[0+i], (float)v); - bounds[2+i] = nsvg__maxf(bounds[2+i], (float)v); - } - } -} - -static NSVGparser* nsvg__createParser() -{ - NSVGparser* p; - p = (NSVGparser*)malloc(sizeof(NSVGparser)); - if (p == NULL) goto error; - memset(p, 0, sizeof(NSVGparser)); - - p->image = (NSVGimage*)malloc(sizeof(NSVGimage)); - if (p->image == NULL) goto error; - memset(p->image, 0, sizeof(NSVGimage)); - - // Init style - nsvg__xformIdentity(p->attr[0].xform); - memset(p->attr[0].id, 0, sizeof p->attr[0].id); - p->attr[0].fillColor = NSVG_RGB(0,0,0); - p->attr[0].strokeColor = NSVG_RGB(0,0,0); - p->attr[0].opacity = 1; - p->attr[0].fillOpacity = 1; - p->attr[0].strokeOpacity = 1; - p->attr[0].stopOpacity = 1; - p->attr[0].strokeWidth = 1; - p->attr[0].strokeLineJoin = NSVG_JOIN_MITER; - p->attr[0].strokeLineCap = NSVG_CAP_BUTT; - p->attr[0].miterLimit = 4; - p->attr[0].fillRule = NSVG_FILLRULE_NONZERO; - p->attr[0].hasFill = 1; - p->attr[0].visible = 1; - - return p; - -error: - if (p) { - if (p->image) free(p->image); - free(p); - } - return NULL; -} - -static void nsvg__deletePaths(NSVGpath* path) -{ - while (path) { - NSVGpath *next = path->next; - if (path->pts != NULL) - free(path->pts); - free(path); - path = next; - } -} - -static void nsvg__deletePaint(NSVGpaint* paint) -{ - if (paint->type == NSVG_PAINT_LINEAR_GRADIENT || paint->type == NSVG_PAINT_RADIAL_GRADIENT) - free(paint->gradient); -} - -static void nsvg__deleteGradientData(NSVGgradientData* grad) -{ - NSVGgradientData* next; - while (grad != NULL) { - next = grad->next; - free(grad->stops); - free(grad); - grad = next; - } -} - -static void nsvg__deleteParser(NSVGparser* p) -{ - if (p != NULL) { - nsvg__deletePaths(p->plist); - nsvg__deleteGradientData(p->gradients); - nsvgDelete(p->image); - free(p->pts); - free(p); - } -} - -static void nsvg__resetPath(NSVGparser* p) -{ - p->npts = 0; -} - -static void nsvg__addPoint(NSVGparser* p, float x, float y) -{ - if (p->npts+1 > p->cpts) { - p->cpts = p->cpts ? p->cpts*2 : 8; - p->pts = (float*)realloc(p->pts, p->cpts*2*sizeof(float)); - if (!p->pts) return; - } - p->pts[p->npts*2+0] = x; - p->pts[p->npts*2+1] = y; - p->npts++; -} - -static void nsvg__moveTo(NSVGparser* p, float x, float y) -{ - if (p->npts > 0) { - p->pts[(p->npts-1)*2+0] = x; - p->pts[(p->npts-1)*2+1] = y; - } else { - nsvg__addPoint(p, x, y); - } -} - -static void nsvg__lineTo(NSVGparser* p, float x, float y) -{ - float px,py, dx,dy; - if (p->npts > 0) { - px = p->pts[(p->npts-1)*2+0]; - py = p->pts[(p->npts-1)*2+1]; - dx = x - px; - dy = y - py; - nsvg__addPoint(p, px + dx/3.0f, py + dy/3.0f); - nsvg__addPoint(p, x - dx/3.0f, y - dy/3.0f); - nsvg__addPoint(p, x, y); - } -} - -static void nsvg__cubicBezTo(NSVGparser* p, float cpx1, float cpy1, float cpx2, float cpy2, float x, float y) -{ - nsvg__addPoint(p, cpx1, cpy1); - nsvg__addPoint(p, cpx2, cpy2); - nsvg__addPoint(p, x, y); -} - -static NSVGattrib* nsvg__getAttr(NSVGparser* p) -{ - return &p->attr[p->attrHead]; -} - -static void nsvg__pushAttr(NSVGparser* p) -{ - if (p->attrHead < NSVG_MAX_ATTR-1) { - p->attrHead++; - memcpy(&p->attr[p->attrHead], &p->attr[p->attrHead-1], sizeof(NSVGattrib)); - } -} - -static void nsvg__popAttr(NSVGparser* p) -{ - if (p->attrHead > 0) - p->attrHead--; -} - -static float nsvg__actualOrigX(NSVGparser* p) -{ - return p->viewMinx; -} - -static float nsvg__actualOrigY(NSVGparser* p) -{ - return p->viewMiny; -} - -static float nsvg__actualWidth(NSVGparser* p) -{ - return p->viewWidth; -} - -static float nsvg__actualHeight(NSVGparser* p) -{ - return p->viewHeight; -} - -static float nsvg__actualLength(NSVGparser* p) -{ - float w = nsvg__actualWidth(p), h = nsvg__actualHeight(p); - return sqrtf(w*w + h*h) / sqrtf(2.0f); -} - -static float nsvg__convertToPixels(NSVGparser* p, NSVGcoordinate c, float orig, float length) -{ - NSVGattrib* attr = nsvg__getAttr(p); - switch (c.units) { - case NSVG_UNITS_USER: return c.value; - case NSVG_UNITS_PX: return c.value; - case NSVG_UNITS_PT: return c.value / 72.0f * p->dpi; - case NSVG_UNITS_PC: return c.value / 6.0f * p->dpi; - case NSVG_UNITS_MM: return c.value / 25.4f * p->dpi; - case NSVG_UNITS_CM: return c.value / 2.54f * p->dpi; - case NSVG_UNITS_IN: return c.value * p->dpi; - case NSVG_UNITS_EM: return c.value * attr->fontSize; - case NSVG_UNITS_EX: return c.value * attr->fontSize * 0.52f; // x-height of Helvetica. - case NSVG_UNITS_PERCENT: return orig + c.value / 100.0f * length; - default: return c.value; - } - return c.value; -} - -static NSVGgradientData* nsvg__findGradientData(NSVGparser* p, const char* id) -{ - NSVGgradientData* grad = p->gradients; - while (grad) { - if (strcmp(grad->id, id) == 0) - return grad; - grad = grad->next; - } - return NULL; -} - -static NSVGgradient* nsvg__createGradient(NSVGparser* p, const char* id, const float* localBounds, char* paintType) -{ - NSVGattrib* attr = nsvg__getAttr(p); - NSVGgradientData* data = NULL; - NSVGgradientData* ref = NULL; - NSVGgradientStop* stops = NULL; - NSVGgradient* grad; - float ox, oy, sw, sh, sl; - int nstops = 0; - - data = nsvg__findGradientData(p, id); - if (data == NULL) return NULL; - - // TODO: use ref to fill in all unset values too. - ref = data; - while (ref != NULL) { - if (stops == NULL && ref->stops != NULL) { - stops = ref->stops; - nstops = ref->nstops; - break; - } - ref = nsvg__findGradientData(p, ref->ref); - } - if (stops == NULL) return NULL; - - grad = (NSVGgradient*)malloc(sizeof(NSVGgradient) + sizeof(NSVGgradientStop)*(nstops-1)); - if (grad == NULL) return NULL; - - // The shape width and height. - if (data->units == NSVG_OBJECT_SPACE) { - ox = localBounds[0]; - oy = localBounds[1]; - sw = localBounds[2] - localBounds[0]; - sh = localBounds[3] - localBounds[1]; - } else { - ox = nsvg__actualOrigX(p); - oy = nsvg__actualOrigY(p); - sw = nsvg__actualWidth(p); - sh = nsvg__actualHeight(p); - } - sl = sqrtf(sw*sw + sh*sh) / sqrtf(2.0f); - - if (data->type == NSVG_PAINT_LINEAR_GRADIENT) { - float x1, y1, x2, y2, dx, dy; - x1 = nsvg__convertToPixels(p, data->linear.x1, ox, sw); - y1 = nsvg__convertToPixels(p, data->linear.y1, oy, sh); - x2 = nsvg__convertToPixels(p, data->linear.x2, ox, sw); - y2 = nsvg__convertToPixels(p, data->linear.y2, oy, sh); - // Calculate transform aligned to the line - dx = x2 - x1; - dy = y2 - y1; - grad->xform[0] = dy; grad->xform[1] = -dx; - grad->xform[2] = dx; grad->xform[3] = dy; - grad->xform[4] = x1; grad->xform[5] = y1; - } else { - float cx, cy, fx, fy, r; - cx = nsvg__convertToPixels(p, data->radial.cx, ox, sw); - cy = nsvg__convertToPixels(p, data->radial.cy, oy, sh); - fx = nsvg__convertToPixels(p, data->radial.fx, ox, sw); - fy = nsvg__convertToPixels(p, data->radial.fy, oy, sh); - r = nsvg__convertToPixels(p, data->radial.r, 0, sl); - // Calculate transform aligned to the circle - grad->xform[0] = r; grad->xform[1] = 0; - grad->xform[2] = 0; grad->xform[3] = r; - grad->xform[4] = cx; grad->xform[5] = cy; - grad->fx = fx / r; - grad->fy = fy / r; - } - - nsvg__xformMultiply(grad->xform, data->xform); - nsvg__xformMultiply(grad->xform, attr->xform); - - grad->spread = data->spread; - memcpy(grad->stops, stops, nstops*sizeof(NSVGgradientStop)); - grad->nstops = nstops; - - *paintType = data->type; - - return grad; -} - -static float nsvg__getAverageScale(float* t) -{ - float sx = sqrtf(t[0]*t[0] + t[2]*t[2]); - float sy = sqrtf(t[1]*t[1] + t[3]*t[3]); - return (sx + sy) * 0.5f; -} - -static void nsvg__getLocalBounds(float* bounds, NSVGshape *shape, float* xform) -{ - NSVGpath* path; - float curve[4*2], curveBounds[4]; - int i, first = 1; - for (path = shape->paths; path != NULL; path = path->next) { - nsvg__xformPoint(&curve[0], &curve[1], path->pts[0], path->pts[1], xform); - for (i = 0; i < path->npts-1; i += 3) { - nsvg__xformPoint(&curve[2], &curve[3], path->pts[(i+1)*2], path->pts[(i+1)*2+1], xform); - nsvg__xformPoint(&curve[4], &curve[5], path->pts[(i+2)*2], path->pts[(i+2)*2+1], xform); - nsvg__xformPoint(&curve[6], &curve[7], path->pts[(i+3)*2], path->pts[(i+3)*2+1], xform); - nsvg__curveBounds(curveBounds, curve); - if (first) { - bounds[0] = curveBounds[0]; - bounds[1] = curveBounds[1]; - bounds[2] = curveBounds[2]; - bounds[3] = curveBounds[3]; - first = 0; - } else { - bounds[0] = nsvg__minf(bounds[0], curveBounds[0]); - bounds[1] = nsvg__minf(bounds[1], curveBounds[1]); - bounds[2] = nsvg__maxf(bounds[2], curveBounds[2]); - bounds[3] = nsvg__maxf(bounds[3], curveBounds[3]); - } - curve[0] = curve[6]; - curve[1] = curve[7]; - } - } -} - -static void nsvg__addShape(NSVGparser* p) -{ - NSVGattrib* attr = nsvg__getAttr(p); - float scale = 1.0f; - NSVGshape* shape; - NSVGpath* path; - int i; - - if (p->plist == NULL) - return; - - shape = (NSVGshape*)malloc(sizeof(NSVGshape)); - if (shape == NULL) goto error; - memset(shape, 0, sizeof(NSVGshape)); - - memcpy(shape->id, attr->id, sizeof shape->id); - scale = nsvg__getAverageScale(attr->xform); - shape->strokeWidth = attr->strokeWidth * scale; - shape->strokeDashOffset = attr->strokeDashOffset * scale; - shape->strokeDashCount = (char)attr->strokeDashCount; - for (i = 0; i < attr->strokeDashCount; i++) - shape->strokeDashArray[i] = attr->strokeDashArray[i] * scale; - shape->strokeLineJoin = attr->strokeLineJoin; - shape->strokeLineCap = attr->strokeLineCap; - shape->miterLimit = attr->miterLimit; - shape->fillRule = attr->fillRule; - shape->opacity = attr->opacity; - - shape->paths = p->plist; - p->plist = NULL; - - // Calculate shape bounds - shape->bounds[0] = shape->paths->bounds[0]; - shape->bounds[1] = shape->paths->bounds[1]; - shape->bounds[2] = shape->paths->bounds[2]; - shape->bounds[3] = shape->paths->bounds[3]; - for (path = shape->paths->next; path != NULL; path = path->next) { - shape->bounds[0] = nsvg__minf(shape->bounds[0], path->bounds[0]); - shape->bounds[1] = nsvg__minf(shape->bounds[1], path->bounds[1]); - shape->bounds[2] = nsvg__maxf(shape->bounds[2], path->bounds[2]); - shape->bounds[3] = nsvg__maxf(shape->bounds[3], path->bounds[3]); - } - - // Set fill - if (attr->hasFill == 0) { - shape->fill.type = NSVG_PAINT_NONE; - } else if (attr->hasFill == 1) { - shape->fill.type = NSVG_PAINT_COLOR; - shape->fill.color = attr->fillColor; - shape->fill.color |= (unsigned int)(attr->fillOpacity*255) << 24; - } else if (attr->hasFill == 2) { - float inv[6], localBounds[4]; - nsvg__xformInverse(inv, attr->xform); - nsvg__getLocalBounds(localBounds, shape, inv); - shape->fill.gradient = nsvg__createGradient(p, attr->fillGradient, localBounds, &shape->fill.type); - if (shape->fill.gradient == NULL) { - shape->fill.type = NSVG_PAINT_NONE; - } - } - - // Set stroke - if (attr->hasStroke == 0) { - shape->stroke.type = NSVG_PAINT_NONE; - } else if (attr->hasStroke == 1) { - shape->stroke.type = NSVG_PAINT_COLOR; - shape->stroke.color = attr->strokeColor; - shape->stroke.color |= (unsigned int)(attr->strokeOpacity*255) << 24; - } else if (attr->hasStroke == 2) { - float inv[6], localBounds[4]; - nsvg__xformInverse(inv, attr->xform); - nsvg__getLocalBounds(localBounds, shape, inv); - shape->stroke.gradient = nsvg__createGradient(p, attr->strokeGradient, localBounds, &shape->stroke.type); - if (shape->stroke.gradient == NULL) - shape->stroke.type = NSVG_PAINT_NONE; - } - - // Set flags - shape->flags = (attr->visible ? NSVG_FLAGS_VISIBLE : 0x00); - - // Add to tail - if (p->image->shapes == NULL) - p->image->shapes = shape; - else - p->shapesTail->next = shape; - p->shapesTail = shape; - - return; - -error: - if (shape) free(shape); -} - -static void nsvg__addPath(NSVGparser* p, char closed) -{ - NSVGattrib* attr = nsvg__getAttr(p); - NSVGpath* path = NULL; - float bounds[4]; - float* curve; - int i; - - if (p->npts < 4) - return; - - if (closed) - nsvg__lineTo(p, p->pts[0], p->pts[1]); - - path = (NSVGpath*)malloc(sizeof(NSVGpath)); - if (path == NULL) goto error; - memset(path, 0, sizeof(NSVGpath)); - - path->pts = (float*)malloc(p->npts*2*sizeof(float)); - if (path->pts == NULL) goto error; - path->closed = closed; - path->npts = p->npts; - - // Transform path. - for (i = 0; i < p->npts; ++i) - nsvg__xformPoint(&path->pts[i*2], &path->pts[i*2+1], p->pts[i*2], p->pts[i*2+1], attr->xform); - - // Find bounds - for (i = 0; i < path->npts-1; i += 3) { - curve = &path->pts[i*2]; - nsvg__curveBounds(bounds, curve); - if (i == 0) { - path->bounds[0] = bounds[0]; - path->bounds[1] = bounds[1]; - path->bounds[2] = bounds[2]; - path->bounds[3] = bounds[3]; - } else { - path->bounds[0] = nsvg__minf(path->bounds[0], bounds[0]); - path->bounds[1] = nsvg__minf(path->bounds[1], bounds[1]); - path->bounds[2] = nsvg__maxf(path->bounds[2], bounds[2]); - path->bounds[3] = nsvg__maxf(path->bounds[3], bounds[3]); - } - } - - path->next = p->plist; - p->plist = path; - - return; - -error: - if (path != NULL) { - if (path->pts != NULL) free(path->pts); - free(path); - } -} - -// We roll our own string to float because the std library one uses locale and messes things up. -static double nsvg__atof(const char* s) -{ - char* cur = (char*)s; - char* end = NULL; - double res = 0.0, sign = 1.0; - long long intPart = 0, fracPart = 0; - char hasIntPart = 0, hasFracPart = 0; - - // Parse optional sign - if (*cur == '+') { - cur++; - } else if (*cur == '-') { - sign = -1; - cur++; - } - - // Parse integer part - if (nsvg__isdigit(*cur)) { - // Parse digit sequence - intPart = (double)strtoll(cur, &end, 10); - if (cur != end) { - res = (double)intPart; - hasIntPart = 1; - cur = end; - } - } - - // Parse fractional part. - if (*cur == '.') { - cur++; // Skip '.' - if (nsvg__isdigit(*cur)) { - // Parse digit sequence - fracPart = strtoll(cur, &end, 10); - if (cur != end) { - res += (double)fracPart / pow(10.0, (double)(end - cur)); - hasFracPart = 1; - cur = end; - } - } - } - - // A valid number should have integer or fractional part. - if (!hasIntPart && !hasFracPart) - return 0.0; - - // Parse optional exponent - if (*cur == 'e' || *cur == 'E') { - int expPart = 0; - cur++; // skip 'E' - expPart = strtol(cur, &end, 10); // Parse digit sequence with sign - if (cur != end) { - res *= pow(10.0, (double)expPart); - } - } - - return res * sign; -} - - -static const char* nsvg__parseNumber(const char* s, char* it, const int size) -{ - const int last = size-1; - int i = 0; - - // sign - if (*s == '-' || *s == '+') { - if (i < last) it[i++] = *s; - s++; - } - // integer part - while (*s && nsvg__isdigit(*s)) { - if (i < last) it[i++] = *s; - s++; - } - if (*s == '.') { - // decimal point - if (i < last) it[i++] = *s; - s++; - // fraction part - while (*s && nsvg__isdigit(*s)) { - if (i < last) it[i++] = *s; - s++; - } - } - // exponent - if (*s == 'e' || *s == 'E') { - if (i < last) it[i++] = *s; - s++; - if (*s == '-' || *s == '+') { - if (i < last) it[i++] = *s; - s++; - } - while (*s && nsvg__isdigit(*s)) { - if (i < last) it[i++] = *s; - s++; - } - } - it[i] = '\0'; - - return s; -} - -static const char* nsvg__getNextPathItem(const char* s, char* it) -{ - it[0] = '\0'; - // Skip white spaces and commas - while (*s && (nsvg__isspace(*s) || *s == ',')) s++; - if (!*s) return s; - if (*s == '-' || *s == '+' || *s == '.' || nsvg__isdigit(*s)) { - s = nsvg__parseNumber(s, it, 64); - } else { - // Parse command - it[0] = *s++; - it[1] = '\0'; - return s; - } - - return s; -} - -static unsigned int nsvg__parseColorHex(const char* str) -{ - unsigned int c = 0, r = 0, g = 0, b = 0; - int n = 0; - str++; // skip # - // Calculate number of characters. - while(str[n] && !nsvg__isspace(str[n])) - n++; - if (n == 6) { - sscanf(str, "%x", &c); - } else if (n == 3) { - sscanf(str, "%x", &c); - c = (c&0xf) | ((c&0xf0) << 4) | ((c&0xf00) << 8); - c |= c<<4; - } - r = (c >> 16) & 0xff; - g = (c >> 8) & 0xff; - b = c & 0xff; - return NSVG_RGB(r,g,b); -} - -static unsigned int nsvg__parseColorRGB(const char* str) -{ - int r = -1, g = -1, b = -1; - char s1[32]="", s2[32]=""; - sscanf(str + 4, "%d%[%%, \t]%d%[%%, \t]%d", &r, s1, &g, s2, &b); - if (strchr(s1, '%')) { - return NSVG_RGB((r*255)/100,(g*255)/100,(b*255)/100); - } else { - return NSVG_RGB(r,g,b); - } -} - -typedef struct NSVGNamedColor { - const char* name; - unsigned int color; -} NSVGNamedColor; - -NSVGNamedColor nsvg__colors[] = { - - { "red", NSVG_RGB(255, 0, 0) }, - { "green", NSVG_RGB( 0, 128, 0) }, - { "blue", NSVG_RGB( 0, 0, 255) }, - { "yellow", NSVG_RGB(255, 255, 0) }, - { "cyan", NSVG_RGB( 0, 255, 255) }, - { "magenta", NSVG_RGB(255, 0, 255) }, - { "black", NSVG_RGB( 0, 0, 0) }, - { "grey", NSVG_RGB(128, 128, 128) }, - { "gray", NSVG_RGB(128, 128, 128) }, - { "white", NSVG_RGB(255, 255, 255) }, - -#ifdef NANOSVG_ALL_COLOR_KEYWORDS - { "aliceblue", NSVG_RGB(240, 248, 255) }, - { "antiquewhite", NSVG_RGB(250, 235, 215) }, - { "aqua", NSVG_RGB( 0, 255, 255) }, - { "aquamarine", NSVG_RGB(127, 255, 212) }, - { "azure", NSVG_RGB(240, 255, 255) }, - { "beige", NSVG_RGB(245, 245, 220) }, - { "bisque", NSVG_RGB(255, 228, 196) }, - { "blanchedalmond", NSVG_RGB(255, 235, 205) }, - { "blueviolet", NSVG_RGB(138, 43, 226) }, - { "brown", NSVG_RGB(165, 42, 42) }, - { "burlywood", NSVG_RGB(222, 184, 135) }, - { "cadetblue", NSVG_RGB( 95, 158, 160) }, - { "chartreuse", NSVG_RGB(127, 255, 0) }, - { "chocolate", NSVG_RGB(210, 105, 30) }, - { "coral", NSVG_RGB(255, 127, 80) }, - { "cornflowerblue", NSVG_RGB(100, 149, 237) }, - { "cornsilk", NSVG_RGB(255, 248, 220) }, - { "crimson", NSVG_RGB(220, 20, 60) }, - { "darkblue", NSVG_RGB( 0, 0, 139) }, - { "darkcyan", NSVG_RGB( 0, 139, 139) }, - { "darkgoldenrod", NSVG_RGB(184, 134, 11) }, - { "darkgray", NSVG_RGB(169, 169, 169) }, - { "darkgreen", NSVG_RGB( 0, 100, 0) }, - { "darkgrey", NSVG_RGB(169, 169, 169) }, - { "darkkhaki", NSVG_RGB(189, 183, 107) }, - { "darkmagenta", NSVG_RGB(139, 0, 139) }, - { "darkolivegreen", NSVG_RGB( 85, 107, 47) }, - { "darkorange", NSVG_RGB(255, 140, 0) }, - { "darkorchid", NSVG_RGB(153, 50, 204) }, - { "darkred", NSVG_RGB(139, 0, 0) }, - { "darksalmon", NSVG_RGB(233, 150, 122) }, - { "darkseagreen", NSVG_RGB(143, 188, 143) }, - { "darkslateblue", NSVG_RGB( 72, 61, 139) }, - { "darkslategray", NSVG_RGB( 47, 79, 79) }, - { "darkslategrey", NSVG_RGB( 47, 79, 79) }, - { "darkturquoise", NSVG_RGB( 0, 206, 209) }, - { "darkviolet", NSVG_RGB(148, 0, 211) }, - { "deeppink", NSVG_RGB(255, 20, 147) }, - { "deepskyblue", NSVG_RGB( 0, 191, 255) }, - { "dimgray", NSVG_RGB(105, 105, 105) }, - { "dimgrey", NSVG_RGB(105, 105, 105) }, - { "dodgerblue", NSVG_RGB( 30, 144, 255) }, - { "firebrick", NSVG_RGB(178, 34, 34) }, - { "floralwhite", NSVG_RGB(255, 250, 240) }, - { "forestgreen", NSVG_RGB( 34, 139, 34) }, - { "fuchsia", NSVG_RGB(255, 0, 255) }, - { "gainsboro", NSVG_RGB(220, 220, 220) }, - { "ghostwhite", NSVG_RGB(248, 248, 255) }, - { "gold", NSVG_RGB(255, 215, 0) }, - { "goldenrod", NSVG_RGB(218, 165, 32) }, - { "greenyellow", NSVG_RGB(173, 255, 47) }, - { "honeydew", NSVG_RGB(240, 255, 240) }, - { "hotpink", NSVG_RGB(255, 105, 180) }, - { "indianred", NSVG_RGB(205, 92, 92) }, - { "indigo", NSVG_RGB( 75, 0, 130) }, - { "ivory", NSVG_RGB(255, 255, 240) }, - { "khaki", NSVG_RGB(240, 230, 140) }, - { "lavender", NSVG_RGB(230, 230, 250) }, - { "lavenderblush", NSVG_RGB(255, 240, 245) }, - { "lawngreen", NSVG_RGB(124, 252, 0) }, - { "lemonchiffon", NSVG_RGB(255, 250, 205) }, - { "lightblue", NSVG_RGB(173, 216, 230) }, - { "lightcoral", NSVG_RGB(240, 128, 128) }, - { "lightcyan", NSVG_RGB(224, 255, 255) }, - { "lightgoldenrodyellow", NSVG_RGB(250, 250, 210) }, - { "lightgray", NSVG_RGB(211, 211, 211) }, - { "lightgreen", NSVG_RGB(144, 238, 144) }, - { "lightgrey", NSVG_RGB(211, 211, 211) }, - { "lightpink", NSVG_RGB(255, 182, 193) }, - { "lightsalmon", NSVG_RGB(255, 160, 122) }, - { "lightseagreen", NSVG_RGB( 32, 178, 170) }, - { "lightskyblue", NSVG_RGB(135, 206, 250) }, - { "lightslategray", NSVG_RGB(119, 136, 153) }, - { "lightslategrey", NSVG_RGB(119, 136, 153) }, - { "lightsteelblue", NSVG_RGB(176, 196, 222) }, - { "lightyellow", NSVG_RGB(255, 255, 224) }, - { "lime", NSVG_RGB( 0, 255, 0) }, - { "limegreen", NSVG_RGB( 50, 205, 50) }, - { "linen", NSVG_RGB(250, 240, 230) }, - { "maroon", NSVG_RGB(128, 0, 0) }, - { "mediumaquamarine", NSVG_RGB(102, 205, 170) }, - { "mediumblue", NSVG_RGB( 0, 0, 205) }, - { "mediumorchid", NSVG_RGB(186, 85, 211) }, - { "mediumpurple", NSVG_RGB(147, 112, 219) }, - { "mediumseagreen", NSVG_RGB( 60, 179, 113) }, - { "mediumslateblue", NSVG_RGB(123, 104, 238) }, - { "mediumspringgreen", NSVG_RGB( 0, 250, 154) }, - { "mediumturquoise", NSVG_RGB( 72, 209, 204) }, - { "mediumvioletred", NSVG_RGB(199, 21, 133) }, - { "midnightblue", NSVG_RGB( 25, 25, 112) }, - { "mintcream", NSVG_RGB(245, 255, 250) }, - { "mistyrose", NSVG_RGB(255, 228, 225) }, - { "moccasin", NSVG_RGB(255, 228, 181) }, - { "navajowhite", NSVG_RGB(255, 222, 173) }, - { "navy", NSVG_RGB( 0, 0, 128) }, - { "oldlace", NSVG_RGB(253, 245, 230) }, - { "olive", NSVG_RGB(128, 128, 0) }, - { "olivedrab", NSVG_RGB(107, 142, 35) }, - { "orange", NSVG_RGB(255, 165, 0) }, - { "orangered", NSVG_RGB(255, 69, 0) }, - { "orchid", NSVG_RGB(218, 112, 214) }, - { "palegoldenrod", NSVG_RGB(238, 232, 170) }, - { "palegreen", NSVG_RGB(152, 251, 152) }, - { "paleturquoise", NSVG_RGB(175, 238, 238) }, - { "palevioletred", NSVG_RGB(219, 112, 147) }, - { "papayawhip", NSVG_RGB(255, 239, 213) }, - { "peachpuff", NSVG_RGB(255, 218, 185) }, - { "peru", NSVG_RGB(205, 133, 63) }, - { "pink", NSVG_RGB(255, 192, 203) }, - { "plum", NSVG_RGB(221, 160, 221) }, - { "powderblue", NSVG_RGB(176, 224, 230) }, - { "purple", NSVG_RGB(128, 0, 128) }, - { "rosybrown", NSVG_RGB(188, 143, 143) }, - { "royalblue", NSVG_RGB( 65, 105, 225) }, - { "saddlebrown", NSVG_RGB(139, 69, 19) }, - { "salmon", NSVG_RGB(250, 128, 114) }, - { "sandybrown", NSVG_RGB(244, 164, 96) }, - { "seagreen", NSVG_RGB( 46, 139, 87) }, - { "seashell", NSVG_RGB(255, 245, 238) }, - { "sienna", NSVG_RGB(160, 82, 45) }, - { "silver", NSVG_RGB(192, 192, 192) }, - { "skyblue", NSVG_RGB(135, 206, 235) }, - { "slateblue", NSVG_RGB(106, 90, 205) }, - { "slategray", NSVG_RGB(112, 128, 144) }, - { "slategrey", NSVG_RGB(112, 128, 144) }, - { "snow", NSVG_RGB(255, 250, 250) }, - { "springgreen", NSVG_RGB( 0, 255, 127) }, - { "steelblue", NSVG_RGB( 70, 130, 180) }, - { "tan", NSVG_RGB(210, 180, 140) }, - { "teal", NSVG_RGB( 0, 128, 128) }, - { "thistle", NSVG_RGB(216, 191, 216) }, - { "tomato", NSVG_RGB(255, 99, 71) }, - { "turquoise", NSVG_RGB( 64, 224, 208) }, - { "violet", NSVG_RGB(238, 130, 238) }, - { "wheat", NSVG_RGB(245, 222, 179) }, - { "whitesmoke", NSVG_RGB(245, 245, 245) }, - { "yellowgreen", NSVG_RGB(154, 205, 50) }, -#endif -}; - -static unsigned int nsvg__parseColorName(const char* str) -{ - int i, ncolors = sizeof(nsvg__colors) / sizeof(NSVGNamedColor); - - for (i = 0; i < ncolors; i++) { - if (strcmp(nsvg__colors[i].name, str) == 0) { - return nsvg__colors[i].color; - } - } - - return NSVG_RGB(128, 128, 128); -} - -static unsigned int nsvg__parseColor(const char* str) -{ - size_t len = 0; - while(*str == ' ') ++str; - len = strlen(str); - if (len >= 1 && *str == '#') - return nsvg__parseColorHex(str); - else if (len >= 4 && str[0] == 'r' && str[1] == 'g' && str[2] == 'b' && str[3] == '(') - return nsvg__parseColorRGB(str); - return nsvg__parseColorName(str); -} - -static float nsvg__parseOpacity(const char* str) -{ - float val = nsvg__atof(str); - if (val < 0.0f) val = 0.0f; - if (val > 1.0f) val = 1.0f; - return val; -} - -static float nsvg__parseMiterLimit(const char* str) -{ - float val = nsvg__atof(str); - if (val < 0.0f) val = 0.0f; - return val; -} - -static int nsvg__parseUnits(const char* units) -{ - if (units[0] == 'p' && units[1] == 'x') - return NSVG_UNITS_PX; - else if (units[0] == 'p' && units[1] == 't') - return NSVG_UNITS_PT; - else if (units[0] == 'p' && units[1] == 'c') - return NSVG_UNITS_PC; - else if (units[0] == 'm' && units[1] == 'm') - return NSVG_UNITS_MM; - else if (units[0] == 'c' && units[1] == 'm') - return NSVG_UNITS_CM; - else if (units[0] == 'i' && units[1] == 'n') - return NSVG_UNITS_IN; - else if (units[0] == '%') - return NSVG_UNITS_PERCENT; - else if (units[0] == 'e' && units[1] == 'm') - return NSVG_UNITS_EM; - else if (units[0] == 'e' && units[1] == 'x') - return NSVG_UNITS_EX; - return NSVG_UNITS_USER; -} - -static NSVGcoordinate nsvg__parseCoordinateRaw(const char* str) -{ - NSVGcoordinate coord = {0, NSVG_UNITS_USER}; - char buf[64]; - coord.units = nsvg__parseUnits(nsvg__parseNumber(str, buf, 64)); - coord.value = nsvg__atof(buf); - return coord; -} - -static NSVGcoordinate nsvg__coord(float v, int units) -{ - NSVGcoordinate coord = {v, units}; - return coord; -} - -static float nsvg__parseCoordinate(NSVGparser* p, const char* str, float orig, float length) -{ - NSVGcoordinate coord = nsvg__parseCoordinateRaw(str); - return nsvg__convertToPixels(p, coord, orig, length); -} - -static int nsvg__parseTransformArgs(const char* str, float* args, int maxNa, int* na) -{ - const char* end; - const char* ptr; - char it[64]; - - *na = 0; - ptr = str; - while (*ptr && *ptr != '(') ++ptr; - if (*ptr == 0) - return 1; - end = ptr; - while (*end && *end != ')') ++end; - if (*end == 0) - return 1; - - while (ptr < end) { - if (*ptr == '-' || *ptr == '+' || *ptr == '.' || nsvg__isdigit(*ptr)) { - if (*na >= maxNa) return 0; - ptr = nsvg__parseNumber(ptr, it, 64); - args[(*na)++] = (float)nsvg__atof(it); - } else { - ++ptr; - } - } - return (int)(end - str); -} - - -static int nsvg__parseMatrix(float* xform, const char* str) -{ - float t[6]; - int na = 0; - int len = nsvg__parseTransformArgs(str, t, 6, &na); - if (na != 6) return len; - memcpy(xform, t, sizeof(float)*6); - return len; -} - -static int nsvg__parseTranslate(float* xform, const char* str) -{ - float args[2]; - float t[6]; - int na = 0; - int len = nsvg__parseTransformArgs(str, args, 2, &na); - if (na == 1) args[1] = 0.0; - - nsvg__xformSetTranslation(t, args[0], args[1]); - memcpy(xform, t, sizeof(float)*6); - return len; -} - -static int nsvg__parseScale(float* xform, const char* str) -{ - float args[2]; - int na = 0; - float t[6]; - int len = nsvg__parseTransformArgs(str, args, 2, &na); - if (na == 1) args[1] = args[0]; - nsvg__xformSetScale(t, args[0], args[1]); - memcpy(xform, t, sizeof(float)*6); - return len; -} - -static int nsvg__parseSkewX(float* xform, const char* str) -{ - float args[1]; - int na = 0; - float t[6]; - int len = nsvg__parseTransformArgs(str, args, 1, &na); - nsvg__xformSetSkewX(t, args[0]/180.0f*NSVG_PI); - memcpy(xform, t, sizeof(float)*6); - return len; -} - -static int nsvg__parseSkewY(float* xform, const char* str) -{ - float args[1]; - int na = 0; - float t[6]; - int len = nsvg__parseTransformArgs(str, args, 1, &na); - nsvg__xformSetSkewY(t, args[0]/180.0f*NSVG_PI); - memcpy(xform, t, sizeof(float)*6); - return len; -} - -static int nsvg__parseRotate(float* xform, const char* str) -{ - float args[3]; - int na = 0; - float m[6]; - float t[6]; - int len = nsvg__parseTransformArgs(str, args, 3, &na); - if (na == 1) - args[1] = args[2] = 0.0f; - nsvg__xformIdentity(m); - - if (na > 1) { - nsvg__xformSetTranslation(t, -args[1], -args[2]); - nsvg__xformMultiply(m, t); - } - - nsvg__xformSetRotation(t, args[0]/180.0f*NSVG_PI); - nsvg__xformMultiply(m, t); - - if (na > 1) { - nsvg__xformSetTranslation(t, args[1], args[2]); - nsvg__xformMultiply(m, t); - } - - memcpy(xform, m, sizeof(float)*6); - - return len; -} - -static void nsvg__parseTransform(float* xform, const char* str) -{ - float t[6]; - nsvg__xformIdentity(xform); - while (*str) - { - if (strncmp(str, "matrix", 6) == 0) - str += nsvg__parseMatrix(t, str); - else if (strncmp(str, "translate", 9) == 0) - str += nsvg__parseTranslate(t, str); - else if (strncmp(str, "scale", 5) == 0) - str += nsvg__parseScale(t, str); - else if (strncmp(str, "rotate", 6) == 0) - str += nsvg__parseRotate(t, str); - else if (strncmp(str, "skewX", 5) == 0) - str += nsvg__parseSkewX(t, str); - else if (strncmp(str, "skewY", 5) == 0) - str += nsvg__parseSkewY(t, str); - else{ - ++str; - continue; - } - - nsvg__xformPremultiply(xform, t); - } -} - -static void nsvg__parseUrl(char* id, const char* str) -{ - int i = 0; - str += 4; // "url("; - if (*str == '#') - str++; - while (i < 63 && *str != ')') { - id[i] = *str++; - i++; - } - id[i] = '\0'; -} - -static char nsvg__parseLineCap(const char* str) -{ - if (strcmp(str, "butt") == 0) - return NSVG_CAP_BUTT; - else if (strcmp(str, "round") == 0) - return NSVG_CAP_ROUND; - else if (strcmp(str, "square") == 0) - return NSVG_CAP_SQUARE; - // TODO: handle inherit. - return NSVG_CAP_BUTT; -} - -static char nsvg__parseLineJoin(const char* str) -{ - if (strcmp(str, "miter") == 0) - return NSVG_JOIN_MITER; - else if (strcmp(str, "round") == 0) - return NSVG_JOIN_ROUND; - else if (strcmp(str, "bevel") == 0) - return NSVG_JOIN_BEVEL; - // TODO: handle inherit. - return NSVG_JOIN_MITER; -} - -static char nsvg__parseFillRule(const char* str) -{ - if (strcmp(str, "nonzero") == 0) - return NSVG_FILLRULE_NONZERO; - else if (strcmp(str, "evenodd") == 0) - return NSVG_FILLRULE_EVENODD; - // TODO: handle inherit. - return NSVG_FILLRULE_NONZERO; -} - -static const char* nsvg__getNextDashItem(const char* s, char* it) -{ - int n = 0; - it[0] = '\0'; - // Skip white spaces and commas - while (*s && (nsvg__isspace(*s) || *s == ',')) s++; - // Advance until whitespace, comma or end. - while (*s && (!nsvg__isspace(*s) && *s != ',')) { - if (n < 63) - it[n++] = *s; - s++; - } - it[n++] = '\0'; - return s; -} - -static int nsvg__parseStrokeDashArray(NSVGparser* p, const char* str, float* strokeDashArray) -{ - char item[64]; - int count = 0, i; - float sum = 0.0f; - - // Handle "none" - if (str[0] == 'n') - return 0; - - // Parse dashes - while (*str) { - str = nsvg__getNextDashItem(str, item); - if (!*item) break; - if (count < NSVG_MAX_DASHES) - strokeDashArray[count++] = fabsf(nsvg__parseCoordinate(p, item, 0.0f, nsvg__actualLength(p))); - } - - for (i = 0; i < count; i++) - sum += strokeDashArray[i]; - if (sum <= 1e-6f) - count = 0; - - return count; -} - -static void nsvg__parseStyle(NSVGparser* p, const char* str); - -static int nsvg__parseAttr(NSVGparser* p, const char* name, const char* value) -{ - float xform[6]; - NSVGattrib* attr = nsvg__getAttr(p); - if (!attr) return 0; - - if (strcmp(name, "style") == 0) { - nsvg__parseStyle(p, value); - } else if (strcmp(name, "display") == 0) { - if (strcmp(value, "none") == 0) - attr->visible = 0; - // Don't reset ->visible on display:inline, one display:none hides the whole subtree - - } else if (strcmp(name, "fill") == 0) { - if (strcmp(value, "none") == 0) { - attr->hasFill = 0; - } else if (strncmp(value, "url(", 4) == 0) { - attr->hasFill = 2; - nsvg__parseUrl(attr->fillGradient, value); - } else { - attr->hasFill = 1; - attr->fillColor = nsvg__parseColor(value); - } - } else if (strcmp(name, "opacity") == 0) { - attr->opacity = nsvg__parseOpacity(value); - } else if (strcmp(name, "fill-opacity") == 0) { - attr->fillOpacity = nsvg__parseOpacity(value); - } else if (strcmp(name, "stroke") == 0) { - if (strcmp(value, "none") == 0) { - attr->hasStroke = 0; - } else if (strncmp(value, "url(", 4) == 0) { - attr->hasStroke = 2; - nsvg__parseUrl(attr->strokeGradient, value); - } else { - attr->hasStroke = 1; - attr->strokeColor = nsvg__parseColor(value); - } - } else if (strcmp(name, "stroke-width") == 0) { - attr->strokeWidth = nsvg__parseCoordinate(p, value, 0.0f, nsvg__actualLength(p)); - } else if (strcmp(name, "stroke-dasharray") == 0) { - attr->strokeDashCount = nsvg__parseStrokeDashArray(p, value, attr->strokeDashArray); - } else if (strcmp(name, "stroke-dashoffset") == 0) { - attr->strokeDashOffset = nsvg__parseCoordinate(p, value, 0.0f, nsvg__actualLength(p)); - } else if (strcmp(name, "stroke-opacity") == 0) { - attr->strokeOpacity = nsvg__parseOpacity(value); - } else if (strcmp(name, "stroke-linecap") == 0) { - attr->strokeLineCap = nsvg__parseLineCap(value); - } else if (strcmp(name, "stroke-linejoin") == 0) { - attr->strokeLineJoin = nsvg__parseLineJoin(value); - } else if (strcmp(name, "stroke-miterlimit") == 0) { - attr->miterLimit = nsvg__parseMiterLimit(value); - } else if (strcmp(name, "fill-rule") == 0) { - attr->fillRule = nsvg__parseFillRule(value); - } else if (strcmp(name, "font-size") == 0) { - attr->fontSize = nsvg__parseCoordinate(p, value, 0.0f, nsvg__actualLength(p)); - } else if (strcmp(name, "transform") == 0) { - nsvg__parseTransform(xform, value); - nsvg__xformPremultiply(attr->xform, xform); - } else if (strcmp(name, "stop-color") == 0) { - attr->stopColor = nsvg__parseColor(value); - } else if (strcmp(name, "stop-opacity") == 0) { - attr->stopOpacity = nsvg__parseOpacity(value); - } else if (strcmp(name, "offset") == 0) { - attr->stopOffset = nsvg__parseCoordinate(p, value, 0.0f, 1.0f); - } else if (strcmp(name, "id") == 0) { - strncpy(attr->id, value, 63); - attr->id[63] = '\0'; - } else { - return 0; - } - return 1; -} - -static int nsvg__parseNameValue(NSVGparser* p, const char* start, const char* end) -{ - const char* str; - const char* val; - char name[512]; - char value[512]; - int n; - - str = start; - while (str < end && *str != ':') ++str; - - val = str; - - // Right Trim - while (str > start && (*str == ':' || nsvg__isspace(*str))) --str; - ++str; - - n = (int)(str - start); - if (n > 511) n = 511; - if (n) memcpy(name, start, n); - name[n] = 0; - - while (val < end && (*val == ':' || nsvg__isspace(*val))) ++val; - - n = (int)(end - val); - if (n > 511) n = 511; - if (n) memcpy(value, val, n); - value[n] = 0; - - return nsvg__parseAttr(p, name, value); -} - -static void nsvg__parseStyle(NSVGparser* p, const char* str) -{ - const char* start; - const char* end; - - while (*str) { - // Left Trim - while(*str && nsvg__isspace(*str)) ++str; - start = str; - while(*str && *str != ';') ++str; - end = str; - - // Right Trim - while (end > start && (*end == ';' || nsvg__isspace(*end))) --end; - ++end; - - nsvg__parseNameValue(p, start, end); - if (*str) ++str; - } -} - -static void nsvg__parseAttribs(NSVGparser* p, const char** attr) -{ - int i; - for (i = 0; attr[i]; i += 2) - { - if (strcmp(attr[i], "style") == 0) - nsvg__parseStyle(p, attr[i + 1]); - else - nsvg__parseAttr(p, attr[i], attr[i + 1]); - } -} - -static int nsvg__getArgsPerElement(char cmd) -{ - switch (cmd) { - case 'v': - case 'V': - case 'h': - case 'H': - return 1; - case 'm': - case 'M': - case 'l': - case 'L': - case 't': - case 'T': - return 2; - case 'q': - case 'Q': - case 's': - case 'S': - return 4; - case 'c': - case 'C': - return 6; - case 'a': - case 'A': - return 7; - } - return 0; -} - -static void nsvg__pathMoveTo(NSVGparser* p, float* cpx, float* cpy, float* args, int rel) -{ - if (rel) { - *cpx += args[0]; - *cpy += args[1]; - } else { - *cpx = args[0]; - *cpy = args[1]; - } - nsvg__moveTo(p, *cpx, *cpy); -} - -static void nsvg__pathLineTo(NSVGparser* p, float* cpx, float* cpy, float* args, int rel) -{ - if (rel) { - *cpx += args[0]; - *cpy += args[1]; - } else { - *cpx = args[0]; - *cpy = args[1]; - } - nsvg__lineTo(p, *cpx, *cpy); -} - -static void nsvg__pathHLineTo(NSVGparser* p, float* cpx, float* cpy, float* args, int rel) -{ - if (rel) - *cpx += args[0]; - else - *cpx = args[0]; - nsvg__lineTo(p, *cpx, *cpy); -} - -static void nsvg__pathVLineTo(NSVGparser* p, float* cpx, float* cpy, float* args, int rel) -{ - if (rel) - *cpy += args[0]; - else - *cpy = args[0]; - nsvg__lineTo(p, *cpx, *cpy); -} - -static void nsvg__pathCubicBezTo(NSVGparser* p, float* cpx, float* cpy, - float* cpx2, float* cpy2, float* args, int rel) -{ - float x2, y2, cx1, cy1, cx2, cy2; - - if (rel) { - cx1 = *cpx + args[0]; - cy1 = *cpy + args[1]; - cx2 = *cpx + args[2]; - cy2 = *cpy + args[3]; - x2 = *cpx + args[4]; - y2 = *cpy + args[5]; - } else { - cx1 = args[0]; - cy1 = args[1]; - cx2 = args[2]; - cy2 = args[3]; - x2 = args[4]; - y2 = args[5]; - } - - nsvg__cubicBezTo(p, cx1,cy1, cx2,cy2, x2,y2); - - *cpx2 = cx2; - *cpy2 = cy2; - *cpx = x2; - *cpy = y2; -} - -static void nsvg__pathCubicBezShortTo(NSVGparser* p, float* cpx, float* cpy, - float* cpx2, float* cpy2, float* args, int rel) -{ - float x1, y1, x2, y2, cx1, cy1, cx2, cy2; - - x1 = *cpx; - y1 = *cpy; - if (rel) { - cx2 = *cpx + args[0]; - cy2 = *cpy + args[1]; - x2 = *cpx + args[2]; - y2 = *cpy + args[3]; - } else { - cx2 = args[0]; - cy2 = args[1]; - x2 = args[2]; - y2 = args[3]; - } - - cx1 = 2*x1 - *cpx2; - cy1 = 2*y1 - *cpy2; - - nsvg__cubicBezTo(p, cx1,cy1, cx2,cy2, x2,y2); - - *cpx2 = cx2; - *cpy2 = cy2; - *cpx = x2; - *cpy = y2; -} - -static void nsvg__pathQuadBezTo(NSVGparser* p, float* cpx, float* cpy, - float* cpx2, float* cpy2, float* args, int rel) -{ - float x1, y1, x2, y2, cx, cy; - float cx1, cy1, cx2, cy2; - - x1 = *cpx; - y1 = *cpy; - if (rel) { - cx = *cpx + args[0]; - cy = *cpy + args[1]; - x2 = *cpx + args[2]; - y2 = *cpy + args[3]; - } else { - cx = args[0]; - cy = args[1]; - x2 = args[2]; - y2 = args[3]; - } - - // Convert to cubic bezier - cx1 = x1 + 2.0f/3.0f*(cx - x1); - cy1 = y1 + 2.0f/3.0f*(cy - y1); - cx2 = x2 + 2.0f/3.0f*(cx - x2); - cy2 = y2 + 2.0f/3.0f*(cy - y2); - - nsvg__cubicBezTo(p, cx1,cy1, cx2,cy2, x2,y2); - - *cpx2 = cx; - *cpy2 = cy; - *cpx = x2; - *cpy = y2; -} - -static void nsvg__pathQuadBezShortTo(NSVGparser* p, float* cpx, float* cpy, - float* cpx2, float* cpy2, float* args, int rel) -{ - float x1, y1, x2, y2, cx, cy; - float cx1, cy1, cx2, cy2; - - x1 = *cpx; - y1 = *cpy; - if (rel) { - x2 = *cpx + args[0]; - y2 = *cpy + args[1]; - } else { - x2 = args[0]; - y2 = args[1]; - } - - cx = 2*x1 - *cpx2; - cy = 2*y1 - *cpy2; - - // Convert to cubix bezier - cx1 = x1 + 2.0f/3.0f*(cx - x1); - cy1 = y1 + 2.0f/3.0f*(cy - y1); - cx2 = x2 + 2.0f/3.0f*(cx - x2); - cy2 = y2 + 2.0f/3.0f*(cy - y2); - - nsvg__cubicBezTo(p, cx1,cy1, cx2,cy2, x2,y2); - - *cpx2 = cx; - *cpy2 = cy; - *cpx = x2; - *cpy = y2; -} - -static float nsvg__sqr(float x) { return x*x; } -static float nsvg__vmag(float x, float y) { return sqrtf(x*x + y*y); } - -static float nsvg__vecrat(float ux, float uy, float vx, float vy) -{ - return (ux*vx + uy*vy) / (nsvg__vmag(ux,uy) * nsvg__vmag(vx,vy)); -} - -static float nsvg__vecang(float ux, float uy, float vx, float vy) -{ - float r = nsvg__vecrat(ux,uy, vx,vy); - if (r < -1.0f) r = -1.0f; - if (r > 1.0f) r = 1.0f; - return ((ux*vy < uy*vx) ? -1.0f : 1.0f) * acosf(r); -} - -static void nsvg__pathArcTo(NSVGparser* p, float* cpx, float* cpy, float* args, int rel) -{ - // Ported from canvg (https://code.google.com/p/canvg/) - float rx, ry, rotx; - float x1, y1, x2, y2, cx, cy, dx, dy, d; - float x1p, y1p, cxp, cyp, s, sa, sb; - float ux, uy, vx, vy, a1, da; - float x, y, tanx, tany, a, px = 0, py = 0, ptanx = 0, ptany = 0, t[6]; - float sinrx, cosrx; - int fa, fs; - int i, ndivs; - float hda, kappa; - - rx = fabsf(args[0]); // y radius - ry = fabsf(args[1]); // x radius - rotx = args[2] / 180.0f * NSVG_PI; // x rotation angle - fa = fabsf(args[3]) > 1e-6 ? 1 : 0; // Large arc - fs = fabsf(args[4]) > 1e-6 ? 1 : 0; // Sweep direction - x1 = *cpx; // start point - y1 = *cpy; - if (rel) { // end point - x2 = *cpx + args[5]; - y2 = *cpy + args[6]; - } else { - x2 = args[5]; - y2 = args[6]; - } - - dx = x1 - x2; - dy = y1 - y2; - d = sqrtf(dx*dx + dy*dy); - if (d < 1e-6f || rx < 1e-6f || ry < 1e-6f) { - // The arc degenerates to a line - nsvg__lineTo(p, x2, y2); - *cpx = x2; - *cpy = y2; - return; - } - - sinrx = sinf(rotx); - cosrx = cosf(rotx); - - // Convert to center point parameterization. - // http://www.w3.org/TR/SVG11/implnote.html#ArcImplementationNotes - // 1) Compute x1', y1' - x1p = cosrx * dx / 2.0f + sinrx * dy / 2.0f; - y1p = -sinrx * dx / 2.0f + cosrx * dy / 2.0f; - d = nsvg__sqr(x1p)/nsvg__sqr(rx) + nsvg__sqr(y1p)/nsvg__sqr(ry); - if (d > 1) { - d = sqrtf(d); - rx *= d; - ry *= d; - } - // 2) Compute cx', cy' - s = 0.0f; - sa = nsvg__sqr(rx)*nsvg__sqr(ry) - nsvg__sqr(rx)*nsvg__sqr(y1p) - nsvg__sqr(ry)*nsvg__sqr(x1p); - sb = nsvg__sqr(rx)*nsvg__sqr(y1p) + nsvg__sqr(ry)*nsvg__sqr(x1p); - if (sa < 0.0f) sa = 0.0f; - if (sb > 0.0f) - s = sqrtf(sa / sb); - if (fa == fs) - s = -s; - cxp = s * rx * y1p / ry; - cyp = s * -ry * x1p / rx; - - // 3) Compute cx,cy from cx',cy' - cx = (x1 + x2)/2.0f + cosrx*cxp - sinrx*cyp; - cy = (y1 + y2)/2.0f + sinrx*cxp + cosrx*cyp; - - // 4) Calculate theta1, and delta theta. - ux = (x1p - cxp) / rx; - uy = (y1p - cyp) / ry; - vx = (-x1p - cxp) / rx; - vy = (-y1p - cyp) / ry; - a1 = nsvg__vecang(1.0f,0.0f, ux,uy); // Initial angle - da = nsvg__vecang(ux,uy, vx,vy); // Delta angle - -// if (vecrat(ux,uy,vx,vy) <= -1.0f) da = NSVG_PI; -// if (vecrat(ux,uy,vx,vy) >= 1.0f) da = 0; - - if (fs == 0 && da > 0) - da -= 2 * NSVG_PI; - else if (fs == 1 && da < 0) - da += 2 * NSVG_PI; - - // Approximate the arc using cubic spline segments. - t[0] = cosrx; t[1] = sinrx; - t[2] = -sinrx; t[3] = cosrx; - t[4] = cx; t[5] = cy; - - // Split arc into max 90 degree segments. - // The loop assumes an iteration per end point (including start and end), this +1. - ndivs = (int)(fabsf(da) / (NSVG_PI*0.5f) + 1.0f); - hda = (da / (float)ndivs) / 2.0f; - kappa = fabsf(4.0f / 3.0f * (1.0f - cosf(hda)) / sinf(hda)); - if (da < 0.0f) - kappa = -kappa; - - for (i = 0; i <= ndivs; i++) { - a = a1 + da * ((float)i/(float)ndivs); - dx = cosf(a); - dy = sinf(a); - nsvg__xformPoint(&x, &y, dx*rx, dy*ry, t); // position - nsvg__xformVec(&tanx, &tany, -dy*rx * kappa, dx*ry * kappa, t); // tangent - if (i > 0) - nsvg__cubicBezTo(p, px+ptanx,py+ptany, x-tanx, y-tany, x, y); - px = x; - py = y; - ptanx = tanx; - ptany = tany; - } - - *cpx = x2; - *cpy = y2; -} - -static void nsvg__parsePath(NSVGparser* p, const char** attr) -{ - const char* s = NULL; - char cmd = '\0'; - float args[10]; - int nargs; - int rargs = 0; - float cpx, cpy, cpx2, cpy2; - const char* tmp[4]; - char closedFlag; - int i; - char item[64]; - - for (i = 0; attr[i]; i += 2) { - if (strcmp(attr[i], "d") == 0) { - s = attr[i + 1]; - } else { - tmp[0] = attr[i]; - tmp[1] = attr[i + 1]; - tmp[2] = 0; - tmp[3] = 0; - nsvg__parseAttribs(p, tmp); - } - } - - if (s) { - nsvg__resetPath(p); - cpx = 0; cpy = 0; - cpx2 = 0; cpy2 = 0; - closedFlag = 0; - nargs = 0; - - while (*s) { - s = nsvg__getNextPathItem(s, item); - if (!*item) break; - if (nsvg__isnum(item[0])) { - if (nargs < 10) - args[nargs++] = (float)nsvg__atof(item); - if (nargs >= rargs) { - switch (cmd) { - case 'm': - case 'M': - nsvg__pathMoveTo(p, &cpx, &cpy, args, cmd == 'm' ? 1 : 0); - // Moveto can be followed by multiple coordinate pairs, - // which should be treated as linetos. - cmd = (cmd == 'm') ? 'l' : 'L'; - rargs = nsvg__getArgsPerElement(cmd); - cpx2 = cpx; cpy2 = cpy; - break; - case 'l': - case 'L': - nsvg__pathLineTo(p, &cpx, &cpy, args, cmd == 'l' ? 1 : 0); - cpx2 = cpx; cpy2 = cpy; - break; - case 'H': - case 'h': - nsvg__pathHLineTo(p, &cpx, &cpy, args, cmd == 'h' ? 1 : 0); - cpx2 = cpx; cpy2 = cpy; - break; - case 'V': - case 'v': - nsvg__pathVLineTo(p, &cpx, &cpy, args, cmd == 'v' ? 1 : 0); - cpx2 = cpx; cpy2 = cpy; - break; - case 'C': - case 'c': - nsvg__pathCubicBezTo(p, &cpx, &cpy, &cpx2, &cpy2, args, cmd == 'c' ? 1 : 0); - break; - case 'S': - case 's': - nsvg__pathCubicBezShortTo(p, &cpx, &cpy, &cpx2, &cpy2, args, cmd == 's' ? 1 : 0); - break; - case 'Q': - case 'q': - nsvg__pathQuadBezTo(p, &cpx, &cpy, &cpx2, &cpy2, args, cmd == 'q' ? 1 : 0); - break; - case 'T': - case 't': - nsvg__pathQuadBezShortTo(p, &cpx, &cpy, &cpx2, &cpy2, args, cmd == 't' ? 1 : 0); - break; - case 'A': - case 'a': - nsvg__pathArcTo(p, &cpx, &cpy, args, cmd == 'a' ? 1 : 0); - cpx2 = cpx; cpy2 = cpy; - break; - default: - if (nargs >= 2) { - cpx = args[nargs-2]; - cpy = args[nargs-1]; - cpx2 = cpx; cpy2 = cpy; - } - break; - } - nargs = 0; - } - } else { - cmd = item[0]; - rargs = nsvg__getArgsPerElement(cmd); - if (cmd == 'M' || cmd == 'm') { - // Commit path. - if (p->npts > 0) - nsvg__addPath(p, closedFlag); - // Start new subpath. - nsvg__resetPath(p); - closedFlag = 0; - nargs = 0; - } else if (cmd == 'Z' || cmd == 'z') { - closedFlag = 1; - // Commit path. - if (p->npts > 0) { - // Move current point to first point - cpx = p->pts[0]; - cpy = p->pts[1]; - cpx2 = cpx; cpy2 = cpy; - nsvg__addPath(p, closedFlag); - } - // Start new subpath. - nsvg__resetPath(p); - nsvg__moveTo(p, cpx, cpy); - closedFlag = 0; - nargs = 0; - } - } - } - // Commit path. - if (p->npts) - nsvg__addPath(p, closedFlag); - } - - nsvg__addShape(p); -} - -static void nsvg__parseRect(NSVGparser* p, const char** attr) -{ - float x = 0.0f; - float y = 0.0f; - float w = 0.0f; - float h = 0.0f; - float rx = -1.0f; // marks not set - float ry = -1.0f; - int i; - - for (i = 0; attr[i]; i += 2) { - if (!nsvg__parseAttr(p, attr[i], attr[i + 1])) { - if (strcmp(attr[i], "x") == 0) x = nsvg__parseCoordinate(p, attr[i+1], nsvg__actualOrigX(p), nsvg__actualWidth(p)); - if (strcmp(attr[i], "y") == 0) y = nsvg__parseCoordinate(p, attr[i+1], nsvg__actualOrigY(p), nsvg__actualHeight(p)); - if (strcmp(attr[i], "width") == 0) w = nsvg__parseCoordinate(p, attr[i+1], 0.0f, nsvg__actualWidth(p)); - if (strcmp(attr[i], "height") == 0) h = nsvg__parseCoordinate(p, attr[i+1], 0.0f, nsvg__actualHeight(p)); - if (strcmp(attr[i], "rx") == 0) rx = fabsf(nsvg__parseCoordinate(p, attr[i+1], 0.0f, nsvg__actualWidth(p))); - if (strcmp(attr[i], "ry") == 0) ry = fabsf(nsvg__parseCoordinate(p, attr[i+1], 0.0f, nsvg__actualHeight(p))); - } - } - - if (rx < 0.0f && ry > 0.0f) rx = ry; - if (ry < 0.0f && rx > 0.0f) ry = rx; - if (rx < 0.0f) rx = 0.0f; - if (ry < 0.0f) ry = 0.0f; - if (rx > w/2.0f) rx = w/2.0f; - if (ry > h/2.0f) ry = h/2.0f; - - if (w != 0.0f && h != 0.0f) { - nsvg__resetPath(p); - - if (rx < 0.00001f || ry < 0.0001f) { - nsvg__moveTo(p, x, y); - nsvg__lineTo(p, x+w, y); - nsvg__lineTo(p, x+w, y+h); - nsvg__lineTo(p, x, y+h); - } else { - // Rounded rectangle - nsvg__moveTo(p, x+rx, y); - nsvg__lineTo(p, x+w-rx, y); - nsvg__cubicBezTo(p, x+w-rx*(1-NSVG_KAPPA90), y, x+w, y+ry*(1-NSVG_KAPPA90), x+w, y+ry); - nsvg__lineTo(p, x+w, y+h-ry); - nsvg__cubicBezTo(p, x+w, y+h-ry*(1-NSVG_KAPPA90), x+w-rx*(1-NSVG_KAPPA90), y+h, x+w-rx, y+h); - nsvg__lineTo(p, x+rx, y+h); - nsvg__cubicBezTo(p, x+rx*(1-NSVG_KAPPA90), y+h, x, y+h-ry*(1-NSVG_KAPPA90), x, y+h-ry); - nsvg__lineTo(p, x, y+ry); - nsvg__cubicBezTo(p, x, y+ry*(1-NSVG_KAPPA90), x+rx*(1-NSVG_KAPPA90), y, x+rx, y); - } - - nsvg__addPath(p, 1); - - nsvg__addShape(p); - } -} - -static void nsvg__parseCircle(NSVGparser* p, const char** attr) -{ - float cx = 0.0f; - float cy = 0.0f; - float r = 0.0f; - int i; - - for (i = 0; attr[i]; i += 2) { - if (!nsvg__parseAttr(p, attr[i], attr[i + 1])) { - if (strcmp(attr[i], "cx") == 0) cx = nsvg__parseCoordinate(p, attr[i+1], nsvg__actualOrigX(p), nsvg__actualWidth(p)); - if (strcmp(attr[i], "cy") == 0) cy = nsvg__parseCoordinate(p, attr[i+1], nsvg__actualOrigY(p), nsvg__actualHeight(p)); - if (strcmp(attr[i], "r") == 0) r = fabsf(nsvg__parseCoordinate(p, attr[i+1], 0.0f, nsvg__actualLength(p))); - } - } - - if (r > 0.0f) { - nsvg__resetPath(p); - - nsvg__moveTo(p, cx+r, cy); - nsvg__cubicBezTo(p, cx+r, cy+r*NSVG_KAPPA90, cx+r*NSVG_KAPPA90, cy+r, cx, cy+r); - nsvg__cubicBezTo(p, cx-r*NSVG_KAPPA90, cy+r, cx-r, cy+r*NSVG_KAPPA90, cx-r, cy); - nsvg__cubicBezTo(p, cx-r, cy-r*NSVG_KAPPA90, cx-r*NSVG_KAPPA90, cy-r, cx, cy-r); - nsvg__cubicBezTo(p, cx+r*NSVG_KAPPA90, cy-r, cx+r, cy-r*NSVG_KAPPA90, cx+r, cy); - - nsvg__addPath(p, 1); - - nsvg__addShape(p); - } -} - -static void nsvg__parseEllipse(NSVGparser* p, const char** attr) -{ - float cx = 0.0f; - float cy = 0.0f; - float rx = 0.0f; - float ry = 0.0f; - int i; - - for (i = 0; attr[i]; i += 2) { - if (!nsvg__parseAttr(p, attr[i], attr[i + 1])) { - if (strcmp(attr[i], "cx") == 0) cx = nsvg__parseCoordinate(p, attr[i+1], nsvg__actualOrigX(p), nsvg__actualWidth(p)); - if (strcmp(attr[i], "cy") == 0) cy = nsvg__parseCoordinate(p, attr[i+1], nsvg__actualOrigY(p), nsvg__actualHeight(p)); - if (strcmp(attr[i], "rx") == 0) rx = fabsf(nsvg__parseCoordinate(p, attr[i+1], 0.0f, nsvg__actualWidth(p))); - if (strcmp(attr[i], "ry") == 0) ry = fabsf(nsvg__parseCoordinate(p, attr[i+1], 0.0f, nsvg__actualHeight(p))); - } - } - - if (rx > 0.0f && ry > 0.0f) { - - nsvg__resetPath(p); - - nsvg__moveTo(p, cx+rx, cy); - nsvg__cubicBezTo(p, cx+rx, cy+ry*NSVG_KAPPA90, cx+rx*NSVG_KAPPA90, cy+ry, cx, cy+ry); - nsvg__cubicBezTo(p, cx-rx*NSVG_KAPPA90, cy+ry, cx-rx, cy+ry*NSVG_KAPPA90, cx-rx, cy); - nsvg__cubicBezTo(p, cx-rx, cy-ry*NSVG_KAPPA90, cx-rx*NSVG_KAPPA90, cy-ry, cx, cy-ry); - nsvg__cubicBezTo(p, cx+rx*NSVG_KAPPA90, cy-ry, cx+rx, cy-ry*NSVG_KAPPA90, cx+rx, cy); - - nsvg__addPath(p, 1); - - nsvg__addShape(p); - } -} - -static void nsvg__parseLine(NSVGparser* p, const char** attr) -{ - float x1 = 0.0; - float y1 = 0.0; - float x2 = 0.0; - float y2 = 0.0; - int i; - - for (i = 0; attr[i]; i += 2) { - if (!nsvg__parseAttr(p, attr[i], attr[i + 1])) { - if (strcmp(attr[i], "x1") == 0) x1 = nsvg__parseCoordinate(p, attr[i + 1], nsvg__actualOrigX(p), nsvg__actualWidth(p)); - if (strcmp(attr[i], "y1") == 0) y1 = nsvg__parseCoordinate(p, attr[i + 1], nsvg__actualOrigY(p), nsvg__actualHeight(p)); - if (strcmp(attr[i], "x2") == 0) x2 = nsvg__parseCoordinate(p, attr[i + 1], nsvg__actualOrigX(p), nsvg__actualWidth(p)); - if (strcmp(attr[i], "y2") == 0) y2 = nsvg__parseCoordinate(p, attr[i + 1], nsvg__actualOrigY(p), nsvg__actualHeight(p)); - } - } - - nsvg__resetPath(p); - - nsvg__moveTo(p, x1, y1); - nsvg__lineTo(p, x2, y2); - - nsvg__addPath(p, 0); - - nsvg__addShape(p); -} - -static void nsvg__parsePoly(NSVGparser* p, const char** attr, int closeFlag) -{ - int i; - const char* s; - float args[2]; - int nargs, npts = 0; - char item[64]; - - nsvg__resetPath(p); - - for (i = 0; attr[i]; i += 2) { - if (!nsvg__parseAttr(p, attr[i], attr[i + 1])) { - if (strcmp(attr[i], "points") == 0) { - s = attr[i + 1]; - nargs = 0; - while (*s) { - s = nsvg__getNextPathItem(s, item); - args[nargs++] = (float)nsvg__atof(item); - if (nargs >= 2) { - if (npts == 0) - nsvg__moveTo(p, args[0], args[1]); - else - nsvg__lineTo(p, args[0], args[1]); - nargs = 0; - npts++; - } - } - } - } - } - - nsvg__addPath(p, (char)closeFlag); - - nsvg__addShape(p); -} - -static void nsvg__parseSVG(NSVGparser* p, const char** attr) -{ - int i; - for (i = 0; attr[i]; i += 2) { - if (!nsvg__parseAttr(p, attr[i], attr[i + 1])) { - if (strcmp(attr[i], "width") == 0) { - p->image->width = nsvg__parseCoordinate(p, attr[i + 1], 0.0f, 0.0f); - } else if (strcmp(attr[i], "height") == 0) { - p->image->height = nsvg__parseCoordinate(p, attr[i + 1], 0.0f, 0.0f); - } else if (strcmp(attr[i], "viewBox") == 0) { - const char *s = attr[i + 1]; - char buf[64]; - s = nsvg__parseNumber(s, buf, 64); - p->viewMinx = nsvg__atof(buf); - while (*s && (nsvg__isspace(*s) || *s == '%' || *s == ',')) s++; - if (!*s) return; - s = nsvg__parseNumber(s, buf, 64); - p->viewMiny = nsvg__atof(buf); - while (*s && (nsvg__isspace(*s) || *s == '%' || *s == ',')) s++; - if (!*s) return; - s = nsvg__parseNumber(s, buf, 64); - p->viewWidth = nsvg__atof(buf); - while (*s && (nsvg__isspace(*s) || *s == '%' || *s == ',')) s++; - if (!*s) return; - s = nsvg__parseNumber(s, buf, 64); - p->viewHeight = nsvg__atof(buf); - } else if (strcmp(attr[i], "preserveAspectRatio") == 0) { - if (strstr(attr[i + 1], "none") != 0) { - // No uniform scaling - p->alignType = NSVG_ALIGN_NONE; - } else { - // Parse X align - if (strstr(attr[i + 1], "xMin") != 0) - p->alignX = NSVG_ALIGN_MIN; - else if (strstr(attr[i + 1], "xMid") != 0) - p->alignX = NSVG_ALIGN_MID; - else if (strstr(attr[i + 1], "xMax") != 0) - p->alignX = NSVG_ALIGN_MAX; - // Parse X align - if (strstr(attr[i + 1], "yMin") != 0) - p->alignY = NSVG_ALIGN_MIN; - else if (strstr(attr[i + 1], "yMid") != 0) - p->alignY = NSVG_ALIGN_MID; - else if (strstr(attr[i + 1], "yMax") != 0) - p->alignY = NSVG_ALIGN_MAX; - // Parse meet/slice - p->alignType = NSVG_ALIGN_MEET; - if (strstr(attr[i + 1], "slice") != 0) - p->alignType = NSVG_ALIGN_SLICE; - } - } - } - } -} - -static void nsvg__parseGradient(NSVGparser* p, const char** attr, char type) -{ - int i; - NSVGgradientData* grad = (NSVGgradientData*)malloc(sizeof(NSVGgradientData)); - if (grad == NULL) return; - memset(grad, 0, sizeof(NSVGgradientData)); - grad->units = NSVG_OBJECT_SPACE; - grad->type = type; - if (grad->type == NSVG_PAINT_LINEAR_GRADIENT) { - grad->linear.x1 = nsvg__coord(0.0f, NSVG_UNITS_PERCENT); - grad->linear.y1 = nsvg__coord(0.0f, NSVG_UNITS_PERCENT); - grad->linear.x2 = nsvg__coord(100.0f, NSVG_UNITS_PERCENT); - grad->linear.y2 = nsvg__coord(0.0f, NSVG_UNITS_PERCENT); - } else if (grad->type == NSVG_PAINT_RADIAL_GRADIENT) { - grad->radial.cx = nsvg__coord(50.0f, NSVG_UNITS_PERCENT); - grad->radial.cy = nsvg__coord(50.0f, NSVG_UNITS_PERCENT); - grad->radial.r = nsvg__coord(50.0f, NSVG_UNITS_PERCENT); - } - - nsvg__xformIdentity(grad->xform); - - for (i = 0; attr[i]; i += 2) { - if (strcmp(attr[i], "id") == 0) { - strncpy(grad->id, attr[i+1], 63); - grad->id[63] = '\0'; - } else if (!nsvg__parseAttr(p, attr[i], attr[i + 1])) { - if (strcmp(attr[i], "gradientUnits") == 0) { - if (strcmp(attr[i+1], "objectBoundingBox") == 0) - grad->units = NSVG_OBJECT_SPACE; - else - grad->units = NSVG_USER_SPACE; - } else if (strcmp(attr[i], "gradientTransform") == 0) { - nsvg__parseTransform(grad->xform, attr[i + 1]); - } else if (strcmp(attr[i], "cx") == 0) { - grad->radial.cx = nsvg__parseCoordinateRaw(attr[i + 1]); - } else if (strcmp(attr[i], "cy") == 0) { - grad->radial.cy = nsvg__parseCoordinateRaw(attr[i + 1]); - } else if (strcmp(attr[i], "r") == 0) { - grad->radial.r = nsvg__parseCoordinateRaw(attr[i + 1]); - } else if (strcmp(attr[i], "fx") == 0) { - grad->radial.fx = nsvg__parseCoordinateRaw(attr[i + 1]); - } else if (strcmp(attr[i], "fy") == 0) { - grad->radial.fy = nsvg__parseCoordinateRaw(attr[i + 1]); - } else if (strcmp(attr[i], "x1") == 0) { - grad->linear.x1 = nsvg__parseCoordinateRaw(attr[i + 1]); - } else if (strcmp(attr[i], "y1") == 0) { - grad->linear.y1 = nsvg__parseCoordinateRaw(attr[i + 1]); - } else if (strcmp(attr[i], "x2") == 0) { - grad->linear.x2 = nsvg__parseCoordinateRaw(attr[i + 1]); - } else if (strcmp(attr[i], "y2") == 0) { - grad->linear.y2 = nsvg__parseCoordinateRaw(attr[i + 1]); - } else if (strcmp(attr[i], "spreadMethod") == 0) { - if (strcmp(attr[i+1], "pad") == 0) - grad->spread = NSVG_SPREAD_PAD; - else if (strcmp(attr[i+1], "reflect") == 0) - grad->spread = NSVG_SPREAD_REFLECT; - else if (strcmp(attr[i+1], "repeat") == 0) - grad->spread = NSVG_SPREAD_REPEAT; - } else if (strcmp(attr[i], "xlink:href") == 0) { - const char *href = attr[i+1]; - strncpy(grad->ref, href+1, 62); - grad->ref[62] = '\0'; - } - } - } - - grad->next = p->gradients; - p->gradients = grad; -} - -static void nsvg__parseGradientStop(NSVGparser* p, const char** attr) -{ - NSVGattrib* curAttr = nsvg__getAttr(p); - NSVGgradientData* grad; - NSVGgradientStop* stop; - int i, idx; - - curAttr->stopOffset = 0; - curAttr->stopColor = 0; - curAttr->stopOpacity = 1.0f; - - for (i = 0; attr[i]; i += 2) { - nsvg__parseAttr(p, attr[i], attr[i + 1]); - } - - // Add stop to the last gradient. - grad = p->gradients; - if (grad == NULL) return; - - grad->nstops++; - grad->stops = (NSVGgradientStop*)realloc(grad->stops, sizeof(NSVGgradientStop)*grad->nstops); - if (grad->stops == NULL) return; - - // Insert - idx = grad->nstops-1; - for (i = 0; i < grad->nstops-1; i++) { - if (curAttr->stopOffset < grad->stops[i].offset) { - idx = i; - break; - } - } - if (idx != grad->nstops-1) { - for (i = grad->nstops-1; i > idx; i--) - grad->stops[i] = grad->stops[i-1]; - } - - stop = &grad->stops[idx]; - stop->color = curAttr->stopColor; - stop->color |= (unsigned int)(curAttr->stopOpacity*255) << 24; - stop->offset = curAttr->stopOffset; -} - -static void nsvg__startElement(void* ud, const char* el, const char** attr) -{ - NSVGparser* p = (NSVGparser*)ud; - - if (p->defsFlag) { - // Skip everything but gradients in defs - if (strcmp(el, "linearGradient") == 0) { - nsvg__parseGradient(p, attr, NSVG_PAINT_LINEAR_GRADIENT); - } else if (strcmp(el, "radialGradient") == 0) { - nsvg__parseGradient(p, attr, NSVG_PAINT_RADIAL_GRADIENT); - } else if (strcmp(el, "stop") == 0) { - nsvg__parseGradientStop(p, attr); - } - return; - } - - if (strcmp(el, "g") == 0) { - nsvg__pushAttr(p); - nsvg__parseAttribs(p, attr); - } else if (strcmp(el, "path") == 0) { - if (p->pathFlag) // Do not allow nested paths. - return; - nsvg__pushAttr(p); - nsvg__parsePath(p, attr); - nsvg__popAttr(p); - } else if (strcmp(el, "rect") == 0) { - nsvg__pushAttr(p); - nsvg__parseRect(p, attr); - nsvg__popAttr(p); - } else if (strcmp(el, "circle") == 0) { - nsvg__pushAttr(p); - nsvg__parseCircle(p, attr); - nsvg__popAttr(p); - } else if (strcmp(el, "ellipse") == 0) { - nsvg__pushAttr(p); - nsvg__parseEllipse(p, attr); - nsvg__popAttr(p); - } else if (strcmp(el, "line") == 0) { - nsvg__pushAttr(p); - nsvg__parseLine(p, attr); - nsvg__popAttr(p); - } else if (strcmp(el, "polyline") == 0) { - nsvg__pushAttr(p); - nsvg__parsePoly(p, attr, 0); - nsvg__popAttr(p); - } else if (strcmp(el, "polygon") == 0) { - nsvg__pushAttr(p); - nsvg__parsePoly(p, attr, 1); - nsvg__popAttr(p); - } else if (strcmp(el, "linearGradient") == 0) { - nsvg__parseGradient(p, attr, NSVG_PAINT_LINEAR_GRADIENT); - } else if (strcmp(el, "radialGradient") == 0) { - nsvg__parseGradient(p, attr, NSVG_PAINT_RADIAL_GRADIENT); - } else if (strcmp(el, "stop") == 0) { - nsvg__parseGradientStop(p, attr); - } else if (strcmp(el, "defs") == 0) { - p->defsFlag = 1; - } else if (strcmp(el, "svg") == 0) { - nsvg__parseSVG(p, attr); - } -} - -static void nsvg__endElement(void* ud, const char* el) -{ - NSVGparser* p = (NSVGparser*)ud; - - if (strcmp(el, "g") == 0) { - nsvg__popAttr(p); - } else if (strcmp(el, "path") == 0) { - p->pathFlag = 0; - } else if (strcmp(el, "defs") == 0) { - p->defsFlag = 0; - } -} - -static void nsvg__content(void* ud, const char* s) -{ - NSVG_NOTUSED(ud); - NSVG_NOTUSED(s); - // empty -} - -static void nsvg__imageBounds(NSVGparser* p, float* bounds) -{ - NSVGshape* shape; - shape = p->image->shapes; - if (shape == NULL) { - bounds[0] = bounds[1] = bounds[2] = bounds[3] = 0.0; - return; - } - bounds[0] = shape->bounds[0]; - bounds[1] = shape->bounds[1]; - bounds[2] = shape->bounds[2]; - bounds[3] = shape->bounds[3]; - for (shape = shape->next; shape != NULL; shape = shape->next) { - bounds[0] = nsvg__minf(bounds[0], shape->bounds[0]); - bounds[1] = nsvg__minf(bounds[1], shape->bounds[1]); - bounds[2] = nsvg__maxf(bounds[2], shape->bounds[2]); - bounds[3] = nsvg__maxf(bounds[3], shape->bounds[3]); - } -} - -static float nsvg__viewAlign(float content, float container, int type) -{ - if (type == NSVG_ALIGN_MIN) - return 0; - else if (type == NSVG_ALIGN_MAX) - return container - content; - // mid - return (container - content) * 0.5f; -} - -static void nsvg__scaleGradient(NSVGgradient* grad, float tx, float ty, float sx, float sy) -{ - float t[6]; - nsvg__xformSetTranslation(t, tx, ty); - nsvg__xformMultiply (grad->xform, t); - - nsvg__xformSetScale(t, sx, sy); - nsvg__xformMultiply (grad->xform, t); -} - -static void nsvg__scaleToViewbox(NSVGparser* p, const char* units) -{ - NSVGshape* shape; - NSVGpath* path; - float tx, ty, sx, sy, us, bounds[4], t[6], avgs; - int i; - float* pt; - - // Guess image size if not set completely. - nsvg__imageBounds(p, bounds); - - if (p->viewWidth == 0) { - if (p->image->width > 0) { - p->viewWidth = p->image->width; - } else { - p->viewMinx = bounds[0]; - p->viewWidth = bounds[2] - bounds[0]; - } - } - if (p->viewHeight == 0) { - if (p->image->height > 0) { - p->viewHeight = p->image->height; - } else { - p->viewMiny = bounds[1]; - p->viewHeight = bounds[3] - bounds[1]; - } - } - if (p->image->width == 0) - p->image->width = p->viewWidth; - if (p->image->height == 0) - p->image->height = p->viewHeight; - - tx = -p->viewMinx; - ty = -p->viewMiny; - sx = p->viewWidth > 0 ? p->image->width / p->viewWidth : 0; - sy = p->viewHeight > 0 ? p->image->height / p->viewHeight : 0; - // Unit scaling - us = 1.0f / nsvg__convertToPixels(p, nsvg__coord(1.0f, nsvg__parseUnits(units)), 0.0f, 1.0f); - - // Fix aspect ratio - if (p->alignType == NSVG_ALIGN_MEET) { - // fit whole image into viewbox - sx = sy = nsvg__minf(sx, sy); - tx += nsvg__viewAlign(p->viewWidth*sx, p->image->width, p->alignX) / sx; - ty += nsvg__viewAlign(p->viewHeight*sy, p->image->height, p->alignY) / sy; - } else if (p->alignType == NSVG_ALIGN_SLICE) { - // fill whole viewbox with image - sx = sy = nsvg__maxf(sx, sy); - tx += nsvg__viewAlign(p->viewWidth*sx, p->image->width, p->alignX) / sx; - ty += nsvg__viewAlign(p->viewHeight*sy, p->image->height, p->alignY) / sy; - } - - // Transform - sx *= us; - sy *= us; - avgs = (sx+sy) / 2.0f; - for (shape = p->image->shapes; shape != NULL; shape = shape->next) { - shape->bounds[0] = (shape->bounds[0] + tx) * sx; - shape->bounds[1] = (shape->bounds[1] + ty) * sy; - shape->bounds[2] = (shape->bounds[2] + tx) * sx; - shape->bounds[3] = (shape->bounds[3] + ty) * sy; - for (path = shape->paths; path != NULL; path = path->next) { - path->bounds[0] = (path->bounds[0] + tx) * sx; - path->bounds[1] = (path->bounds[1] + ty) * sy; - path->bounds[2] = (path->bounds[2] + tx) * sx; - path->bounds[3] = (path->bounds[3] + ty) * sy; - for (i =0; i < path->npts; i++) { - pt = &path->pts[i*2]; - pt[0] = (pt[0] + tx) * sx; - pt[1] = (pt[1] + ty) * sy; - } - } - - if (shape->fill.type == NSVG_PAINT_LINEAR_GRADIENT || shape->fill.type == NSVG_PAINT_RADIAL_GRADIENT) { - nsvg__scaleGradient(shape->fill.gradient, tx,ty, sx,sy); - memcpy(t, shape->fill.gradient->xform, sizeof(float)*6); - nsvg__xformInverse(shape->fill.gradient->xform, t); - } - if (shape->stroke.type == NSVG_PAINT_LINEAR_GRADIENT || shape->stroke.type == NSVG_PAINT_RADIAL_GRADIENT) { - nsvg__scaleGradient(shape->stroke.gradient, tx,ty, sx,sy); - memcpy(t, shape->stroke.gradient->xform, sizeof(float)*6); - nsvg__xformInverse(shape->stroke.gradient->xform, t); - } - - shape->strokeWidth *= avgs; - shape->strokeDashOffset *= avgs; - for (i = 0; i < shape->strokeDashCount; i++) - shape->strokeDashArray[i] *= avgs; - } -} - -NSVGimage* nsvgParse(char* input, const char* units, float dpi) -{ - NSVGparser* p; - NSVGimage* ret = 0; - - p = nsvg__createParser(); - if (p == NULL) { - return NULL; - } - p->dpi = dpi; - - nsvg__parseXML(input, nsvg__startElement, nsvg__endElement, nsvg__content, p); - - // Scale to viewBox - nsvg__scaleToViewbox(p, units); - - ret = p->image; - p->image = NULL; - - nsvg__deleteParser(p); - - return ret; -} - -#include - -NSVGimage* nsvgParseFromFile(const char* filename, const char* units, float dpi) -{ - FILE* fp = NULL; - size_t size; - char* data = NULL; - NSVGimage* image = NULL; - - fp = boost::nowide::fopen(filename, "rb"); - if (!fp) goto error; - fseek(fp, 0, SEEK_END); - size = ftell(fp); - fseek(fp, 0, SEEK_SET); - data = (char*)malloc(size+1); - if (data == NULL) goto error; - if (fread(data, 1, size, fp) != size) goto error; - data[size] = '\0'; // Must be null terminated. - fclose(fp); - - image = nsvgParse(data, units, dpi); - free(data); - return image; - -error: - if (fp) fclose(fp); - if (data) free(data); - if (image) nsvgDelete(image); - return NULL; -} - -NSVGpath* nsvgDuplicatePath(NSVGpath* p) -{ - NSVGpath* res = NULL; - - if (p == NULL) - return NULL; - - res = (NSVGpath*)malloc(sizeof(NSVGpath)); - if (res == NULL) goto error; - memset(res, 0, sizeof(NSVGpath)); - - res->pts = (float*)malloc(p->npts*2*sizeof(float)); - if (res->pts == NULL) goto error; - memcpy(res->pts, p->pts, p->npts * sizeof(float) * 2); - res->npts = p->npts; - - memcpy(res->bounds, p->bounds, sizeof(p->bounds)); - - res->closed = p->closed; - - return res; - -error: - if (res != NULL) { - free(res->pts); - free(res); - } - return NULL; -} - -void nsvgDelete(NSVGimage* image) -{ - NSVGshape *snext, *shape; - if (image == NULL) return; - shape = image->shapes; - while (shape != NULL) { - snext = shape->next; - nsvg__deletePaths(shape->paths); - nsvg__deletePaint(&shape->fill); - nsvg__deletePaint(&shape->stroke); - free(shape); - shape = snext; - } - free(image); -} - -#endif diff --git a/src/nanosvg/nanosvgrast.h b/src/nanosvg/nanosvgrast.h deleted file mode 100644 index b740c316c..000000000 --- a/src/nanosvg/nanosvgrast.h +++ /dev/null @@ -1,1452 +0,0 @@ -/* - * Copyright (c) 2013-14 Mikko Mononen memon@inside.org - * - * This software is provided 'as-is', without any express or implied - * warranty. In no event will the authors be held liable for any damages - * arising from the use of this software. - * - * Permission is granted to anyone to use this software for any purpose, - * including commercial applications, and to alter it and redistribute it - * freely, subject to the following restrictions: - * - * 1. The origin of this software must not be misrepresented; you must not - * claim that you wrote the original software. If you use this software - * in a product, an acknowledgment in the product documentation would be - * appreciated but is not required. - * 2. Altered source versions must be plainly marked as such, and must not be - * misrepresented as being the original software. - * 3. This notice may not be removed or altered from any source distribution. - * - * The polygon rasterization is heavily based on stb_truetype rasterizer - * by Sean Barrett - http://nothings.org/ - * - */ - -#ifndef NANOSVGRAST_H -#define NANOSVGRAST_H - -#ifndef NANOSVGRAST_CPLUSPLUS -#ifdef __cplusplus -extern "C" { -#endif -#endif - -typedef struct NSVGrasterizer NSVGrasterizer; - -/* Example Usage: - // Load SVG - NSVGimage* image; - image = nsvgParseFromFile("test.svg", "px", 96); - - // Create rasterizer (can be used to render multiple images). - struct NSVGrasterizer* rast = nsvgCreateRasterizer(); - // Allocate memory for image - unsigned char* img = malloc(w*h*4); - // Rasterize - nsvgRasterize(rast, image, 0,0,1, img, w, h, w*4); -*/ - -// Allocated rasterizer context. -NSVGrasterizer* nsvgCreateRasterizer(); - -// Rasterizes SVG image, returns RGBA image (non-premultiplied alpha) -// r - pointer to rasterizer context -// image - pointer to image to rasterize -// tx,ty - image offset (applied after scaling) -// scale - image scale -// dst - pointer to destination image data, 4 bytes per pixel (RGBA) -// w - width of the image to render -// h - height of the image to render -// stride - number of bytes per scaleline in the destination buffer -void nsvgRasterize(NSVGrasterizer* r, - NSVGimage* image, float tx, float ty, float scale, - unsigned char* dst, int w, int h, int stride); - -// Deletes rasterizer context. -void nsvgDeleteRasterizer(NSVGrasterizer*); - - -#ifndef NANOSVGRAST_CPLUSPLUS -#ifdef __cplusplus -} -#endif -#endif - -#endif // NANOSVGRAST_H - -#ifdef NANOSVGRAST_IMPLEMENTATION - -#include - -#define NSVG__SUBSAMPLES 5 -#define NSVG__FIXSHIFT 10 -#define NSVG__FIX (1 << NSVG__FIXSHIFT) -#define NSVG__FIXMASK (NSVG__FIX-1) -#define NSVG__MEMPAGE_SIZE 1024 - -typedef struct NSVGedge { - float x0,y0, x1,y1; - int dir; - struct NSVGedge* next; -} NSVGedge; - -typedef struct NSVGpoint { - float x, y; - float dx, dy; - float len; - float dmx, dmy; - unsigned char flags; -} NSVGpoint; - -typedef struct NSVGactiveEdge { - int x,dx; - float ey; - int dir; - struct NSVGactiveEdge *next; -} NSVGactiveEdge; - -typedef struct NSVGmemPage { - unsigned char mem[NSVG__MEMPAGE_SIZE]; - int size; - struct NSVGmemPage* next; -} NSVGmemPage; - -typedef struct NSVGcachedPaint { - char type; - char spread; - float xform[6]; - unsigned int colors[256]; -} NSVGcachedPaint; - -struct NSVGrasterizer -{ - float px, py; - - float tessTol; - float distTol; - - NSVGedge* edges; - int nedges; - int cedges; - - NSVGpoint* points; - int npoints; - int cpoints; - - NSVGpoint* points2; - int npoints2; - int cpoints2; - - NSVGactiveEdge* freelist; - NSVGmemPage* pages; - NSVGmemPage* curpage; - - unsigned char* scanline; - int cscanline; - - unsigned char* bitmap; - int width, height, stride; -}; - -NSVGrasterizer* nsvgCreateRasterizer() -{ - NSVGrasterizer* r = (NSVGrasterizer*)malloc(sizeof(NSVGrasterizer)); - if (r == NULL) goto error; - memset(r, 0, sizeof(NSVGrasterizer)); - - r->tessTol = 0.25f; - r->distTol = 0.01f; - - return r; - -error: - nsvgDeleteRasterizer(r); - return NULL; -} - -void nsvgDeleteRasterizer(NSVGrasterizer* r) -{ - NSVGmemPage* p; - - if (r == NULL) return; - - p = r->pages; - while (p != NULL) { - NSVGmemPage* next = p->next; - free(p); - p = next; - } - - if (r->edges) free(r->edges); - if (r->points) free(r->points); - if (r->points2) free(r->points2); - if (r->scanline) free(r->scanline); - - free(r); -} - -static NSVGmemPage* nsvg__nextPage(NSVGrasterizer* r, NSVGmemPage* cur) -{ - NSVGmemPage *newp; - - // If using existing chain, return the next page in chain - if (cur != NULL && cur->next != NULL) { - return cur->next; - } - - // Alloc new page - newp = (NSVGmemPage*)malloc(sizeof(NSVGmemPage)); - if (newp == NULL) return NULL; - memset(newp, 0, sizeof(NSVGmemPage)); - - // Add to linked list - if (cur != NULL) - cur->next = newp; - else - r->pages = newp; - - return newp; -} - -static void nsvg__resetPool(NSVGrasterizer* r) -{ - NSVGmemPage* p = r->pages; - while (p != NULL) { - p->size = 0; - p = p->next; - } - r->curpage = r->pages; -} - -static unsigned char* nsvg__alloc(NSVGrasterizer* r, int size) -{ - unsigned char* buf; - if (size > NSVG__MEMPAGE_SIZE) return NULL; - if (r->curpage == NULL || r->curpage->size+size > NSVG__MEMPAGE_SIZE) { - r->curpage = nsvg__nextPage(r, r->curpage); - } - buf = &r->curpage->mem[r->curpage->size]; - r->curpage->size += size; - return buf; -} - -static int nsvg__ptEquals(float x1, float y1, float x2, float y2, float tol) -{ - float dx = x2 - x1; - float dy = y2 - y1; - return dx*dx + dy*dy < tol*tol; -} - -static void nsvg__addPathPoint(NSVGrasterizer* r, float x, float y, int flags) -{ - NSVGpoint* pt; - - if (r->npoints > 0) { - pt = &r->points[r->npoints-1]; - if (nsvg__ptEquals(pt->x,pt->y, x,y, r->distTol)) { - pt->flags = (unsigned char)(pt->flags | flags); - return; - } - } - - if (r->npoints+1 > r->cpoints) { - r->cpoints = r->cpoints > 0 ? r->cpoints * 2 : 64; - r->points = (NSVGpoint*)realloc(r->points, sizeof(NSVGpoint) * r->cpoints); - if (r->points == NULL) return; - } - - pt = &r->points[r->npoints]; - pt->x = x; - pt->y = y; - pt->flags = (unsigned char)flags; - r->npoints++; -} - -static void nsvg__appendPathPoint(NSVGrasterizer* r, NSVGpoint pt) -{ - if (r->npoints+1 > r->cpoints) { - r->cpoints = r->cpoints > 0 ? r->cpoints * 2 : 64; - r->points = (NSVGpoint*)realloc(r->points, sizeof(NSVGpoint) * r->cpoints); - if (r->points == NULL) return; - } - r->points[r->npoints] = pt; - r->npoints++; -} - -static void nsvg__duplicatePoints(NSVGrasterizer* r) -{ - if (r->npoints > r->cpoints2) { - r->cpoints2 = r->npoints; - r->points2 = (NSVGpoint*)realloc(r->points2, sizeof(NSVGpoint) * r->cpoints2); - if (r->points2 == NULL) return; - } - - memcpy(r->points2, r->points, sizeof(NSVGpoint) * r->npoints); - r->npoints2 = r->npoints; -} - -static void nsvg__addEdge(NSVGrasterizer* r, float x0, float y0, float x1, float y1) -{ - NSVGedge* e; - - // Skip horizontal edges - if (y0 == y1) - return; - - if (r->nedges+1 > r->cedges) { - r->cedges = r->cedges > 0 ? r->cedges * 2 : 64; - r->edges = (NSVGedge*)realloc(r->edges, sizeof(NSVGedge) * r->cedges); - if (r->edges == NULL) return; - } - - e = &r->edges[r->nedges]; - r->nedges++; - - if (y0 < y1) { - e->x0 = x0; - e->y0 = y0; - e->x1 = x1; - e->y1 = y1; - e->dir = 1; - } else { - e->x0 = x1; - e->y0 = y1; - e->x1 = x0; - e->y1 = y0; - e->dir = -1; - } -} - -static float nsvg__normalize(float *x, float* y) -{ - float d = sqrtf((*x)*(*x) + (*y)*(*y)); - if (d > 1e-6f) { - float id = 1.0f / d; - *x *= id; - *y *= id; - } - return d; -} - -static float nsvg__absf(float x) { return x < 0 ? -x : x; } - -static void nsvg__flattenCubicBez(NSVGrasterizer* r, - float x1, float y1, float x2, float y2, - float x3, float y3, float x4, float y4, - int level, int type) -{ - float x12,y12,x23,y23,x34,y34,x123,y123,x234,y234,x1234,y1234; - float dx,dy,d2,d3; - - if (level > 10) return; - - x12 = (x1+x2)*0.5f; - y12 = (y1+y2)*0.5f; - x23 = (x2+x3)*0.5f; - y23 = (y2+y3)*0.5f; - x34 = (x3+x4)*0.5f; - y34 = (y3+y4)*0.5f; - x123 = (x12+x23)*0.5f; - y123 = (y12+y23)*0.5f; - - dx = x4 - x1; - dy = y4 - y1; - d2 = nsvg__absf(((x2 - x4) * dy - (y2 - y4) * dx)); - d3 = nsvg__absf(((x3 - x4) * dy - (y3 - y4) * dx)); - - if ((d2 + d3)*(d2 + d3) < r->tessTol * (dx*dx + dy*dy)) { - nsvg__addPathPoint(r, x4, y4, type); - return; - } - - x234 = (x23+x34)*0.5f; - y234 = (y23+y34)*0.5f; - x1234 = (x123+x234)*0.5f; - y1234 = (y123+y234)*0.5f; - - nsvg__flattenCubicBez(r, x1,y1, x12,y12, x123,y123, x1234,y1234, level+1, 0); - nsvg__flattenCubicBez(r, x1234,y1234, x234,y234, x34,y34, x4,y4, level+1, type); -} - -static void nsvg__flattenShape(NSVGrasterizer* r, NSVGshape* shape, float scale) -{ - int i, j; - NSVGpath* path; - - for (path = shape->paths; path != NULL; path = path->next) { - r->npoints = 0; - // Flatten path - nsvg__addPathPoint(r, path->pts[0]*scale, path->pts[1]*scale, 0); - for (i = 0; i < path->npts-1; i += 3) { - float* p = &path->pts[i*2]; - nsvg__flattenCubicBez(r, p[0]*scale,p[1]*scale, p[2]*scale,p[3]*scale, p[4]*scale,p[5]*scale, p[6]*scale,p[7]*scale, 0, 0); - } - // Close path - nsvg__addPathPoint(r, path->pts[0]*scale, path->pts[1]*scale, 0); - // Build edges - for (i = 0, j = r->npoints-1; i < r->npoints; j = i++) - nsvg__addEdge(r, r->points[j].x, r->points[j].y, r->points[i].x, r->points[i].y); - } -} - -enum NSVGpointFlags -{ - NSVG_PT_CORNER = 0x01, - NSVG_PT_BEVEL = 0x02, - NSVG_PT_LEFT = 0x04 -}; - -static void nsvg__initClosed(NSVGpoint* left, NSVGpoint* right, NSVGpoint* p0, NSVGpoint* p1, float lineWidth) -{ - float w = lineWidth * 0.5f; - float dx = p1->x - p0->x; - float dy = p1->y - p0->y; - float len = nsvg__normalize(&dx, &dy); - float px = p0->x + dx*len*0.5f, py = p0->y + dy*len*0.5f; - float dlx = dy, dly = -dx; - float lx = px - dlx*w, ly = py - dly*w; - float rx = px + dlx*w, ry = py + dly*w; - left->x = lx; left->y = ly; - right->x = rx; right->y = ry; -} - -static void nsvg__buttCap(NSVGrasterizer* r, NSVGpoint* left, NSVGpoint* right, NSVGpoint* p, float dx, float dy, float lineWidth, int connect) -{ - float w = lineWidth * 0.5f; - float px = p->x, py = p->y; - float dlx = dy, dly = -dx; - float lx = px - dlx*w, ly = py - dly*w; - float rx = px + dlx*w, ry = py + dly*w; - - nsvg__addEdge(r, lx, ly, rx, ry); - - if (connect) { - nsvg__addEdge(r, left->x, left->y, lx, ly); - nsvg__addEdge(r, rx, ry, right->x, right->y); - } - left->x = lx; left->y = ly; - right->x = rx; right->y = ry; -} - -static void nsvg__squareCap(NSVGrasterizer* r, NSVGpoint* left, NSVGpoint* right, NSVGpoint* p, float dx, float dy, float lineWidth, int connect) -{ - float w = lineWidth * 0.5f; - float px = p->x - dx*w, py = p->y - dy*w; - float dlx = dy, dly = -dx; - float lx = px - dlx*w, ly = py - dly*w; - float rx = px + dlx*w, ry = py + dly*w; - - nsvg__addEdge(r, lx, ly, rx, ry); - - if (connect) { - nsvg__addEdge(r, left->x, left->y, lx, ly); - nsvg__addEdge(r, rx, ry, right->x, right->y); - } - left->x = lx; left->y = ly; - right->x = rx; right->y = ry; -} - -#ifndef NSVG_PI -#define NSVG_PI (3.14159265358979323846264338327f) -#endif - -static void nsvg__roundCap(NSVGrasterizer* r, NSVGpoint* left, NSVGpoint* right, NSVGpoint* p, float dx, float dy, float lineWidth, int ncap, int connect) -{ - int i; - float w = lineWidth * 0.5f; - float px = p->x, py = p->y; - float dlx = dy, dly = -dx; - float lx = 0, ly = 0, rx = 0, ry = 0, prevx = 0, prevy = 0; - - for (i = 0; i < ncap; i++) { - float a = (float)i/(float)(ncap-1)*NSVG_PI; - float ax = cosf(a) * w, ay = sinf(a) * w; - float x = px - dlx*ax - dx*ay; - float y = py - dly*ax - dy*ay; - - if (i > 0) - nsvg__addEdge(r, prevx, prevy, x, y); - - prevx = x; - prevy = y; - - if (i == 0) { - lx = x; ly = y; - } else if (i == ncap-1) { - rx = x; ry = y; - } - } - - if (connect) { - nsvg__addEdge(r, left->x, left->y, lx, ly); - nsvg__addEdge(r, rx, ry, right->x, right->y); - } - - left->x = lx; left->y = ly; - right->x = rx; right->y = ry; -} - -static void nsvg__bevelJoin(NSVGrasterizer* r, NSVGpoint* left, NSVGpoint* right, NSVGpoint* p0, NSVGpoint* p1, float lineWidth) -{ - float w = lineWidth * 0.5f; - float dlx0 = p0->dy, dly0 = -p0->dx; - float dlx1 = p1->dy, dly1 = -p1->dx; - float lx0 = p1->x - (dlx0 * w), ly0 = p1->y - (dly0 * w); - float rx0 = p1->x + (dlx0 * w), ry0 = p1->y + (dly0 * w); - float lx1 = p1->x - (dlx1 * w), ly1 = p1->y - (dly1 * w); - float rx1 = p1->x + (dlx1 * w), ry1 = p1->y + (dly1 * w); - - nsvg__addEdge(r, lx0, ly0, left->x, left->y); - nsvg__addEdge(r, lx1, ly1, lx0, ly0); - - nsvg__addEdge(r, right->x, right->y, rx0, ry0); - nsvg__addEdge(r, rx0, ry0, rx1, ry1); - - left->x = lx1; left->y = ly1; - right->x = rx1; right->y = ry1; -} - -static void nsvg__miterJoin(NSVGrasterizer* r, NSVGpoint* left, NSVGpoint* right, NSVGpoint* p0, NSVGpoint* p1, float lineWidth) -{ - float w = lineWidth * 0.5f; - float dlx0 = p0->dy, dly0 = -p0->dx; - float dlx1 = p1->dy, dly1 = -p1->dx; - float lx0, rx0, lx1, rx1; - float ly0, ry0, ly1, ry1; - - if (p1->flags & NSVG_PT_LEFT) { - lx0 = lx1 = p1->x - p1->dmx * w; - ly0 = ly1 = p1->y - p1->dmy * w; - nsvg__addEdge(r, lx1, ly1, left->x, left->y); - - rx0 = p1->x + (dlx0 * w); - ry0 = p1->y + (dly0 * w); - rx1 = p1->x + (dlx1 * w); - ry1 = p1->y + (dly1 * w); - nsvg__addEdge(r, right->x, right->y, rx0, ry0); - nsvg__addEdge(r, rx0, ry0, rx1, ry1); - } else { - lx0 = p1->x - (dlx0 * w); - ly0 = p1->y - (dly0 * w); - lx1 = p1->x - (dlx1 * w); - ly1 = p1->y - (dly1 * w); - nsvg__addEdge(r, lx0, ly0, left->x, left->y); - nsvg__addEdge(r, lx1, ly1, lx0, ly0); - - rx0 = rx1 = p1->x + p1->dmx * w; - ry0 = ry1 = p1->y + p1->dmy * w; - nsvg__addEdge(r, right->x, right->y, rx1, ry1); - } - - left->x = lx1; left->y = ly1; - right->x = rx1; right->y = ry1; -} - -static void nsvg__roundJoin(NSVGrasterizer* r, NSVGpoint* left, NSVGpoint* right, NSVGpoint* p0, NSVGpoint* p1, float lineWidth, int ncap) -{ - int i, n; - float w = lineWidth * 0.5f; - float dlx0 = p0->dy, dly0 = -p0->dx; - float dlx1 = p1->dy, dly1 = -p1->dx; - float a0 = atan2f(dly0, dlx0); - float a1 = atan2f(dly1, dlx1); - float da = a1 - a0; - float lx, ly, rx, ry; - - if (da < NSVG_PI) da += NSVG_PI*2; - if (da > NSVG_PI) da -= NSVG_PI*2; - - n = (int)ceilf((nsvg__absf(da) / NSVG_PI) * (float)ncap); - if (n < 2) n = 2; - if (n > ncap) n = ncap; - - lx = left->x; - ly = left->y; - rx = right->x; - ry = right->y; - - for (i = 0; i < n; i++) { - float u = (float)i/(float)(n-1); - float a = a0 + u*da; - float ax = cosf(a) * w, ay = sinf(a) * w; - float lx1 = p1->x - ax, ly1 = p1->y - ay; - float rx1 = p1->x + ax, ry1 = p1->y + ay; - - nsvg__addEdge(r, lx1, ly1, lx, ly); - nsvg__addEdge(r, rx, ry, rx1, ry1); - - lx = lx1; ly = ly1; - rx = rx1; ry = ry1; - } - - left->x = lx; left->y = ly; - right->x = rx; right->y = ry; -} - -static void nsvg__straightJoin(NSVGrasterizer* r, NSVGpoint* left, NSVGpoint* right, NSVGpoint* p1, float lineWidth) -{ - float w = lineWidth * 0.5f; - float lx = p1->x - (p1->dmx * w), ly = p1->y - (p1->dmy * w); - float rx = p1->x + (p1->dmx * w), ry = p1->y + (p1->dmy * w); - - nsvg__addEdge(r, lx, ly, left->x, left->y); - nsvg__addEdge(r, right->x, right->y, rx, ry); - - left->x = lx; left->y = ly; - right->x = rx; right->y = ry; -} - -static int nsvg__curveDivs(float r, float arc, float tol) -{ - float da = acosf(r / (r + tol)) * 2.0f; - int divs = (int)ceilf(arc / da); - if (divs < 2) divs = 2; - return divs; -} - -static void nsvg__expandStroke(NSVGrasterizer* r, NSVGpoint* points, int npoints, int closed, int lineJoin, int lineCap, float lineWidth) -{ - int ncap = nsvg__curveDivs(lineWidth*0.5f, NSVG_PI, r->tessTol); // Calculate divisions per half circle. - NSVGpoint left = {0,0,0,0,0,0,0,0}, right = {0,0,0,0,0,0,0,0}, firstLeft = {0,0,0,0,0,0,0,0}, firstRight = {0,0,0,0,0,0,0,0}; - NSVGpoint* p0, *p1; - int j, s, e; - - // Build stroke edges - if (closed) { - // Looping - p0 = &points[npoints-1]; - p1 = &points[0]; - s = 0; - e = npoints; - } else { - // Add cap - p0 = &points[0]; - p1 = &points[1]; - s = 1; - e = npoints-1; - } - - if (closed) { - nsvg__initClosed(&left, &right, p0, p1, lineWidth); - firstLeft = left; - firstRight = right; - } else { - // Add cap - float dx = p1->x - p0->x; - float dy = p1->y - p0->y; - nsvg__normalize(&dx, &dy); - if (lineCap == NSVG_CAP_BUTT) - nsvg__buttCap(r, &left, &right, p0, dx, dy, lineWidth, 0); - else if (lineCap == NSVG_CAP_SQUARE) - nsvg__squareCap(r, &left, &right, p0, dx, dy, lineWidth, 0); - else if (lineCap == NSVG_CAP_ROUND) - nsvg__roundCap(r, &left, &right, p0, dx, dy, lineWidth, ncap, 0); - } - - for (j = s; j < e; ++j) { - if (p1->flags & NSVG_PT_CORNER) { - if (lineJoin == NSVG_JOIN_ROUND) - nsvg__roundJoin(r, &left, &right, p0, p1, lineWidth, ncap); - else if (lineJoin == NSVG_JOIN_BEVEL || (p1->flags & NSVG_PT_BEVEL)) - nsvg__bevelJoin(r, &left, &right, p0, p1, lineWidth); - else - nsvg__miterJoin(r, &left, &right, p0, p1, lineWidth); - } else { - nsvg__straightJoin(r, &left, &right, p1, lineWidth); - } - p0 = p1++; - } - - if (closed) { - // Loop it - nsvg__addEdge(r, firstLeft.x, firstLeft.y, left.x, left.y); - nsvg__addEdge(r, right.x, right.y, firstRight.x, firstRight.y); - } else { - // Add cap - float dx = p1->x - p0->x; - float dy = p1->y - p0->y; - nsvg__normalize(&dx, &dy); - if (lineCap == NSVG_CAP_BUTT) - nsvg__buttCap(r, &right, &left, p1, -dx, -dy, lineWidth, 1); - else if (lineCap == NSVG_CAP_SQUARE) - nsvg__squareCap(r, &right, &left, p1, -dx, -dy, lineWidth, 1); - else if (lineCap == NSVG_CAP_ROUND) - nsvg__roundCap(r, &right, &left, p1, -dx, -dy, lineWidth, ncap, 1); - } -} - -static void nsvg__prepareStroke(NSVGrasterizer* r, float miterLimit, int lineJoin) -{ - int i, j; - NSVGpoint* p0, *p1; - - p0 = &r->points[r->npoints-1]; - p1 = &r->points[0]; - for (i = 0; i < r->npoints; i++) { - // Calculate segment direction and length - p0->dx = p1->x - p0->x; - p0->dy = p1->y - p0->y; - p0->len = nsvg__normalize(&p0->dx, &p0->dy); - // Advance - p0 = p1++; - } - - // calculate joins - p0 = &r->points[r->npoints-1]; - p1 = &r->points[0]; - for (j = 0; j < r->npoints; j++) { - float dlx0, dly0, dlx1, dly1, dmr2, cross; - dlx0 = p0->dy; - dly0 = -p0->dx; - dlx1 = p1->dy; - dly1 = -p1->dx; - // Calculate extrusions - p1->dmx = (dlx0 + dlx1) * 0.5f; - p1->dmy = (dly0 + dly1) * 0.5f; - dmr2 = p1->dmx*p1->dmx + p1->dmy*p1->dmy; - if (dmr2 > 0.000001f) { - float s2 = 1.0f / dmr2; - if (s2 > 600.0f) { - s2 = 600.0f; - } - p1->dmx *= s2; - p1->dmy *= s2; - } - - // Clear flags, but keep the corner. - p1->flags = (p1->flags & NSVG_PT_CORNER) ? NSVG_PT_CORNER : 0; - - // Keep track of left turns. - cross = p1->dx * p0->dy - p0->dx * p1->dy; - if (cross > 0.0f) - p1->flags |= NSVG_PT_LEFT; - - // Check to see if the corner needs to be beveled. - if (p1->flags & NSVG_PT_CORNER) { - if ((dmr2 * miterLimit*miterLimit) < 1.0f || lineJoin == NSVG_JOIN_BEVEL || lineJoin == NSVG_JOIN_ROUND) { - p1->flags |= NSVG_PT_BEVEL; - } - } - - p0 = p1++; - } -} - -static void nsvg__flattenShapeStroke(NSVGrasterizer* r, NSVGshape* shape, float scale) -{ - int i, j, closed; - NSVGpath* path; - NSVGpoint* p0, *p1; - float miterLimit = shape->miterLimit; - int lineJoin = shape->strokeLineJoin; - int lineCap = shape->strokeLineCap; - float lineWidth = shape->strokeWidth * scale; - - for (path = shape->paths; path != NULL; path = path->next) { - // Flatten path - r->npoints = 0; - nsvg__addPathPoint(r, path->pts[0]*scale, path->pts[1]*scale, NSVG_PT_CORNER); - for (i = 0; i < path->npts-1; i += 3) { - float* p = &path->pts[i*2]; - nsvg__flattenCubicBez(r, p[0]*scale,p[1]*scale, p[2]*scale,p[3]*scale, p[4]*scale,p[5]*scale, p[6]*scale,p[7]*scale, 0, NSVG_PT_CORNER); - } - if (r->npoints < 2) - continue; - - closed = path->closed; - - // If the first and last points are the same, remove the last, mark as closed path. - p0 = &r->points[r->npoints-1]; - p1 = &r->points[0]; - if (nsvg__ptEquals(p0->x,p0->y, p1->x,p1->y, r->distTol)) { - r->npoints--; - p0 = &r->points[r->npoints-1]; - closed = 1; - } - - if (shape->strokeDashCount > 0) { - int idash = 0, dashState = 1; - float totalDist = 0, dashLen, allDashLen, dashOffset; - NSVGpoint cur; - - if (closed) - nsvg__appendPathPoint(r, r->points[0]); - - // Duplicate points -> points2. - nsvg__duplicatePoints(r); - - r->npoints = 0; - cur = r->points2[0]; - nsvg__appendPathPoint(r, cur); - - // Figure out dash offset. - allDashLen = 0; - for (j = 0; j < shape->strokeDashCount; j++) - allDashLen += shape->strokeDashArray[j]; - if (shape->strokeDashCount & 1) - allDashLen *= 2.0f; - // Find location inside pattern - dashOffset = fmodf(shape->strokeDashOffset, allDashLen); - if (dashOffset < 0.0f) - dashOffset += allDashLen; - - while (dashOffset > shape->strokeDashArray[idash]) { - dashOffset -= shape->strokeDashArray[idash]; - idash = (idash + 1) % shape->strokeDashCount; - } - dashLen = (shape->strokeDashArray[idash] - dashOffset) * scale; - - for (j = 1; j < r->npoints2; ) { - float dx = r->points2[j].x - cur.x; - float dy = r->points2[j].y - cur.y; - float dist = sqrtf(dx*dx + dy*dy); - - if ((totalDist + dist) > dashLen) { - // Calculate intermediate point - float d = (dashLen - totalDist) / dist; - float x = cur.x + dx * d; - float y = cur.y + dy * d; - nsvg__addPathPoint(r, x, y, NSVG_PT_CORNER); - - // Stroke - if (r->npoints > 1 && dashState) { - nsvg__prepareStroke(r, miterLimit, lineJoin); - nsvg__expandStroke(r, r->points, r->npoints, 0, lineJoin, lineCap, lineWidth); - } - // Advance dash pattern - dashState = !dashState; - idash = (idash+1) % shape->strokeDashCount; - dashLen = shape->strokeDashArray[idash] * scale; - // Restart - cur.x = x; - cur.y = y; - cur.flags = NSVG_PT_CORNER; - totalDist = 0.0f; - r->npoints = 0; - nsvg__appendPathPoint(r, cur); - } else { - totalDist += dist; - cur = r->points2[j]; - nsvg__appendPathPoint(r, cur); - j++; - } - } - // Stroke any leftover path - if (r->npoints > 1 && dashState) - nsvg__expandStroke(r, r->points, r->npoints, 0, lineJoin, lineCap, lineWidth); - } else { - nsvg__prepareStroke(r, miterLimit, lineJoin); - nsvg__expandStroke(r, r->points, r->npoints, closed, lineJoin, lineCap, lineWidth); - } - } -} - -static int nsvg__cmpEdge(const void *p, const void *q) -{ - const NSVGedge* a = (const NSVGedge*)p; - const NSVGedge* b = (const NSVGedge*)q; - - if (a->y0 < b->y0) return -1; - if (a->y0 > b->y0) return 1; - return 0; -} - - -static NSVGactiveEdge* nsvg__addActive(NSVGrasterizer* r, NSVGedge* e, float startPoint) -{ - NSVGactiveEdge* z; - - if (r->freelist != NULL) { - // Restore from freelist. - z = r->freelist; - r->freelist = z->next; - } else { - // Alloc new edge. - z = (NSVGactiveEdge*)nsvg__alloc(r, sizeof(NSVGactiveEdge)); - if (z == NULL) return NULL; - } - - float dxdy = (e->x1 - e->x0) / (e->y1 - e->y0); -// STBTT_assert(e->y0 <= start_point); - // round dx down to avoid going too far - if (dxdy < 0) - z->dx = (int)(-floorf(NSVG__FIX * -dxdy)); - else - z->dx = (int)floorf(NSVG__FIX * dxdy); - z->x = (int)floorf(NSVG__FIX * (e->x0 + dxdy * (startPoint - e->y0))); -// z->x -= off_x * FIX; - z->ey = e->y1; - z->next = 0; - z->dir = e->dir; - - return z; -} - -static void nsvg__freeActive(NSVGrasterizer* r, NSVGactiveEdge* z) -{ - z->next = r->freelist; - r->freelist = z; -} - -static void nsvg__fillScanline(unsigned char* scanline, int len, int x0, int x1, int maxWeight, int* xmin, int* xmax) -{ - int i = x0 >> NSVG__FIXSHIFT; - int j = x1 >> NSVG__FIXSHIFT; - if (i < *xmin) *xmin = i; - if (j > *xmax) *xmax = j; - if (i < len && j >= 0) { - if (i == j) { - // x0,x1 are the same pixel, so compute combined coverage - scanline[i] = (unsigned char)(scanline[i] + ((x1 - x0) * maxWeight >> NSVG__FIXSHIFT)); - } else { - if (i >= 0) // add antialiasing for x0 - scanline[i] = (unsigned char)(scanline[i] + (((NSVG__FIX - (x0 & NSVG__FIXMASK)) * maxWeight) >> NSVG__FIXSHIFT)); - else - i = -1; // clip - - if (j < len) // add antialiasing for x1 - scanline[j] = (unsigned char)(scanline[j] + (((x1 & NSVG__FIXMASK) * maxWeight) >> NSVG__FIXSHIFT)); - else - j = len; // clip - - for (++i; i < j; ++i) // fill pixels between x0 and x1 - scanline[i] = (unsigned char)(scanline[i] + maxWeight); - } - } -} - -// note: this routine clips fills that extend off the edges... ideally this -// wouldn't happen, but it could happen if the truetype glyph bounding boxes -// are wrong, or if the user supplies a too-small bitmap -static void nsvg__fillActiveEdges(unsigned char* scanline, int len, NSVGactiveEdge* e, int maxWeight, int* xmin, int* xmax, char fillRule) -{ - // non-zero winding fill - int x0 = 0, w = 0; - - if (fillRule == NSVG_FILLRULE_NONZERO) { - // Non-zero - while (e != NULL) { - if (w == 0) { - // if we're currently at zero, we need to record the edge start point - x0 = e->x; w += e->dir; - } else { - int x1 = e->x; w += e->dir; - // if we went to zero, we need to draw - if (w == 0) - nsvg__fillScanline(scanline, len, x0, x1, maxWeight, xmin, xmax); - } - e = e->next; - } - } else if (fillRule == NSVG_FILLRULE_EVENODD) { - // Even-odd - while (e != NULL) { - if (w == 0) { - // if we're currently at zero, we need to record the edge start point - x0 = e->x; w = 1; - } else { - int x1 = e->x; w = 0; - nsvg__fillScanline(scanline, len, x0, x1, maxWeight, xmin, xmax); - } - e = e->next; - } - } -} - -static float nsvg__clampf(float a, float mn, float mx) { return a < mn ? mn : (a > mx ? mx : a); } - -static unsigned int nsvg__RGBA(unsigned char r, unsigned char g, unsigned char b, unsigned char a) -{ - return (r) | (g << 8) | (b << 16) | (a << 24); -} - -static unsigned int nsvg__lerpRGBA(unsigned int c0, unsigned int c1, float u) -{ - int iu = (int)(nsvg__clampf(u, 0.0f, 1.0f) * 256.0f); - int r = (((c0) & 0xff)*(256-iu) + (((c1) & 0xff)*iu)) >> 8; - int g = (((c0>>8) & 0xff)*(256-iu) + (((c1>>8) & 0xff)*iu)) >> 8; - int b = (((c0>>16) & 0xff)*(256-iu) + (((c1>>16) & 0xff)*iu)) >> 8; - int a = (((c0>>24) & 0xff)*(256-iu) + (((c1>>24) & 0xff)*iu)) >> 8; - return nsvg__RGBA((unsigned char)r, (unsigned char)g, (unsigned char)b, (unsigned char)a); -} - -static unsigned int nsvg__applyOpacity(unsigned int c, float u) -{ - int iu = (int)(nsvg__clampf(u, 0.0f, 1.0f) * 256.0f); - int r = (c) & 0xff; - int g = (c>>8) & 0xff; - int b = (c>>16) & 0xff; - int a = (((c>>24) & 0xff)*iu) >> 8; - return nsvg__RGBA((unsigned char)r, (unsigned char)g, (unsigned char)b, (unsigned char)a); -} - -static inline int nsvg__div255(int x) -{ - return ((x+1) * 257) >> 16; -} - -static void nsvg__scanlineSolid(unsigned char* dst, int count, unsigned char* cover, int x, int y, - float tx, float ty, float scale, NSVGcachedPaint* cache) -{ - - if (cache->type == NSVG_PAINT_COLOR) { - int i, cr, cg, cb, ca; - cr = cache->colors[0] & 0xff; - cg = (cache->colors[0] >> 8) & 0xff; - cb = (cache->colors[0] >> 16) & 0xff; - ca = (cache->colors[0] >> 24) & 0xff; - - for (i = 0; i < count; i++) { - int r,g,b; - int a = nsvg__div255((int)cover[0] * ca); - int ia = 255 - a; - // Premultiply - r = nsvg__div255(cr * a); - g = nsvg__div255(cg * a); - b = nsvg__div255(cb * a); - - // Blend over - r += nsvg__div255(ia * (int)dst[0]); - g += nsvg__div255(ia * (int)dst[1]); - b += nsvg__div255(ia * (int)dst[2]); - a += nsvg__div255(ia * (int)dst[3]); - - dst[0] = (unsigned char)r; - dst[1] = (unsigned char)g; - dst[2] = (unsigned char)b; - dst[3] = (unsigned char)a; - - cover++; - dst += 4; - } - } else if (cache->type == NSVG_PAINT_LINEAR_GRADIENT) { - // TODO: spread modes. - // TODO: plenty of opportunities to optimize. - float fx, fy, dx, gy; - float* t = cache->xform; - int i, cr, cg, cb, ca; - unsigned int c; - - fx = ((float)x - tx) / scale; - fy = ((float)y - ty) / scale; - dx = 1.0f / scale; - - for (i = 0; i < count; i++) { - int r,g,b,a,ia; - gy = fx*t[1] + fy*t[3] + t[5]; - c = cache->colors[(int)nsvg__clampf(gy*255.0f, 0, 255.0f)]; - cr = (c) & 0xff; - cg = (c >> 8) & 0xff; - cb = (c >> 16) & 0xff; - ca = (c >> 24) & 0xff; - - a = nsvg__div255((int)cover[0] * ca); - ia = 255 - a; - - // Premultiply - r = nsvg__div255(cr * a); - g = nsvg__div255(cg * a); - b = nsvg__div255(cb * a); - - // Blend over - r += nsvg__div255(ia * (int)dst[0]); - g += nsvg__div255(ia * (int)dst[1]); - b += nsvg__div255(ia * (int)dst[2]); - a += nsvg__div255(ia * (int)dst[3]); - - dst[0] = (unsigned char)r; - dst[1] = (unsigned char)g; - dst[2] = (unsigned char)b; - dst[3] = (unsigned char)a; - - cover++; - dst += 4; - fx += dx; - } - } else if (cache->type == NSVG_PAINT_RADIAL_GRADIENT) { - // TODO: spread modes. - // TODO: plenty of opportunities to optimize. - // TODO: focus (fx,fy) - float fx, fy, dx, gx, gy, gd; - float* t = cache->xform; - int i, cr, cg, cb, ca; - unsigned int c; - - fx = ((float)x - tx) / scale; - fy = ((float)y - ty) / scale; - dx = 1.0f / scale; - - for (i = 0; i < count; i++) { - int r,g,b,a,ia; - gx = fx*t[0] + fy*t[2] + t[4]; - gy = fx*t[1] + fy*t[3] + t[5]; - gd = sqrtf(gx*gx + gy*gy); - c = cache->colors[(int)nsvg__clampf(gd*255.0f, 0, 255.0f)]; - cr = (c) & 0xff; - cg = (c >> 8) & 0xff; - cb = (c >> 16) & 0xff; - ca = (c >> 24) & 0xff; - - a = nsvg__div255((int)cover[0] * ca); - ia = 255 - a; - - // Premultiply - r = nsvg__div255(cr * a); - g = nsvg__div255(cg * a); - b = nsvg__div255(cb * a); - - // Blend over - r += nsvg__div255(ia * (int)dst[0]); - g += nsvg__div255(ia * (int)dst[1]); - b += nsvg__div255(ia * (int)dst[2]); - a += nsvg__div255(ia * (int)dst[3]); - - dst[0] = (unsigned char)r; - dst[1] = (unsigned char)g; - dst[2] = (unsigned char)b; - dst[3] = (unsigned char)a; - - cover++; - dst += 4; - fx += dx; - } - } -} - -static void nsvg__rasterizeSortedEdges(NSVGrasterizer *r, float tx, float ty, float scale, NSVGcachedPaint* cache, char fillRule) -{ - NSVGactiveEdge *active = NULL; - int y, s; - int e = 0; - int maxWeight = (255 / NSVG__SUBSAMPLES); // weight per vertical scanline - int xmin, xmax; - - for (y = 0; y < r->height; y++) { - memset(r->scanline, 0, r->width); - xmin = r->width; - xmax = 0; - for (s = 0; s < NSVG__SUBSAMPLES; ++s) { - // find center of pixel for this scanline - float scany = (float)(y*NSVG__SUBSAMPLES + s) + 0.5f; - NSVGactiveEdge **step = &active; - - // update all active edges; - // remove all active edges that terminate before the center of this scanline - while (*step) { - NSVGactiveEdge *z = *step; - if (z->ey <= scany) { - *step = z->next; // delete from list -// NSVG__assert(z->valid); - nsvg__freeActive(r, z); - } else { - z->x += z->dx; // advance to position for current scanline - step = &((*step)->next); // advance through list - } - } - - // resort the list if needed - for (;;) { - int changed = 0; - step = &active; - while (*step && (*step)->next) { - if ((*step)->x > (*step)->next->x) { - NSVGactiveEdge* t = *step; - NSVGactiveEdge* q = t->next; - t->next = q->next; - q->next = t; - *step = q; - changed = 1; - } - step = &(*step)->next; - } - if (!changed) break; - } - - // insert all edges that start before the center of this scanline -- omit ones that also end on this scanline - while (e < r->nedges && r->edges[e].y0 <= scany) { - if (r->edges[e].y1 > scany) { - NSVGactiveEdge* z = nsvg__addActive(r, &r->edges[e], scany); - if (z == NULL) break; - // find insertion point - if (active == NULL) { - active = z; - } else if (z->x < active->x) { - // insert at front - z->next = active; - active = z; - } else { - // find thing to insert AFTER - NSVGactiveEdge* p = active; - while (p->next && p->next->x < z->x) - p = p->next; - // at this point, p->next->x is NOT < z->x - z->next = p->next; - p->next = z; - } - } - e++; - } - - // now process all active edges in non-zero fashion - if (active != NULL) - nsvg__fillActiveEdges(r->scanline, r->width, active, maxWeight, &xmin, &xmax, fillRule); - } - // Blit - if (xmin < 0) xmin = 0; - if (xmax > r->width-1) xmax = r->width-1; - if (xmin <= xmax) { - nsvg__scanlineSolid(&r->bitmap[y * r->stride] + xmin*4, xmax-xmin+1, &r->scanline[xmin], xmin, y, tx,ty, scale, cache); - } - } - -} - -static void nsvg__unpremultiplyAlpha(unsigned char* image, int w, int h, int stride) -{ - int x,y; - - // Unpremultiply - for (y = 0; y < h; y++) { - unsigned char *row = &image[y*stride]; - for (x = 0; x < w; x++) { - int r = row[0], g = row[1], b = row[2], a = row[3]; - if (a != 0) { - row[0] = (unsigned char)(r*255/a); - row[1] = (unsigned char)(g*255/a); - row[2] = (unsigned char)(b*255/a); - } - row += 4; - } - } - - // Defringe - for (y = 0; y < h; y++) { - unsigned char *row = &image[y*stride]; - for (x = 0; x < w; x++) { - int r = 0, g = 0, b = 0, a = row[3], n = 0; - if (a == 0) { - if (x-1 > 0 && row[-1] != 0) { - r += row[-4]; - g += row[-3]; - b += row[-2]; - n++; - } - if (x+1 < w && row[7] != 0) { - r += row[4]; - g += row[5]; - b += row[6]; - n++; - } - if (y-1 > 0 && row[-stride+3] != 0) { - r += row[-stride]; - g += row[-stride+1]; - b += row[-stride+2]; - n++; - } - if (y+1 < h && row[stride+3] != 0) { - r += row[stride]; - g += row[stride+1]; - b += row[stride+2]; - n++; - } - if (n > 0) { - row[0] = (unsigned char)(r/n); - row[1] = (unsigned char)(g/n); - row[2] = (unsigned char)(b/n); - } - } - row += 4; - } - } -} - - -static void nsvg__initPaint(NSVGcachedPaint* cache, NSVGpaint* paint, float opacity) -{ - int i, j; - NSVGgradient* grad; - - cache->type = paint->type; - - if (paint->type == NSVG_PAINT_COLOR) { - cache->colors[0] = nsvg__applyOpacity(paint->color, opacity); - return; - } - - grad = paint->gradient; - - cache->spread = grad->spread; - memcpy(cache->xform, grad->xform, sizeof(float)*6); - - if (grad->nstops == 0) { - for (i = 0; i < 256; i++) - cache->colors[i] = 0; - } if (grad->nstops == 1) { - for (i = 0; i < 256; i++) - cache->colors[i] = nsvg__applyOpacity(grad->stops[i].color, opacity); - } else { - unsigned int ca, cb = 0; - float ua, ub, du, u; - int ia, ib, count; - - ca = nsvg__applyOpacity(grad->stops[0].color, opacity); - ua = nsvg__clampf(grad->stops[0].offset, 0, 1); - ub = nsvg__clampf(grad->stops[grad->nstops-1].offset, ua, 1); - ia = (int)(ua * 255.0f); - ib = (int)(ub * 255.0f); - for (i = 0; i < ia; i++) { - cache->colors[i] = ca; - } - - for (i = 0; i < grad->nstops-1; i++) { - ca = nsvg__applyOpacity(grad->stops[i].color, opacity); - cb = nsvg__applyOpacity(grad->stops[i+1].color, opacity); - ua = nsvg__clampf(grad->stops[i].offset, 0, 1); - ub = nsvg__clampf(grad->stops[i+1].offset, 0, 1); - ia = (int)(ua * 255.0f); - ib = (int)(ub * 255.0f); - count = ib - ia; - if (count <= 0) continue; - u = 0; - du = 1.0f / (float)count; - for (j = 0; j < count; j++) { - cache->colors[ia+j] = nsvg__lerpRGBA(ca,cb,u); - u += du; - } - } - - for (i = ib; i < 256; i++) - cache->colors[i] = cb; - } - -} - -/* -static void dumpEdges(NSVGrasterizer* r, const char* name) -{ - float xmin = 0, xmax = 0, ymin = 0, ymax = 0; - NSVGedge *e = NULL; - int i; - if (r->nedges == 0) return; - FILE* fp = fopen(name, "w"); - if (fp == NULL) return; - - xmin = xmax = r->edges[0].x0; - ymin = ymax = r->edges[0].y0; - for (i = 0; i < r->nedges; i++) { - e = &r->edges[i]; - xmin = nsvg__minf(xmin, e->x0); - xmin = nsvg__minf(xmin, e->x1); - xmax = nsvg__maxf(xmax, e->x0); - xmax = nsvg__maxf(xmax, e->x1); - ymin = nsvg__minf(ymin, e->y0); - ymin = nsvg__minf(ymin, e->y1); - ymax = nsvg__maxf(ymax, e->y0); - ymax = nsvg__maxf(ymax, e->y1); - } - - fprintf(fp, "", xmin, ymin, (xmax - xmin), (ymax - ymin)); - - for (i = 0; i < r->nedges; i++) { - e = &r->edges[i]; - fprintf(fp ,"", e->x0,e->y0, e->x1,e->y1); - } - - for (i = 0; i < r->npoints; i++) { - if (i+1 < r->npoints) - fprintf(fp ,"", r->points[i].x, r->points[i].y, r->points[i+1].x, r->points[i+1].y); - fprintf(fp ,"", r->points[i].x, r->points[i].y, r->points[i].flags == 0 ? "#f00" : "#0f0"); - } - - fprintf(fp, ""); - fclose(fp); -} -*/ - -void nsvgRasterize(NSVGrasterizer* r, - NSVGimage* image, float tx, float ty, float scale, - unsigned char* dst, int w, int h, int stride) -{ - NSVGshape *shape = NULL; - NSVGedge *e = NULL; - NSVGcachedPaint cache; - int i; - - r->bitmap = dst; - r->width = w; - r->height = h; - r->stride = stride; - - if (w > r->cscanline) { - r->cscanline = w; - r->scanline = (unsigned char*)realloc(r->scanline, w); - if (r->scanline == NULL) return; - } - - for (i = 0; i < h; i++) - memset(&dst[i*stride], 0, w*4); - - for (shape = image->shapes; shape != NULL; shape = shape->next) { - if (!(shape->flags & NSVG_FLAGS_VISIBLE)) - continue; - - if (shape->fill.type != NSVG_PAINT_NONE) { - nsvg__resetPool(r); - r->freelist = NULL; - r->nedges = 0; - - nsvg__flattenShape(r, shape, scale); - - // Scale and translate edges - for (i = 0; i < r->nedges; i++) { - e = &r->edges[i]; - e->x0 = tx + e->x0; - e->y0 = (ty + e->y0) * NSVG__SUBSAMPLES; - e->x1 = tx + e->x1; - e->y1 = (ty + e->y1) * NSVG__SUBSAMPLES; - } - - // Rasterize edges - qsort(r->edges, r->nedges, sizeof(NSVGedge), nsvg__cmpEdge); - - // now, traverse the scanlines and find the intersections on each scanline, use non-zero rule - nsvg__initPaint(&cache, &shape->fill, shape->opacity); - - nsvg__rasterizeSortedEdges(r, tx,ty,scale, &cache, shape->fillRule); - } - if (shape->stroke.type != NSVG_PAINT_NONE && (shape->strokeWidth * scale) > 0.01f) { - nsvg__resetPool(r); - r->freelist = NULL; - r->nedges = 0; - - nsvg__flattenShapeStroke(r, shape, scale); - -// dumpEdges(r, "edge.svg"); - - // Scale and translate edges - for (i = 0; i < r->nedges; i++) { - e = &r->edges[i]; - e->x0 = tx + e->x0; - e->y0 = (ty + e->y0) * NSVG__SUBSAMPLES; - e->x1 = tx + e->x1; - e->y1 = (ty + e->y1) * NSVG__SUBSAMPLES; - } - - // Rasterize edges - qsort(r->edges, r->nedges, sizeof(NSVGedge), nsvg__cmpEdge); - - // now, traverse the scanlines and find the intersections on each scanline, use non-zero rule - nsvg__initPaint(&cache, &shape->stroke, shape->opacity); - - nsvg__rasterizeSortedEdges(r, tx,ty,scale, &cache, NSVG_FILLRULE_NONZERO); - } - } - - nsvg__unpremultiplyAlpha(dst, w, h, stride); - - r->bitmap = NULL; - r->width = 0; - r->height = 0; - r->stride = 0; -} - -#endif diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt index ed994be18..430da0c34 100644 --- a/src/slic3r/CMakeLists.txt +++ b/src/slic3r/CMakeLists.txt @@ -85,6 +85,8 @@ set(SLIC3R_GUI_SOURCES GUI/GUI_App.hpp GUI/GUI_Utils.cpp GUI/GUI_Utils.hpp + GUI/GUI_Geometry.cpp + GUI/GUI_Geometry.hpp GUI/I18N.cpp GUI/I18N.hpp GUI/MainFrame.cpp @@ -129,6 +131,8 @@ set(SLIC3R_GUI_SOURCES GUI/2DBed.hpp GUI/3DBed.cpp GUI/3DBed.hpp + GUI/CoordAxes.cpp + GUI/CoordAxes.hpp GUI/Camera.cpp GUI/Camera.hpp GUI/wxExtensions.cpp @@ -172,6 +176,7 @@ set(SLIC3R_GUI_SOURCES GUI/Jobs/Worker.hpp GUI/Jobs/BoostThreadWorker.hpp GUI/Jobs/BoostThreadWorker.cpp + GUI/Jobs/UIThreadWorker.hpp GUI/Jobs/BusyCursorJob.hpp GUI/Jobs/PlaterWorker.hpp GUI/Jobs/ArrangeJob.hpp @@ -246,8 +251,12 @@ set(SLIC3R_GUI_SOURCES Utils/TCPConsole.hpp Utils/MKS.cpp Utils/MKS.hpp + Utils/WinRegistry.cpp + Utils/WinRegistry.hpp ) +find_package(NanoSVG REQUIRED) + if (APPLE) list(APPEND SLIC3R_GUI_SOURCES Utils/RetinaHelperImpl.mm @@ -272,7 +281,7 @@ endforeach() encoding_check(libslic3r_gui) -target_link_libraries(libslic3r_gui libslic3r avrdude libcereal imgui GLEW::GLEW OpenGL::GL hidapi libcurl ${wxWidgets_LIBRARIES}) +target_link_libraries(libslic3r_gui libslic3r avrdude libcereal imgui GLEW::GLEW OpenGL::GL hidapi libcurl ${wxWidgets_LIBRARIES} NanoSVG::nanosvg NanoSVG::nanosvgrast) if (MSVC) target_link_libraries(libslic3r_gui Setupapi.lib) diff --git a/src/slic3r/GUI/3DBed.cpp b/src/slic3r/GUI/3DBed.cpp index 1ce1af741..503c35a9b 100644 --- a/src/slic3r/GUI/3DBed.cpp +++ b/src/slic3r/GUI/3DBed.cpp @@ -102,6 +102,7 @@ const float* GeometryBuffer::get_vertices_data() const } #endif // !ENABLE_LEGACY_OPENGL_REMOVAL +#if !ENABLE_WORLD_COORDINATE const float Bed3D::Axes::DefaultStemRadius = 0.5f; const float Bed3D::Axes::DefaultStemLength = 25.0f; const float Bed3D::Axes::DefaultTipRadius = 2.5f * Bed3D::Axes::DefaultStemRadius; @@ -179,6 +180,7 @@ void Bed3D::Axes::render() glsafe(::glDisable(GL_DEPTH_TEST)); } +#endif // !ENABLE_WORLD_COORDINATE bool Bed3D::set_shape(const Pointfs& bed_shape, const double max_print_height, const std::string& custom_texture, const std::string& custom_model, bool force_as_custom) { @@ -341,7 +343,11 @@ BoundingBoxf3 Bed3D::calc_extended_bounding_box() const out.max.z() = 0.0; // extend to contain axes out.merge(m_axes.get_origin() + m_axes.get_total_length() * Vec3d::Ones()); +#if ENABLE_WORLD_COORDINATE + out.merge(out.min + Vec3d(-m_axes.get_tip_radius(), -m_axes.get_tip_radius(), out.max.z())); +#else out.merge(out.min + Vec3d(-Axes::DefaultTipRadius, -Axes::DefaultTipRadius, out.max.z())); +#endif // ENABLE_WORLD_COORDINATE // extend to contain model, if any BoundingBoxf3 model_bb = m_model.get_bounding_box(); if (model_bb.defined) { @@ -539,7 +545,15 @@ std::tuple Bed3D::detect_type(const Point void Bed3D::render_axes() { if (m_build_volume.valid()) +#if ENABLE_WORLD_COORDINATE +#if ENABLE_GL_SHADERS_ATTRIBUTES + m_axes.render(Transform3d::Identity(), 0.25f); +#else + m_axes.render(0.25f); +#endif // ENABLE_GL_SHADERS_ATTRIBUTES +#else m_axes.render(); +#endif // ENABLE_WORLD_COORDINATE } #if ENABLE_GL_SHADERS_ATTRIBUTES diff --git a/src/slic3r/GUI/3DBed.hpp b/src/slic3r/GUI/3DBed.hpp index 708d186a4..3c48a3901 100644 --- a/src/slic3r/GUI/3DBed.hpp +++ b/src/slic3r/GUI/3DBed.hpp @@ -3,7 +3,11 @@ #include "GLTexture.hpp" #include "3DScene.hpp" +#if ENABLE_WORLD_COORDINATE +#include "CoordAxes.hpp" +#else #include "GLModel.hpp" +#endif // ENABLE_WORLD_COORDINATE #include "libslic3r/BuildVolume.hpp" #if ENABLE_LEGACY_OPENGL_REMOVAL @@ -44,6 +48,7 @@ public: class Bed3D { +#if !ENABLE_WORLD_COORDINATE class Axes { public: @@ -67,6 +72,7 @@ class Bed3D float get_total_length() const { return m_stem_length + DefaultTipLength; } void render(); }; +#endif // !ENABLE_WORLD_COORDINATE public: enum class Type : unsigned char @@ -107,7 +113,11 @@ private: #if !ENABLE_LEGACY_OPENGL_REMOVAL unsigned int m_vbo_id{ 0 }; #endif // !ENABLE_LEGACY_OPENGL_REMOVAL +#if ENABLE_WORLD_COORDINATE + CoordAxes m_axes; +#else Axes m_axes; +#endif // ENABLE_WORLD_COORDINATE float m_scale_factor{ 1.0f }; diff --git a/src/slic3r/GUI/3DScene.hpp b/src/slic3r/GUI/3DScene.hpp index a85b8092a..4ef095223 100644 --- a/src/slic3r/GUI/3DScene.hpp +++ b/src/slic3r/GUI/3DScene.hpp @@ -1,783 +1,813 @@ -#ifndef slic3r_3DScene_hpp_ -#define slic3r_3DScene_hpp_ - -#include "libslic3r/libslic3r.h" -#include "libslic3r/Point.hpp" -#include "libslic3r/Line.hpp" -#include "libslic3r/TriangleMesh.hpp" -#include "libslic3r/Utils.hpp" -#include "libslic3r/Geometry.hpp" -#include "libslic3r/Color.hpp" - -#include "GLModel.hpp" - -#include -#include - -#ifndef NDEBUG -#define HAS_GLSAFE -#endif // NDEBUG - -#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) -#else // HAS_GLSAFE - inline void glAssertRecentCall() { } - #define glsafe(cmd) cmd - #define glcheck() -#endif // HAS_GLSAFE - -namespace Slic3r { -class SLAPrintObject; -enum SLAPrintObjectStep : unsigned int; -class BuildVolume; -class DynamicPrintConfig; -class ExtrusionPath; -class ExtrusionMultiPath; -class ExtrusionLoop; -class ExtrusionEntity; -class ExtrusionEntityCollection; -class ModelObject; -class ModelVolume; -enum ModelInstanceEPrintVolumeState : unsigned char; - -// Return appropriate color based on the ModelVolume. -extern ColorRGBA color_from_model_volume(const ModelVolume& model_volume); - -#if !ENABLE_LEGACY_OPENGL_REMOVAL -// A container for interleaved arrays of 3D vertices and normals, -// possibly indexed by triangles and / or quads. -class GLIndexedVertexArray { -public: - // Only Eigen types of Nx16 size are vectorized. This bounding box will not be vectorized. - static_assert(sizeof(Eigen::AlignedBox) == 24, "Eigen::AlignedBox is not being vectorized, thus it does not need to be aligned"); - using BoundingBox = Eigen::AlignedBox; - - GLIndexedVertexArray() { m_bounding_box.setEmpty(); } - GLIndexedVertexArray(const GLIndexedVertexArray &rhs) : - vertices_and_normals_interleaved(rhs.vertices_and_normals_interleaved), - triangle_indices(rhs.triangle_indices), - quad_indices(rhs.quad_indices), - m_bounding_box(rhs.m_bounding_box) - { assert(! rhs.has_VBOs()); m_bounding_box.setEmpty(); } - GLIndexedVertexArray(GLIndexedVertexArray &&rhs) : - vertices_and_normals_interleaved(std::move(rhs.vertices_and_normals_interleaved)), - triangle_indices(std::move(rhs.triangle_indices)), - quad_indices(std::move(rhs.quad_indices)), - m_bounding_box(rhs.m_bounding_box) - { assert(! rhs.has_VBOs()); } - - ~GLIndexedVertexArray() { release_geometry(); } - - GLIndexedVertexArray& operator=(const GLIndexedVertexArray &rhs) - { - assert(vertices_and_normals_interleaved_VBO_id == 0); - assert(triangle_indices_VBO_id == 0); - assert(quad_indices_VBO_id == 0); - assert(rhs.vertices_and_normals_interleaved_VBO_id == 0); - assert(rhs.triangle_indices_VBO_id == 0); - assert(rhs.quad_indices_VBO_id == 0); - this->vertices_and_normals_interleaved = rhs.vertices_and_normals_interleaved; - this->triangle_indices = rhs.triangle_indices; - this->quad_indices = rhs.quad_indices; - this->m_bounding_box = rhs.m_bounding_box; - this->vertices_and_normals_interleaved_size = rhs.vertices_and_normals_interleaved_size; - this->triangle_indices_size = rhs.triangle_indices_size; - this->quad_indices_size = rhs.quad_indices_size; - return *this; - } - - GLIndexedVertexArray& operator=(GLIndexedVertexArray &&rhs) - { - assert(vertices_and_normals_interleaved_VBO_id == 0); - assert(triangle_indices_VBO_id == 0); - assert(quad_indices_VBO_id == 0); - assert(rhs.vertices_and_normals_interleaved_VBO_id == 0); - assert(rhs.triangle_indices_VBO_id == 0); - assert(rhs.quad_indices_VBO_id == 0); - this->vertices_and_normals_interleaved = std::move(rhs.vertices_and_normals_interleaved); - this->triangle_indices = std::move(rhs.triangle_indices); - this->quad_indices = std::move(rhs.quad_indices); - this->m_bounding_box = rhs.m_bounding_box; - this->vertices_and_normals_interleaved_size = rhs.vertices_and_normals_interleaved_size; - this->triangle_indices_size = rhs.triangle_indices_size; - this->quad_indices_size = rhs.quad_indices_size; - return *this; - } - - // Vertices and their normals, interleaved to be used by void glInterleavedArrays(GL_N3F_V3F, 0, x) - std::vector vertices_and_normals_interleaved; - std::vector triangle_indices; - std::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 }; - -#if ENABLE_SMOOTH_NORMALS - void load_mesh_full_shading(const TriangleMesh& mesh, bool smooth_normals = false); - void load_mesh(const TriangleMesh& mesh, bool smooth_normals = false) { this->load_mesh_full_shading(mesh, smooth_normals); } -#else - void load_mesh_full_shading(const TriangleMesh& mesh); - void load_mesh(const TriangleMesh& mesh) { this->load_mesh_full_shading(mesh); } -#endif // ENABLE_SMOOTH_NORMALS - - void load_its_flat_shading(const indexed_triangle_set &its); - - inline bool has_VBOs() const { return vertices_and_normals_interleaved_VBO_id != 0; } - - inline void reserve(size_t sz) { - this->vertices_and_normals_interleaved.reserve(sz * 6); - this->triangle_indices.reserve(sz * 3); - this->quad_indices.reserve(sz * 4); - } - - inline void 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(); - m_bounding_box.extend(Vec3f(x, y, z)); - }; - - 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)); - } - - template - inline void push_geometry(const Eigen::MatrixBase& p, const Eigen::MatrixBase& n) { - push_geometry(float(p(0)), float(p(1)), float(p(2)), float(n(0)), float(n(1)), float(n(2))); - } - - inline void 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(); - }; - - inline void push_quad(int idx1, int idx2, int idx3, int idx4) { - assert(this->vertices_and_normals_interleaved_VBO_id == 0); - if (this->vertices_and_normals_interleaved_VBO_id != 0) - return; - - if (this->quad_indices.size() + 4 > this->vertices_and_normals_interleaved.capacity()) - this->quad_indices.reserve(next_highest_power_of_2(this->quad_indices.size() + 4)); - this->quad_indices.emplace_back(idx1); - this->quad_indices.emplace_back(idx2); - this->quad_indices.emplace_back(idx3); - this->quad_indices.emplace_back(idx4); - this->quad_indices_size = this->quad_indices.size(); - }; - - // 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(bool opengl_initialized); - // Release the geometry data, release OpenGL VBOs. - void release_geometry(); - - void render() const; - void render(const std::pair& tverts_range, const std::pair& qverts_range) const; - - // Is there any geometry data stored? - bool empty() const { return vertices_and_normals_interleaved_size == 0; } - - void 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; - m_bounding_box.setEmpty(); - } - - // Shrink the internal storage to tighly fit the data stored. - void shrink_to_fit() { - this->vertices_and_normals_interleaved.shrink_to_fit(); - this->triangle_indices.shrink_to_fit(); - this->quad_indices.shrink_to_fit(); - } - - const BoundingBox& bounding_box() const { return m_bounding_box; } - - // Return an estimate of the memory consumed by this class. - size_t cpu_memory_used() const { return sizeof(*this) + vertices_and_normals_interleaved.capacity() * sizeof(float) + triangle_indices.capacity() * sizeof(int) + quad_indices.capacity() * sizeof(int); } - // Return an estimate of the memory held by GPU vertex buffers. - size_t gpu_memory_used() const - { - size_t memsize = 0; - if (this->vertices_and_normals_interleaved_VBO_id != 0) - memsize += this->vertices_and_normals_interleaved_size * 4; - if (this->triangle_indices_VBO_id != 0) - memsize += this->triangle_indices_size * 4; - if (this->quad_indices_VBO_id != 0) - memsize += this->quad_indices_size * 4; - return memsize; - } - size_t total_memory_used() const { return this->cpu_memory_used() + this->gpu_memory_used(); } - -private: - BoundingBox m_bounding_box; -}; -#endif // !ENABLE_LEGACY_OPENGL_REMOVAL - -class GLVolume { -public: - static const ColorRGBA SELECTED_COLOR; - static const ColorRGBA HOVER_SELECT_COLOR; - static const ColorRGBA HOVER_DESELECT_COLOR; - static const ColorRGBA OUTSIDE_COLOR; - static const ColorRGBA SELECTED_OUTSIDE_COLOR; - static const ColorRGBA DISABLED_COLOR; - static const ColorRGBA SLA_SUPPORT_COLOR; - static const ColorRGBA SLA_PAD_COLOR; - static const ColorRGBA NEUTRAL_COLOR; - static const std::array MODEL_COLOR; - - enum EHoverState : unsigned char - { - HS_None, - HS_Hover, - HS_Select, - HS_Deselect - }; - - GLVolume(float r = 1.0f, float g = 1.0f, float b = 1.0f, float a = 1.0f); - GLVolume(const ColorRGBA& color) : GLVolume(color.r(), color.g(), color.b(), color.a()) {} - -private: - Geometry::Transformation m_instance_transformation; - Geometry::Transformation m_volume_transformation; - - // Shift in z required by sla supports+pad - double m_sla_shift_z; - // Bounding box of this volume, in unscaled coordinates. - std::optional m_transformed_bounding_box; - // Convex hull of the volume, if any. - std::shared_ptr m_convex_hull; - // Bounding box of this volume, in unscaled coordinates. - std::optional m_transformed_convex_hull_bounding_box; - // Bounding box of the non sinking part of this volume, in unscaled coordinates. - std::optional m_transformed_non_sinking_bounding_box; - - class SinkingContours - { - static const float HalfWidth; - GLVolume& m_parent; - GUI::GLModel m_model; - BoundingBoxf3 m_old_box; - Vec3d m_shift{ Vec3d::Zero() }; - - public: - SinkingContours(GLVolume& volume) : m_parent(volume) {} - void render(); - - private: - void update(); - }; - - SinkingContours m_sinking_contours; - -#if ENABLE_SHOW_NON_MANIFOLD_EDGES - class NonManifoldEdges - { - GLVolume& m_parent; - GUI::GLModel m_model; - bool m_update_needed{ true }; - - public: - NonManifoldEdges(GLVolume& volume) : m_parent(volume) {} - void render(); - void set_as_dirty() { m_update_needed = true; } - - private: - void update(); - }; - - NonManifoldEdges m_non_manifold_edges; -#endif // ENABLE_SHOW_NON_MANIFOLD_EDGES - -public: - // Color of the triangles / quads held by this volume. - ColorRGBA color; - // Color used to render this volume. - ColorRGBA render_color; - - struct CompositeID { - CompositeID(int object_id, int volume_id, int instance_id) : object_id(object_id), volume_id(volume_id), instance_id(instance_id) {} - CompositeID() : object_id(-1), volume_id(-1), instance_id(-1) {} - // Object ID, which is equal to the index of the respective ModelObject in Model.objects array. - int object_id; - // Volume ID, which is equal to the index of the respective ModelVolume in ModelObject.volumes array. - // If negative, it is an index of a geometry produced by the PrintObject for the respective ModelObject, - // and which has no associated ModelVolume in ModelObject.volumes. For example, SLA supports. - // Volume with a negative volume_id cannot be picked independently, it will pick the associated instance. - int volume_id; - // Instance ID, which is equal to the index of the respective ModelInstance in ModelObject.instances array. - int instance_id; - bool operator==(const CompositeID &rhs) const { return object_id == rhs.object_id && volume_id == rhs.volume_id && instance_id == rhs.instance_id; } - bool operator!=(const CompositeID &rhs) const { return ! (*this == rhs); } - bool operator< (const CompositeID &rhs) const - { return object_id < rhs.object_id || (object_id == rhs.object_id && (volume_id < rhs.volume_id || (volume_id == rhs.volume_id && instance_id < rhs.instance_id))); } - }; - CompositeID composite_id; - // Fingerprint of the source geometry. For ModelVolumes, it is the ModelVolume::ID and ModelInstanceID, - // for generated volumes it is the timestamp generated by PrintState::invalidate() or PrintState::set_done(), - // and the associated ModelInstanceID. - // Valid geometry_id should always be positive. - std::pair geometry_id; - // An ID containing the extruder ID (used to select color). - int extruder_id; - - // Various boolean flags. - struct { - // Is this object selected? - bool selected : 1; - // Is this object disabled from selection? - bool disabled : 1; - // Is this object printable? - bool printable : 1; - // Whether or not this volume is active for rendering - bool is_active : 1; - // Whether or not to use this volume when applying zoom_to_volumes() - bool zoom_to_volumes : 1; - // Wheter or not this volume is enabled for outside print volume detection in shader. - bool shader_outside_printer_detection_enabled : 1; - // Wheter or not this volume is outside print volume. - bool is_outside : 1; - // Wheter or not this volume has been generated from a modifier - bool is_modifier : 1; - // Wheter or not this volume has been generated from the wipe tower - bool is_wipe_tower : 1; - // Wheter or not this volume has been generated from an extrusion path - bool is_extrusion_path : 1; - // Whether or not always use the volume's own color (not using SELECTED/HOVER/DISABLED/OUTSIDE) - bool force_native_color : 1; - // Whether or not render this volume in neutral - bool force_neutral_color : 1; - // Whether or not to force rendering of sinking contours - bool force_sinking_contours : 1; - }; - - // Is mouse or rectangle selection over this object to select/deselect it ? - EHoverState hover; - -#if ENABLE_LEGACY_OPENGL_REMOVAL - GUI::GLModel model; -#else - // Interleaved triangles & normals with indexed triangles & quads. - GLIndexedVertexArray indexed_vertex_array; -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - // Ranges of triangle and quad indices to be rendered. - std::pair tverts_range; -#if !ENABLE_LEGACY_OPENGL_REMOVAL - std::pair qverts_range; -#endif // !ENABLE_LEGACY_OPENGL_REMOVAL - - // If the qverts or tverts contain thick extrusions, then offsets keeps pointers of the starts - // of the extrusions per layer. - std::vector print_zs; - // Offset into qverts & tverts, or offsets into indices stored into an OpenGL name_index_buffer. - std::vector offsets; - - // Bounding box of this volume, in unscaled coordinates. - BoundingBoxf3 bounding_box() const { -#if ENABLE_LEGACY_OPENGL_REMOVAL - return this->model.get_bounding_box(); -#else - BoundingBoxf3 out; - if (!this->indexed_vertex_array.bounding_box().isEmpty()) { - out.min = this->indexed_vertex_array.bounding_box().min().cast(); - out.max = this->indexed_vertex_array.bounding_box().max().cast(); - out.defined = true; - } - return out; -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - } - - void set_color(const ColorRGBA& rgba) { color = rgba; } - void set_render_color(const ColorRGBA& rgba) { render_color = rgba; } - // Sets render color in dependence of current state - void set_render_color(bool force_transparent); - // set color according to model volume - void set_color_from_model_volume(const ModelVolume& model_volume); - - const Geometry::Transformation& get_instance_transformation() const { return m_instance_transformation; } - void set_instance_transformation(const Geometry::Transformation& transformation) { m_instance_transformation = transformation; set_bounding_boxes_as_dirty(); } - - const Vec3d& get_instance_offset() const { return m_instance_transformation.get_offset(); } - double get_instance_offset(Axis axis) const { return m_instance_transformation.get_offset(axis); } - - void set_instance_offset(const Vec3d& offset) { m_instance_transformation.set_offset(offset); set_bounding_boxes_as_dirty(); } - void set_instance_offset(Axis axis, double offset) { m_instance_transformation.set_offset(axis, offset); set_bounding_boxes_as_dirty(); } - - const Vec3d& get_instance_rotation() const { return m_instance_transformation.get_rotation(); } - double get_instance_rotation(Axis axis) const { return m_instance_transformation.get_rotation(axis); } - - void set_instance_rotation(const Vec3d& rotation) { m_instance_transformation.set_rotation(rotation); set_bounding_boxes_as_dirty(); } - void set_instance_rotation(Axis axis, double rotation) { m_instance_transformation.set_rotation(axis, rotation); set_bounding_boxes_as_dirty(); } - - Vec3d get_instance_scaling_factor() const { return m_instance_transformation.get_scaling_factor(); } - double get_instance_scaling_factor(Axis axis) const { return m_instance_transformation.get_scaling_factor(axis); } - - void set_instance_scaling_factor(const Vec3d& scaling_factor) { m_instance_transformation.set_scaling_factor(scaling_factor); set_bounding_boxes_as_dirty(); } - void set_instance_scaling_factor(Axis axis, double scaling_factor) { m_instance_transformation.set_scaling_factor(axis, scaling_factor); set_bounding_boxes_as_dirty(); } - - const Vec3d& get_instance_mirror() const { return m_instance_transformation.get_mirror(); } - double get_instance_mirror(Axis axis) const { return m_instance_transformation.get_mirror(axis); } - - void set_instance_mirror(const Vec3d& mirror) { m_instance_transformation.set_mirror(mirror); set_bounding_boxes_as_dirty(); } - void set_instance_mirror(Axis axis, double mirror) { m_instance_transformation.set_mirror(axis, mirror); set_bounding_boxes_as_dirty(); } - - const Geometry::Transformation& get_volume_transformation() const { return m_volume_transformation; } - void set_volume_transformation(const Geometry::Transformation& transformation) { m_volume_transformation = transformation; set_bounding_boxes_as_dirty(); } - - const Vec3d& get_volume_offset() const { return m_volume_transformation.get_offset(); } - double get_volume_offset(Axis axis) const { return m_volume_transformation.get_offset(axis); } - - void set_volume_offset(const Vec3d& offset) { m_volume_transformation.set_offset(offset); set_bounding_boxes_as_dirty(); } - void set_volume_offset(Axis axis, double offset) { m_volume_transformation.set_offset(axis, offset); set_bounding_boxes_as_dirty(); } - - const Vec3d& get_volume_rotation() const { return m_volume_transformation.get_rotation(); } - double get_volume_rotation(Axis axis) const { return m_volume_transformation.get_rotation(axis); } - - void set_volume_rotation(const Vec3d& rotation) { m_volume_transformation.set_rotation(rotation); set_bounding_boxes_as_dirty(); } - void set_volume_rotation(Axis axis, double rotation) { m_volume_transformation.set_rotation(axis, rotation); set_bounding_boxes_as_dirty(); } - - const Vec3d& get_volume_scaling_factor() const { return m_volume_transformation.get_scaling_factor(); } - double get_volume_scaling_factor(Axis axis) const { return m_volume_transformation.get_scaling_factor(axis); } - - void set_volume_scaling_factor(const Vec3d& scaling_factor) { m_volume_transformation.set_scaling_factor(scaling_factor); set_bounding_boxes_as_dirty(); } - void set_volume_scaling_factor(Axis axis, double scaling_factor) { m_volume_transformation.set_scaling_factor(axis, scaling_factor); set_bounding_boxes_as_dirty(); } - - const Vec3d& get_volume_mirror() const { return m_volume_transformation.get_mirror(); } - double get_volume_mirror(Axis axis) const { return m_volume_transformation.get_mirror(axis); } - - void set_volume_mirror(const Vec3d& mirror) { m_volume_transformation.set_mirror(mirror); set_bounding_boxes_as_dirty(); } - void set_volume_mirror(Axis axis, double mirror) { m_volume_transformation.set_mirror(axis, mirror); set_bounding_boxes_as_dirty(); } - - double get_sla_shift_z() const { return m_sla_shift_z; } - void set_sla_shift_z(double z) { m_sla_shift_z = z; } - - void set_convex_hull(std::shared_ptr convex_hull) { m_convex_hull = std::move(convex_hull); } - void set_convex_hull(const TriangleMesh &convex_hull) { m_convex_hull = std::make_shared(convex_hull); } - void set_convex_hull(TriangleMesh &&convex_hull) { m_convex_hull = std::make_shared(std::move(convex_hull)); } - - int object_idx() const { return this->composite_id.object_id; } - int volume_idx() const { return this->composite_id.volume_id; } - int instance_idx() const { return this->composite_id.instance_id; } - - Transform3d world_matrix() const; - bool is_left_handed() const; - - const BoundingBoxf3& transformed_bounding_box() const; - // non-caching variant - BoundingBoxf3 transformed_convex_hull_bounding_box(const Transform3d &trafo) const; - // caching variant - const BoundingBoxf3& transformed_convex_hull_bounding_box() const; - // non-caching variant - BoundingBoxf3 transformed_non_sinking_bounding_box(const Transform3d& trafo) const; - // caching variant - const BoundingBoxf3& transformed_non_sinking_bounding_box() const; - // convex hull - const TriangleMesh* convex_hull() const { return m_convex_hull.get(); } - -#if ENABLE_LEGACY_OPENGL_REMOVAL - bool empty() const { return this->model.is_empty(); } -#else - bool empty() const { return this->indexed_vertex_array.empty(); } -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - - void set_range(double low, double high); - - void render(); - -#if !ENABLE_LEGACY_OPENGL_REMOVAL - void finalize_geometry(bool opengl_initialized) { this->indexed_vertex_array.finalize_geometry(opengl_initialized); } - void release_geometry() { this->indexed_vertex_array.release_geometry(); } -#endif // !ENABLE_LEGACY_OPENGL_REMOVAL - - void set_bounding_boxes_as_dirty() { - m_transformed_bounding_box.reset(); - m_transformed_convex_hull_bounding_box.reset(); - m_transformed_non_sinking_bounding_box.reset(); - } - - bool is_sla_support() const; - bool is_sla_pad() const; - - bool is_sinking() const; - bool is_below_printbed() const; - void render_sinking_contours(); -#if ENABLE_SHOW_NON_MANIFOLD_EDGES - void render_non_manifold_edges(); -#endif // ENABLE_SHOW_NON_MANIFOLD_EDGES - - // Return an estimate of the memory consumed by this class. - size_t cpu_memory_used() const { -#if ENABLE_LEGACY_OPENGL_REMOVAL - return sizeof(*this) + this->model.cpu_memory_used() + this->print_zs.capacity() * sizeof(coordf_t) + - this->offsets.capacity() * sizeof(size_t); - } - // Return an estimate of the memory held by GPU vertex buffers. - size_t gpu_memory_used() const { return this->model.gpu_memory_used(); } -#else - //FIXME what to do wih m_convex_hull? - return sizeof(*this) - sizeof(this->indexed_vertex_array) + this->indexed_vertex_array.cpu_memory_used() + this->print_zs.capacity() * sizeof(coordf_t) + this->offsets.capacity() * sizeof(size_t); - } - // Return an estimate of the memory held by GPU vertex buffers. - size_t gpu_memory_used() const { return this->indexed_vertex_array.gpu_memory_used(); } -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - size_t total_memory_used() const { return this->cpu_memory_used() + this->gpu_memory_used(); } -}; - -typedef std::vector GLVolumePtrs; -typedef std::pair> GLVolumeWithIdAndZ; -typedef std::vector GLVolumeWithIdAndZList; - -class GLVolumeCollection -{ -public: - enum class ERenderType : unsigned char - { - Opaque, - Transparent, - All - }; - - struct PrintVolume - { - // see: Bed3D::EShapeType - int type{ 0 }; - // data contains: - // Rectangle: - // [0] = min.x, [1] = min.y, [2] = max.x, [3] = max.y - // Circle: - // [0] = center.x, [1] = center.y, [3] = radius - std::array data; - // [0] = min z, [1] = max z - std::array zs; - }; - -private: - PrintVolume m_print_volume; - - // z range for clipping in shaders - std::array m_z_range; - - // plane coeffs for clipping in shaders - std::array m_clipping_plane; - - struct Slope - { - // toggle for slope rendering - bool active{ false }; - float normal_z; - }; - - Slope m_slope; - bool m_show_sinking_contours{ false }; -#if ENABLE_SHOW_NON_MANIFOLD_EDGES - bool m_show_non_manifold_edges{ true }; -#endif // ENABLE_SHOW_NON_MANIFOLD_EDGES - -public: - GLVolumePtrs volumes; - - GLVolumeCollection() { set_default_slope_normal_z(); } - ~GLVolumeCollection() { clear(); } - -#if ENABLE_LEGACY_OPENGL_REMOVAL - std::vector load_object( - const ModelObject* model_object, - int obj_idx, - const std::vector& instance_idxs); - - int load_object_volume( - const ModelObject* model_object, - int obj_idx, - int volume_idx, - int instance_idx); - - // Load SLA auxiliary GLVolumes (for support trees or pad). - void load_object_auxiliary( - const SLAPrintObject* print_object, - int obj_idx, - // pairs of - const std::vector>& instances, - SLAPrintObjectStep milestone, - // Timestamp of the last change of the milestone - size_t timestamp); - -#if ENABLE_WIPETOWER_OBJECTID_1000_REMOVAL - int load_wipe_tower_preview( - float pos_x, float pos_y, float width, float depth, float height, float rotation_angle, bool size_unknown, float brim_width); -#else - int load_wipe_tower_preview( - int obj_idx, float pos_x, float pos_y, float width, float depth, float height, float rotation_angle, bool size_unknown, float brim_width); -#endif // ENABLE_WIPETOWER_OBJECTID_1000_REMOVAL -#else - std::vector load_object( - const ModelObject *model_object, - int obj_idx, - const std::vector &instance_idxs, - bool opengl_initialized); - - int load_object_volume( - const ModelObject *model_object, - int obj_idx, - int volume_idx, - int instance_idx, - bool opengl_initialized); - - // Load SLA auxiliary GLVolumes (for support trees or pad). - void load_object_auxiliary( - const SLAPrintObject *print_object, - int obj_idx, - // pairs of - const std::vector>& instances, - SLAPrintObjectStep milestone, - // Timestamp of the last change of the milestone - size_t timestamp, - bool opengl_initialized); - -#if ENABLE_WIPETOWER_OBJECTID_1000_REMOVAL - int load_wipe_tower_preview( - float pos_x, float pos_y, float width, float depth, float height, float rotation_angle, bool size_unknown, float brim_width, bool opengl_initialized); -#else - int load_wipe_tower_preview( - int obj_idx, float pos_x, float pos_y, float width, float depth, float height, float rotation_angle, bool size_unknown, float brim_width, bool opengl_initialized); -#endif // ENABLE_WIPETOWER_OBJECTID_1000_REMOVAL -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - -#if ENABLE_LEGACY_OPENGL_REMOVAL - GLVolume* new_toolpath_volume(const ColorRGBA& rgba); - GLVolume* new_nontoolpath_volume(const ColorRGBA& rgba); -#else - GLVolume* new_toolpath_volume(const ColorRGBA& rgba, size_t reserve_vbo_floats = 0); - GLVolume* new_nontoolpath_volume(const ColorRGBA& rgba, size_t reserve_vbo_floats = 0); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - - // Render the volumes by OpenGL. -#if ENABLE_GL_SHADERS_ATTRIBUTES - void render(ERenderType type, bool disable_cullface, const Transform3d& view_matrix, const Transform3d& projection_matrix, - std::function filter_func = std::function()) const; -#else - void render(ERenderType type, bool disable_cullface, const Transform3d& view_matrix, std::function filter_func = std::function()) const; -#endif // ENABLE_GL_SHADERS_ATTRIBUTES - -#if !ENABLE_LEGACY_OPENGL_REMOVAL - // 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(bool opengl_initialized) { for (auto* v : volumes) v->finalize_geometry(opengl_initialized); } - // Release the geometry data assigned to the volumes. - // If OpenGL VBOs were allocated, an OpenGL context has to be active to release them. - void release_geometry() { for (auto *v : volumes) v->release_geometry(); } -#endif // !ENABLE_LEGACY_OPENGL_REMOVAL - // Clear the geometry - void clear() { for (auto *v : volumes) delete v; volumes.clear(); } - - bool empty() const { return volumes.empty(); } - void set_range(double low, double high) { for (GLVolume* vol : this->volumes) vol->set_range(low, high); } - - void set_print_volume(const PrintVolume& print_volume) { m_print_volume = print_volume; } - - void set_z_range(float min_z, float max_z) { m_z_range[0] = min_z; m_z_range[1] = max_z; } - void set_clipping_plane(const std::array& coeffs) { m_clipping_plane = coeffs; } - - const std::array& get_z_range() const { return m_z_range; } - const std::array& get_clipping_plane() const { return m_clipping_plane; } - - bool is_slope_active() const { return m_slope.active; } - void set_slope_active(bool active) { m_slope.active = active; } - - float get_slope_normal_z() const { return m_slope.normal_z; } - void set_slope_normal_z(float normal_z) { m_slope.normal_z = normal_z; } - void set_default_slope_normal_z() { m_slope.normal_z = -::cos(Geometry::deg2rad(90.0f - 45.0f)); } - void set_show_sinking_contours(bool show) { m_show_sinking_contours = show; } -#if ENABLE_SHOW_NON_MANIFOLD_EDGES - void set_show_non_manifold_edges(bool show) { m_show_non_manifold_edges = show; } -#endif // ENABLE_SHOW_NON_MANIFOLD_EDGES - - // returns true if all the volumes are completely contained in the print volume - // returns the containment state in the given out_state, if non-null - bool check_outside_state(const Slic3r::BuildVolume& build_volume, ModelInstanceEPrintVolumeState* out_state) const; - void reset_outside_state(); - - void update_colors_by_extruder(const DynamicPrintConfig* config); - - // Returns a vector containing the sorted list of all the print_zs of the volumes contained in this collection - std::vector get_current_print_zs(bool active_only) const; - - // Return an estimate of the memory consumed by this class. - size_t cpu_memory_used() const; - // Return an estimate of the memory held by GPU vertex buffers. - size_t gpu_memory_used() const; - size_t total_memory_used() const { return this->cpu_memory_used() + this->gpu_memory_used(); } - // Return CPU, GPU and total memory log line. - std::string log_memory_info() const; - -private: - GLVolumeCollection(const GLVolumeCollection &other); - GLVolumeCollection& operator=(const GLVolumeCollection &); -}; - -GLVolumeWithIdAndZList volumes_to_render(const GLVolumePtrs& volumes, GLVolumeCollection::ERenderType type, const Transform3d& view_matrix, std::function filter_func = nullptr); - -struct _3DScene -{ -#if ENABLE_LEGACY_OPENGL_REMOVAL - static void thick_lines_to_verts(const Lines& lines, const std::vector& widths, const std::vector& heights, bool closed, double top_z, GUI::GLModel::Geometry& geometry); - static void thick_lines_to_verts(const Lines3& lines, const std::vector& widths, const std::vector& heights, bool closed, GUI::GLModel::Geometry& geometry); - static void extrusionentity_to_verts(const ExtrusionPath& extrusion_path, float print_z, const Point& copy, GUI::GLModel::Geometry& geometry); - static void extrusionentity_to_verts(const ExtrusionLoop& extrusion_loop, float print_z, const Point& copy, GUI::GLModel::Geometry& geometry); - static void extrusionentity_to_verts(const ExtrusionMultiPath& extrusion_multi_path, float print_z, const Point& copy, GUI::GLModel::Geometry& geometry); - static void extrusionentity_to_verts(const ExtrusionEntityCollection& extrusion_entity_collection, float print_z, const Point& copy, GUI::GLModel::Geometry& geometry); - static void extrusionentity_to_verts(const ExtrusionEntity* extrusion_entity, float print_z, const Point& copy, GUI::GLModel::Geometry& geometry); -#else - static void thick_lines_to_verts(const Lines& lines, const std::vector& widths, const std::vector& heights, bool closed, double top_z, GLVolume& volume); - static void thick_lines_to_verts(const Lines3& lines, const std::vector& widths, const std::vector& heights, bool closed, GLVolume& volume); - static void extrusionentity_to_verts(const Polyline& polyline, float width, float height, float print_z, GLVolume& volume); - static void extrusionentity_to_verts(const ExtrusionPath& extrusion_path, float print_z, GLVolume& volume); - static void extrusionentity_to_verts(const ExtrusionPath& extrusion_path, float print_z, const Point& copy, GLVolume& volume); - static void extrusionentity_to_verts(const ExtrusionLoop& extrusion_loop, float print_z, const Point& copy, GLVolume& volume); - static void extrusionentity_to_verts(const ExtrusionMultiPath& extrusion_multi_path, float print_z, const Point& copy, GLVolume& volume); - static void extrusionentity_to_verts(const ExtrusionEntityCollection& extrusion_entity_collection, float print_z, const Point& copy, GLVolume& volume); - static void extrusionentity_to_verts(const ExtrusionEntity* extrusion_entity, float print_z, const Point& copy, GLVolume& volume); - static void polyline3_to_verts(const Polyline3& polyline, double width, double height, GLVolume& volume); - static void point3_to_verts(const Vec3crd& point, double width, double height, GLVolume& volume); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL -}; - -} - -#endif +#ifndef slic3r_3DScene_hpp_ +#define slic3r_3DScene_hpp_ + +#include "libslic3r/libslic3r.h" +#include "libslic3r/Point.hpp" +#include "libslic3r/Line.hpp" +#include "libslic3r/TriangleMesh.hpp" +#include "libslic3r/Utils.hpp" +#include "libslic3r/Geometry.hpp" +#include "libslic3r/Color.hpp" + +#include "GLModel.hpp" + +#include +#include + +#ifndef NDEBUG +#define HAS_GLSAFE +#endif // NDEBUG + +#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) +#else // HAS_GLSAFE + inline void glAssertRecentCall() { } + #define glsafe(cmd) cmd + #define glcheck() +#endif // HAS_GLSAFE + +namespace Slic3r { +class SLAPrintObject; +enum SLAPrintObjectStep : unsigned int; +class BuildVolume; +class DynamicPrintConfig; +class ExtrusionPath; +class ExtrusionMultiPath; +class ExtrusionLoop; +class ExtrusionEntity; +class ExtrusionEntityCollection; +class ModelObject; +class ModelVolume; +enum ModelInstanceEPrintVolumeState : unsigned char; + +// Return appropriate color based on the ModelVolume. +extern ColorRGBA color_from_model_volume(const ModelVolume& model_volume); + +#if !ENABLE_LEGACY_OPENGL_REMOVAL +// A container for interleaved arrays of 3D vertices and normals, +// possibly indexed by triangles and / or quads. +class GLIndexedVertexArray { +public: + // Only Eigen types of Nx16 size are vectorized. This bounding box will not be vectorized. + static_assert(sizeof(Eigen::AlignedBox) == 24, "Eigen::AlignedBox is not being vectorized, thus it does not need to be aligned"); + using BoundingBox = Eigen::AlignedBox; + + GLIndexedVertexArray() { m_bounding_box.setEmpty(); } + GLIndexedVertexArray(const GLIndexedVertexArray &rhs) : + vertices_and_normals_interleaved(rhs.vertices_and_normals_interleaved), + triangle_indices(rhs.triangle_indices), + quad_indices(rhs.quad_indices), + m_bounding_box(rhs.m_bounding_box) + { assert(! rhs.has_VBOs()); m_bounding_box.setEmpty(); } + GLIndexedVertexArray(GLIndexedVertexArray &&rhs) : + vertices_and_normals_interleaved(std::move(rhs.vertices_and_normals_interleaved)), + triangle_indices(std::move(rhs.triangle_indices)), + quad_indices(std::move(rhs.quad_indices)), + m_bounding_box(rhs.m_bounding_box) + { assert(! rhs.has_VBOs()); } + + ~GLIndexedVertexArray() { release_geometry(); } + + GLIndexedVertexArray& operator=(const GLIndexedVertexArray &rhs) + { + assert(vertices_and_normals_interleaved_VBO_id == 0); + assert(triangle_indices_VBO_id == 0); + assert(quad_indices_VBO_id == 0); + assert(rhs.vertices_and_normals_interleaved_VBO_id == 0); + assert(rhs.triangle_indices_VBO_id == 0); + assert(rhs.quad_indices_VBO_id == 0); + this->vertices_and_normals_interleaved = rhs.vertices_and_normals_interleaved; + this->triangle_indices = rhs.triangle_indices; + this->quad_indices = rhs.quad_indices; + this->m_bounding_box = rhs.m_bounding_box; + this->vertices_and_normals_interleaved_size = rhs.vertices_and_normals_interleaved_size; + this->triangle_indices_size = rhs.triangle_indices_size; + this->quad_indices_size = rhs.quad_indices_size; + return *this; + } + + GLIndexedVertexArray& operator=(GLIndexedVertexArray &&rhs) + { + assert(vertices_and_normals_interleaved_VBO_id == 0); + assert(triangle_indices_VBO_id == 0); + assert(quad_indices_VBO_id == 0); + assert(rhs.vertices_and_normals_interleaved_VBO_id == 0); + assert(rhs.triangle_indices_VBO_id == 0); + assert(rhs.quad_indices_VBO_id == 0); + this->vertices_and_normals_interleaved = std::move(rhs.vertices_and_normals_interleaved); + this->triangle_indices = std::move(rhs.triangle_indices); + this->quad_indices = std::move(rhs.quad_indices); + this->m_bounding_box = rhs.m_bounding_box; + this->vertices_and_normals_interleaved_size = rhs.vertices_and_normals_interleaved_size; + this->triangle_indices_size = rhs.triangle_indices_size; + this->quad_indices_size = rhs.quad_indices_size; + return *this; + } + + // Vertices and their normals, interleaved to be used by void glInterleavedArrays(GL_N3F_V3F, 0, x) + std::vector vertices_and_normals_interleaved; + std::vector triangle_indices; + std::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 }; + +#if ENABLE_SMOOTH_NORMALS + void load_mesh_full_shading(const TriangleMesh& mesh, bool smooth_normals = false); + void load_mesh(const TriangleMesh& mesh, bool smooth_normals = false) { this->load_mesh_full_shading(mesh, smooth_normals); } +#else + void load_mesh_full_shading(const TriangleMesh& mesh); + void load_mesh(const TriangleMesh& mesh) { this->load_mesh_full_shading(mesh); } +#endif // ENABLE_SMOOTH_NORMALS + + void load_its_flat_shading(const indexed_triangle_set &its); + + inline bool has_VBOs() const { return vertices_and_normals_interleaved_VBO_id != 0; } + + inline void reserve(size_t sz) { + this->vertices_and_normals_interleaved.reserve(sz * 6); + this->triangle_indices.reserve(sz * 3); + this->quad_indices.reserve(sz * 4); + } + + inline void 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(); + m_bounding_box.extend(Vec3f(x, y, z)); + }; + + 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)); + } + + template + inline void push_geometry(const Eigen::MatrixBase& p, const Eigen::MatrixBase& n) { + push_geometry(float(p(0)), float(p(1)), float(p(2)), float(n(0)), float(n(1)), float(n(2))); + } + + inline void 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(); + }; + + inline void push_quad(int idx1, int idx2, int idx3, int idx4) { + assert(this->vertices_and_normals_interleaved_VBO_id == 0); + if (this->vertices_and_normals_interleaved_VBO_id != 0) + return; + + if (this->quad_indices.size() + 4 > this->vertices_and_normals_interleaved.capacity()) + this->quad_indices.reserve(next_highest_power_of_2(this->quad_indices.size() + 4)); + this->quad_indices.emplace_back(idx1); + this->quad_indices.emplace_back(idx2); + this->quad_indices.emplace_back(idx3); + this->quad_indices.emplace_back(idx4); + this->quad_indices_size = this->quad_indices.size(); + }; + + // 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(bool opengl_initialized); + // Release the geometry data, release OpenGL VBOs. + void release_geometry(); + + void render() const; + void render(const std::pair& tverts_range, const std::pair& qverts_range) const; + + // Is there any geometry data stored? + bool empty() const { return vertices_and_normals_interleaved_size == 0; } + + void 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; + m_bounding_box.setEmpty(); + } + + // Shrink the internal storage to tighly fit the data stored. + void shrink_to_fit() { + this->vertices_and_normals_interleaved.shrink_to_fit(); + this->triangle_indices.shrink_to_fit(); + this->quad_indices.shrink_to_fit(); + } + + const BoundingBox& bounding_box() const { return m_bounding_box; } + + // Return an estimate of the memory consumed by this class. + size_t cpu_memory_used() const { return sizeof(*this) + vertices_and_normals_interleaved.capacity() * sizeof(float) + triangle_indices.capacity() * sizeof(int) + quad_indices.capacity() * sizeof(int); } + // Return an estimate of the memory held by GPU vertex buffers. + size_t gpu_memory_used() const + { + size_t memsize = 0; + if (this->vertices_and_normals_interleaved_VBO_id != 0) + memsize += this->vertices_and_normals_interleaved_size * 4; + if (this->triangle_indices_VBO_id != 0) + memsize += this->triangle_indices_size * 4; + if (this->quad_indices_VBO_id != 0) + memsize += this->quad_indices_size * 4; + return memsize; + } + size_t total_memory_used() const { return this->cpu_memory_used() + this->gpu_memory_used(); } + +private: + BoundingBox m_bounding_box; +}; +#endif // !ENABLE_LEGACY_OPENGL_REMOVAL + +class GLVolume { +public: + static const ColorRGBA SELECTED_COLOR; + static const ColorRGBA HOVER_SELECT_COLOR; + static const ColorRGBA HOVER_DESELECT_COLOR; + static const ColorRGBA OUTSIDE_COLOR; + static const ColorRGBA SELECTED_OUTSIDE_COLOR; + static const ColorRGBA DISABLED_COLOR; + static const ColorRGBA SLA_SUPPORT_COLOR; + static const ColorRGBA SLA_PAD_COLOR; + static const ColorRGBA NEUTRAL_COLOR; + static const std::array MODEL_COLOR; + + enum EHoverState : unsigned char + { + HS_None, + HS_Hover, + HS_Select, + HS_Deselect + }; + + GLVolume(float r = 1.0f, float g = 1.0f, float b = 1.0f, float a = 1.0f); + GLVolume(const ColorRGBA& color) : GLVolume(color.r(), color.g(), color.b(), color.a()) {} + +private: + Geometry::Transformation m_instance_transformation; + Geometry::Transformation m_volume_transformation; + + // Shift in z required by sla supports+pad + double m_sla_shift_z; + // Bounding box of this volume, in unscaled coordinates. + std::optional m_transformed_bounding_box; + // Convex hull of the volume, if any. + std::shared_ptr m_convex_hull; + // Bounding box of this volume, in unscaled coordinates. + std::optional m_transformed_convex_hull_bounding_box; + // Bounding box of the non sinking part of this volume, in unscaled coordinates. + std::optional m_transformed_non_sinking_bounding_box; + + class SinkingContours + { + static const float HalfWidth; + GLVolume& m_parent; + GUI::GLModel m_model; + BoundingBoxf3 m_old_box; + Vec3d m_shift{ Vec3d::Zero() }; + + public: + SinkingContours(GLVolume& volume) : m_parent(volume) {} + void render(); + + private: + void update(); + }; + + SinkingContours m_sinking_contours; + +#if ENABLE_SHOW_NON_MANIFOLD_EDGES + class NonManifoldEdges + { + GLVolume& m_parent; + GUI::GLModel m_model; + bool m_update_needed{ true }; + + public: + NonManifoldEdges(GLVolume& volume) : m_parent(volume) {} + void render(); + void set_as_dirty() { m_update_needed = true; } + + private: + void update(); + }; + + NonManifoldEdges m_non_manifold_edges; +#endif // ENABLE_SHOW_NON_MANIFOLD_EDGES + +public: + // Color of the triangles / quads held by this volume. + ColorRGBA color; + // Color used to render this volume. + ColorRGBA render_color; + + struct CompositeID { + CompositeID(int object_id, int volume_id, int instance_id) : object_id(object_id), volume_id(volume_id), instance_id(instance_id) {} + CompositeID() : object_id(-1), volume_id(-1), instance_id(-1) {} + // Object ID, which is equal to the index of the respective ModelObject in Model.objects array. + int object_id; + // Volume ID, which is equal to the index of the respective ModelVolume in ModelObject.volumes array. + // If negative, it is an index of a geometry produced by the PrintObject for the respective ModelObject, + // and which has no associated ModelVolume in ModelObject.volumes. For example, SLA supports. + // Volume with a negative volume_id cannot be picked independently, it will pick the associated instance. + int volume_id; + // Instance ID, which is equal to the index of the respective ModelInstance in ModelObject.instances array. + int instance_id; + bool operator==(const CompositeID &rhs) const { return object_id == rhs.object_id && volume_id == rhs.volume_id && instance_id == rhs.instance_id; } + bool operator!=(const CompositeID &rhs) const { return ! (*this == rhs); } + bool operator< (const CompositeID &rhs) const + { return object_id < rhs.object_id || (object_id == rhs.object_id && (volume_id < rhs.volume_id || (volume_id == rhs.volume_id && instance_id < rhs.instance_id))); } + }; + CompositeID composite_id; + // Fingerprint of the source geometry. For ModelVolumes, it is the ModelVolume::ID and ModelInstanceID, + // for generated volumes it is the timestamp generated by PrintState::invalidate() or PrintState::set_done(), + // and the associated ModelInstanceID. + // Valid geometry_id should always be positive. + std::pair geometry_id; + // An ID containing the extruder ID (used to select color). + int extruder_id; + + // Various boolean flags. + struct { + // Is this object selected? + bool selected : 1; + // Is this object disabled from selection? + bool disabled : 1; + // Is this object printable? + bool printable : 1; + // Whether or not this volume is active for rendering + bool is_active : 1; + // Whether or not to use this volume when applying zoom_to_volumes() + bool zoom_to_volumes : 1; + // Wheter or not this volume is enabled for outside print volume detection in shader. + bool shader_outside_printer_detection_enabled : 1; + // Wheter or not this volume is outside print volume. + bool is_outside : 1; + // Wheter or not this volume has been generated from a modifier + bool is_modifier : 1; + // Wheter or not this volume has been generated from the wipe tower + bool is_wipe_tower : 1; + // Wheter or not this volume has been generated from an extrusion path + bool is_extrusion_path : 1; + // Whether or not always use the volume's own color (not using SELECTED/HOVER/DISABLED/OUTSIDE) + bool force_native_color : 1; + // Whether or not render this volume in neutral + bool force_neutral_color : 1; + // Whether or not to force rendering of sinking contours + bool force_sinking_contours : 1; + }; + + // Is mouse or rectangle selection over this object to select/deselect it ? + EHoverState hover; + +#if ENABLE_LEGACY_OPENGL_REMOVAL + GUI::GLModel model; +#else + // Interleaved triangles & normals with indexed triangles & quads. + GLIndexedVertexArray indexed_vertex_array; +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + // Ranges of triangle and quad indices to be rendered. + std::pair tverts_range; +#if !ENABLE_LEGACY_OPENGL_REMOVAL + std::pair qverts_range; +#endif // !ENABLE_LEGACY_OPENGL_REMOVAL + + // If the qverts or tverts contain thick extrusions, then offsets keeps pointers of the starts + // of the extrusions per layer. + std::vector print_zs; + // Offset into qverts & tverts, or offsets into indices stored into an OpenGL name_index_buffer. + std::vector offsets; + + // Bounding box of this volume, in unscaled coordinates. + BoundingBoxf3 bounding_box() const { +#if ENABLE_LEGACY_OPENGL_REMOVAL + return this->model.get_bounding_box(); +#else + BoundingBoxf3 out; + if (!this->indexed_vertex_array.bounding_box().isEmpty()) { + out.min = this->indexed_vertex_array.bounding_box().min().cast(); + out.max = this->indexed_vertex_array.bounding_box().max().cast(); + out.defined = true; + } + return out; +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + } + + void set_color(const ColorRGBA& rgba) { color = rgba; } + void set_render_color(const ColorRGBA& rgba) { render_color = rgba; } + // Sets render color in dependence of current state + void set_render_color(bool force_transparent); + // set color according to model volume + void set_color_from_model_volume(const ModelVolume& model_volume); + + const Geometry::Transformation& get_instance_transformation() const { return m_instance_transformation; } + void set_instance_transformation(const Geometry::Transformation& transformation) { m_instance_transformation = transformation; set_bounding_boxes_as_dirty(); } +#if ENABLE_WORLD_COORDINATE + void set_instance_transformation(const Transform3d& transform) { m_instance_transformation.set_matrix(transform); set_bounding_boxes_as_dirty(); } + + Vec3d get_instance_offset() const { return m_instance_transformation.get_offset(); } +#else + const Vec3d& get_instance_offset() const { return m_instance_transformation.get_offset(); } +#endif // ENABLE_WORLD_COORDINATE + double get_instance_offset(Axis axis) const { return m_instance_transformation.get_offset(axis); } + + void set_instance_offset(const Vec3d& offset) { m_instance_transformation.set_offset(offset); set_bounding_boxes_as_dirty(); } + void set_instance_offset(Axis axis, double offset) { m_instance_transformation.set_offset(axis, offset); set_bounding_boxes_as_dirty(); } + +#if ENABLE_WORLD_COORDINATE + Vec3d get_instance_rotation() const { return m_instance_transformation.get_rotation(); } +#else + const Vec3d& get_instance_rotation() const { return m_instance_transformation.get_rotation(); } +#endif // ENABLE_WORLD_COORDINATE + double get_instance_rotation(Axis axis) const { return m_instance_transformation.get_rotation(axis); } + + void set_instance_rotation(const Vec3d& rotation) { m_instance_transformation.set_rotation(rotation); set_bounding_boxes_as_dirty(); } + void set_instance_rotation(Axis axis, double rotation) { m_instance_transformation.set_rotation(axis, rotation); set_bounding_boxes_as_dirty(); } + + Vec3d get_instance_scaling_factor() const { return m_instance_transformation.get_scaling_factor(); } + double get_instance_scaling_factor(Axis axis) const { return m_instance_transformation.get_scaling_factor(axis); } + + void set_instance_scaling_factor(const Vec3d& scaling_factor) { m_instance_transformation.set_scaling_factor(scaling_factor); set_bounding_boxes_as_dirty(); } + void set_instance_scaling_factor(Axis axis, double scaling_factor) { m_instance_transformation.set_scaling_factor(axis, scaling_factor); set_bounding_boxes_as_dirty(); } + +#if ENABLE_WORLD_COORDINATE + Vec3d get_instance_mirror() const { return m_instance_transformation.get_mirror(); } +#else + const Vec3d& get_instance_mirror() const { return m_instance_transformation.get_mirror(); } +#endif // ENABLE_WORLD_COORDINATE + double get_instance_mirror(Axis axis) const { return m_instance_transformation.get_mirror(axis); } + + void set_instance_mirror(const Vec3d& mirror) { m_instance_transformation.set_mirror(mirror); set_bounding_boxes_as_dirty(); } + void set_instance_mirror(Axis axis, double mirror) { m_instance_transformation.set_mirror(axis, mirror); set_bounding_boxes_as_dirty(); } + + const Geometry::Transformation& get_volume_transformation() const { return m_volume_transformation; } + void set_volume_transformation(const Geometry::Transformation& transformation) { m_volume_transformation = transformation; set_bounding_boxes_as_dirty(); } +#if ENABLE_WORLD_COORDINATE + void set_volume_transformation(const Transform3d& transform) { m_volume_transformation.set_matrix(transform); set_bounding_boxes_as_dirty(); } + + Vec3d get_volume_offset() const { return m_volume_transformation.get_offset(); } +#else + const Vec3d& get_volume_offset() const { return m_volume_transformation.get_offset(); } +#endif // ENABLE_WORLD_COORDINATE + double get_volume_offset(Axis axis) const { return m_volume_transformation.get_offset(axis); } + + void set_volume_offset(const Vec3d& offset) { m_volume_transformation.set_offset(offset); set_bounding_boxes_as_dirty(); } + void set_volume_offset(Axis axis, double offset) { m_volume_transformation.set_offset(axis, offset); set_bounding_boxes_as_dirty(); } + +#if ENABLE_WORLD_COORDINATE + Vec3d get_volume_rotation() const { return m_volume_transformation.get_rotation(); } +#else + const Vec3d& get_volume_rotation() const { return m_volume_transformation.get_rotation(); } +#endif // ENABLE_WORLD_COORDINATE + double get_volume_rotation(Axis axis) const { return m_volume_transformation.get_rotation(axis); } + + void set_volume_rotation(const Vec3d& rotation) { m_volume_transformation.set_rotation(rotation); set_bounding_boxes_as_dirty(); } + void set_volume_rotation(Axis axis, double rotation) { m_volume_transformation.set_rotation(axis, rotation); set_bounding_boxes_as_dirty(); } + +#if ENABLE_WORLD_COORDINATE + Vec3d get_volume_scaling_factor() const { return m_volume_transformation.get_scaling_factor(); } +#else + const Vec3d& get_volume_scaling_factor() const { return m_volume_transformation.get_scaling_factor(); } +#endif // ENABLE_WORLD_COORDINATE + double get_volume_scaling_factor(Axis axis) const { return m_volume_transformation.get_scaling_factor(axis); } + + void set_volume_scaling_factor(const Vec3d& scaling_factor) { m_volume_transformation.set_scaling_factor(scaling_factor); set_bounding_boxes_as_dirty(); } + void set_volume_scaling_factor(Axis axis, double scaling_factor) { m_volume_transformation.set_scaling_factor(axis, scaling_factor); set_bounding_boxes_as_dirty(); } + +#if ENABLE_WORLD_COORDINATE + Vec3d get_volume_mirror() const { return m_volume_transformation.get_mirror(); } +#else + const Vec3d& get_volume_mirror() const { return m_volume_transformation.get_mirror(); } +#endif // ENABLE_WORLD_COORDINATE + double get_volume_mirror(Axis axis) const { return m_volume_transformation.get_mirror(axis); } + + void set_volume_mirror(const Vec3d& mirror) { m_volume_transformation.set_mirror(mirror); set_bounding_boxes_as_dirty(); } + void set_volume_mirror(Axis axis, double mirror) { m_volume_transformation.set_mirror(axis, mirror); set_bounding_boxes_as_dirty(); } + + double get_sla_shift_z() const { return m_sla_shift_z; } + void set_sla_shift_z(double z) { m_sla_shift_z = z; } + + void set_convex_hull(std::shared_ptr convex_hull) { m_convex_hull = std::move(convex_hull); } + void set_convex_hull(const TriangleMesh &convex_hull) { m_convex_hull = std::make_shared(convex_hull); } + void set_convex_hull(TriangleMesh &&convex_hull) { m_convex_hull = std::make_shared(std::move(convex_hull)); } + + int object_idx() const { return this->composite_id.object_id; } + int volume_idx() const { return this->composite_id.volume_id; } + int instance_idx() const { return this->composite_id.instance_id; } + + Transform3d world_matrix() const; + bool is_left_handed() const; + + const BoundingBoxf3& transformed_bounding_box() const; + // non-caching variant + BoundingBoxf3 transformed_convex_hull_bounding_box(const Transform3d &trafo) const; + // caching variant + const BoundingBoxf3& transformed_convex_hull_bounding_box() const; + // non-caching variant + BoundingBoxf3 transformed_non_sinking_bounding_box(const Transform3d& trafo) const; + // caching variant + const BoundingBoxf3& transformed_non_sinking_bounding_box() const; + // convex hull + const TriangleMesh* convex_hull() const { return m_convex_hull.get(); } + +#if ENABLE_LEGACY_OPENGL_REMOVAL + bool empty() const { return this->model.is_empty(); } +#else + bool empty() const { return this->indexed_vertex_array.empty(); } +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + + void set_range(double low, double high); + + void render(); + +#if !ENABLE_LEGACY_OPENGL_REMOVAL + void finalize_geometry(bool opengl_initialized) { this->indexed_vertex_array.finalize_geometry(opengl_initialized); } + void release_geometry() { this->indexed_vertex_array.release_geometry(); } +#endif // !ENABLE_LEGACY_OPENGL_REMOVAL + + void set_bounding_boxes_as_dirty() { + m_transformed_bounding_box.reset(); + m_transformed_convex_hull_bounding_box.reset(); + m_transformed_non_sinking_bounding_box.reset(); + } + + bool is_sla_support() const; + bool is_sla_pad() const; + + bool is_sinking() const; + bool is_below_printbed() const; + void render_sinking_contours(); +#if ENABLE_SHOW_NON_MANIFOLD_EDGES + void render_non_manifold_edges(); +#endif // ENABLE_SHOW_NON_MANIFOLD_EDGES + + // Return an estimate of the memory consumed by this class. + size_t cpu_memory_used() const { +#if ENABLE_LEGACY_OPENGL_REMOVAL + return sizeof(*this) + this->model.cpu_memory_used() + this->print_zs.capacity() * sizeof(coordf_t) + + this->offsets.capacity() * sizeof(size_t); + } + // Return an estimate of the memory held by GPU vertex buffers. + size_t gpu_memory_used() const { return this->model.gpu_memory_used(); } +#else + //FIXME what to do wih m_convex_hull? + return sizeof(*this) - sizeof(this->indexed_vertex_array) + this->indexed_vertex_array.cpu_memory_used() + this->print_zs.capacity() * sizeof(coordf_t) + this->offsets.capacity() * sizeof(size_t); + } + // Return an estimate of the memory held by GPU vertex buffers. + size_t gpu_memory_used() const { return this->indexed_vertex_array.gpu_memory_used(); } +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + size_t total_memory_used() const { return this->cpu_memory_used() + this->gpu_memory_used(); } +}; + +typedef std::vector GLVolumePtrs; +typedef std::pair> GLVolumeWithIdAndZ; +typedef std::vector GLVolumeWithIdAndZList; + +class GLVolumeCollection +{ +public: + enum class ERenderType : unsigned char + { + Opaque, + Transparent, + All + }; + + struct PrintVolume + { + // see: Bed3D::EShapeType + int type{ 0 }; + // data contains: + // Rectangle: + // [0] = min.x, [1] = min.y, [2] = max.x, [3] = max.y + // Circle: + // [0] = center.x, [1] = center.y, [3] = radius + std::array data; + // [0] = min z, [1] = max z + std::array zs; + }; + +private: + PrintVolume m_print_volume; + + // z range for clipping in shaders + std::array m_z_range; + + // plane coeffs for clipping in shaders + std::array m_clipping_plane; + + struct Slope + { + // toggle for slope rendering + bool active{ false }; + float normal_z; + }; + + Slope m_slope; + bool m_show_sinking_contours{ false }; +#if ENABLE_SHOW_NON_MANIFOLD_EDGES + bool m_show_non_manifold_edges{ true }; +#endif // ENABLE_SHOW_NON_MANIFOLD_EDGES + +public: + GLVolumePtrs volumes; + + GLVolumeCollection() { set_default_slope_normal_z(); } + ~GLVolumeCollection() { clear(); } + +#if ENABLE_LEGACY_OPENGL_REMOVAL + std::vector load_object( + const ModelObject* model_object, + int obj_idx, + const std::vector& instance_idxs); + + int load_object_volume( + const ModelObject* model_object, + int obj_idx, + int volume_idx, + int instance_idx); + + // Load SLA auxiliary GLVolumes (for support trees or pad). + void load_object_auxiliary( + const SLAPrintObject* print_object, + int obj_idx, + // pairs of + const std::vector>& instances, + SLAPrintObjectStep milestone, + // Timestamp of the last change of the milestone + size_t timestamp); + +#if ENABLE_WIPETOWER_OBJECTID_1000_REMOVAL + int load_wipe_tower_preview( + float pos_x, float pos_y, float width, float depth, float height, float rotation_angle, bool size_unknown, float brim_width); +#else + int load_wipe_tower_preview( + int obj_idx, float pos_x, float pos_y, float width, float depth, float height, float rotation_angle, bool size_unknown, float brim_width); +#endif // ENABLE_WIPETOWER_OBJECTID_1000_REMOVAL +#else + std::vector load_object( + const ModelObject *model_object, + int obj_idx, + const std::vector &instance_idxs, + bool opengl_initialized); + + int load_object_volume( + const ModelObject *model_object, + int obj_idx, + int volume_idx, + int instance_idx, + bool opengl_initialized); + + // Load SLA auxiliary GLVolumes (for support trees or pad). + void load_object_auxiliary( + const SLAPrintObject *print_object, + int obj_idx, + // pairs of + const std::vector>& instances, + SLAPrintObjectStep milestone, + // Timestamp of the last change of the milestone + size_t timestamp, + bool opengl_initialized); + +#if ENABLE_WIPETOWER_OBJECTID_1000_REMOVAL + int load_wipe_tower_preview( + float pos_x, float pos_y, float width, float depth, float height, float rotation_angle, bool size_unknown, float brim_width, bool opengl_initialized); +#else + int load_wipe_tower_preview( + int obj_idx, float pos_x, float pos_y, float width, float depth, float height, float rotation_angle, bool size_unknown, float brim_width, bool opengl_initialized); +#endif // ENABLE_WIPETOWER_OBJECTID_1000_REMOVAL +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + +#if ENABLE_LEGACY_OPENGL_REMOVAL + GLVolume* new_toolpath_volume(const ColorRGBA& rgba); + GLVolume* new_nontoolpath_volume(const ColorRGBA& rgba); +#else + GLVolume* new_toolpath_volume(const ColorRGBA& rgba, size_t reserve_vbo_floats = 0); + GLVolume* new_nontoolpath_volume(const ColorRGBA& rgba, size_t reserve_vbo_floats = 0); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + + // Render the volumes by OpenGL. +#if ENABLE_GL_SHADERS_ATTRIBUTES + void render(ERenderType type, bool disable_cullface, const Transform3d& view_matrix, const Transform3d& projection_matrix, + std::function filter_func = std::function()) const; +#else + void render(ERenderType type, bool disable_cullface, const Transform3d& view_matrix, std::function filter_func = std::function()) const; +#endif // ENABLE_GL_SHADERS_ATTRIBUTES + +#if !ENABLE_LEGACY_OPENGL_REMOVAL + // 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(bool opengl_initialized) { for (auto* v : volumes) v->finalize_geometry(opengl_initialized); } + // Release the geometry data assigned to the volumes. + // If OpenGL VBOs were allocated, an OpenGL context has to be active to release them. + void release_geometry() { for (auto *v : volumes) v->release_geometry(); } +#endif // !ENABLE_LEGACY_OPENGL_REMOVAL + // Clear the geometry + void clear() { for (auto *v : volumes) delete v; volumes.clear(); } + + bool empty() const { return volumes.empty(); } + void set_range(double low, double high) { for (GLVolume* vol : this->volumes) vol->set_range(low, high); } + + void set_print_volume(const PrintVolume& print_volume) { m_print_volume = print_volume; } + + void set_z_range(float min_z, float max_z) { m_z_range[0] = min_z; m_z_range[1] = max_z; } + void set_clipping_plane(const std::array& coeffs) { m_clipping_plane = coeffs; } + + const std::array& get_z_range() const { return m_z_range; } + const std::array& get_clipping_plane() const { return m_clipping_plane; } + + bool is_slope_active() const { return m_slope.active; } + void set_slope_active(bool active) { m_slope.active = active; } + + float get_slope_normal_z() const { return m_slope.normal_z; } + void set_slope_normal_z(float normal_z) { m_slope.normal_z = normal_z; } + void set_default_slope_normal_z() { m_slope.normal_z = -::cos(Geometry::deg2rad(90.0f - 45.0f)); } + void set_show_sinking_contours(bool show) { m_show_sinking_contours = show; } +#if ENABLE_SHOW_NON_MANIFOLD_EDGES + void set_show_non_manifold_edges(bool show) { m_show_non_manifold_edges = show; } +#endif // ENABLE_SHOW_NON_MANIFOLD_EDGES + + // returns true if all the volumes are completely contained in the print volume + // returns the containment state in the given out_state, if non-null + bool check_outside_state(const Slic3r::BuildVolume& build_volume, ModelInstanceEPrintVolumeState* out_state) const; + void reset_outside_state(); + + void update_colors_by_extruder(const DynamicPrintConfig* config); + + // Returns a vector containing the sorted list of all the print_zs of the volumes contained in this collection + std::vector get_current_print_zs(bool active_only) const; + + // Return an estimate of the memory consumed by this class. + size_t cpu_memory_used() const; + // Return an estimate of the memory held by GPU vertex buffers. + size_t gpu_memory_used() const; + size_t total_memory_used() const { return this->cpu_memory_used() + this->gpu_memory_used(); } + // Return CPU, GPU and total memory log line. + std::string log_memory_info() const; + +private: + GLVolumeCollection(const GLVolumeCollection &other); + GLVolumeCollection& operator=(const GLVolumeCollection &); +}; + +GLVolumeWithIdAndZList volumes_to_render(const GLVolumePtrs& volumes, GLVolumeCollection::ERenderType type, const Transform3d& view_matrix, std::function filter_func = nullptr); + +struct _3DScene +{ +#if ENABLE_LEGACY_OPENGL_REMOVAL + static void thick_lines_to_verts(const Lines& lines, const std::vector& widths, const std::vector& heights, bool closed, double top_z, GUI::GLModel::Geometry& geometry); + static void thick_lines_to_verts(const Lines3& lines, const std::vector& widths, const std::vector& heights, bool closed, GUI::GLModel::Geometry& geometry); + static void extrusionentity_to_verts(const ExtrusionPath& extrusion_path, float print_z, const Point& copy, GUI::GLModel::Geometry& geometry); + static void extrusionentity_to_verts(const ExtrusionLoop& extrusion_loop, float print_z, const Point& copy, GUI::GLModel::Geometry& geometry); + static void extrusionentity_to_verts(const ExtrusionMultiPath& extrusion_multi_path, float print_z, const Point& copy, GUI::GLModel::Geometry& geometry); + static void extrusionentity_to_verts(const ExtrusionEntityCollection& extrusion_entity_collection, float print_z, const Point& copy, GUI::GLModel::Geometry& geometry); + static void extrusionentity_to_verts(const ExtrusionEntity* extrusion_entity, float print_z, const Point& copy, GUI::GLModel::Geometry& geometry); +#else + static void thick_lines_to_verts(const Lines& lines, const std::vector& widths, const std::vector& heights, bool closed, double top_z, GLVolume& volume); + static void thick_lines_to_verts(const Lines3& lines, const std::vector& widths, const std::vector& heights, bool closed, GLVolume& volume); + static void extrusionentity_to_verts(const Polyline& polyline, float width, float height, float print_z, GLVolume& volume); + static void extrusionentity_to_verts(const ExtrusionPath& extrusion_path, float print_z, GLVolume& volume); + static void extrusionentity_to_verts(const ExtrusionPath& extrusion_path, float print_z, const Point& copy, GLVolume& volume); + static void extrusionentity_to_verts(const ExtrusionLoop& extrusion_loop, float print_z, const Point& copy, GLVolume& volume); + static void extrusionentity_to_verts(const ExtrusionMultiPath& extrusion_multi_path, float print_z, const Point& copy, GLVolume& volume); + static void extrusionentity_to_verts(const ExtrusionEntityCollection& extrusion_entity_collection, float print_z, const Point& copy, GLVolume& volume); + static void extrusionentity_to_verts(const ExtrusionEntity* extrusion_entity, float print_z, const Point& copy, GLVolume& volume); + static void polyline3_to_verts(const Polyline3& polyline, double width, double height, GLVolume& volume); + static void point3_to_verts(const Vec3crd& point, double width, double height, GLVolume& volume); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL +}; + +} + +#endif diff --git a/src/slic3r/GUI/AboutDialog.cpp b/src/slic3r/GUI/AboutDialog.cpp index e444fb03c..abd137941 100644 --- a/src/slic3r/GUI/AboutDialog.cpp +++ b/src/slic3r/GUI/AboutDialog.cpp @@ -221,8 +221,7 @@ AboutDialog::AboutDialog() main_sizer->Add(hsizer, 0, wxEXPAND | wxALL, 20); // logo - m_logo_bitmap = ScalableBitmap(this, wxGetApp().logo_name(), 192); - m_logo = new wxStaticBitmap(this, wxID_ANY, m_logo_bitmap.bmp()); + m_logo = new wxStaticBitmap(this, wxID_ANY, *get_bmp_bundle(wxGetApp().logo_name(), 192)); hsizer->Add(m_logo, 1, wxALIGN_CENTER_VERTICAL); wxBoxSizer* vsizer = new wxBoxSizer(wxVERTICAL); @@ -322,8 +321,8 @@ AboutDialog::AboutDialog() void AboutDialog::on_dpi_changed(const wxRect &suggested_rect) { - m_logo_bitmap.msw_rescale(); - m_logo->SetBitmap(m_logo_bitmap.bmp()); +// m_logo_bitmap.msw_rescale(); +// m_logo->SetBitmap(m_logo_bitmap.bmp()); const wxFont& font = GetFont(); const int fs = font.GetPointSize() - 1; diff --git a/src/slic3r/GUI/BitmapCache.cpp b/src/slic3r/GUI/BitmapCache.cpp index b585798d3..147615277 100644 --- a/src/slic3r/GUI/BitmapCache.cpp +++ b/src/slic3r/GUI/BitmapCache.cpp @@ -15,10 +15,8 @@ #include #endif /* __WXGTK2__ */ -//#define NANOSVG_IMPLEMENTATION -#include "nanosvg/nanosvg.h" -#define NANOSVGRAST_IMPLEMENTATION -#include "nanosvg/nanosvgrast.h" +#include +#include namespace Slic3r { namespace GUI { @@ -62,7 +60,168 @@ static wxBitmap wxImage_to_wxBitmap_with_alpha(wxImage &&image, float scale = 1. #endif } -wxBitmap* BitmapCache::insert(const std::string &bitmap_key, size_t width, size_t height) +wxBitmapBundle* BitmapCache::insert_bndl(const std::string& name, const std::vector& bmps) +{ + wxVector bitmaps; + + std::set scales = {1.0}; +#ifndef __linux__ + +#ifdef __APPLE__ + scales.emplace(m_scale); +#else + size_t disp_cnt = wxDisplay::GetCount(); + for (size_t disp = 0; disp < disp_cnt; ++disp) + scales.emplace(wxDisplay(disp).GetScaleFactor()); +#endif + +#endif // !__linux__ + + for (double scale : scales) { + size_t width = 0; + size_t height = 0; + for (const wxBitmapBundle* bmp_bndl : bmps) { +#ifdef __APPLE__ + wxSize size = bmp_bndl->GetDefaultSize(); +#else + wxSize size = bmp_bndl->GetPreferredBitmapSizeAtScale(scale); +#endif + width += size.GetWidth(); + height = std::max(height, size.GetHeight()); + } + + std::string bitmap_key = name + "," +float_to_string_decimal_point(scale); + +#ifdef __WXGTK2__ + // Broken alpha workaround + wxImage image(width, height); + image.InitAlpha(); + // Fill in with a white color. + memset(image.GetData(), 0x0ff, width * height * 3); + // Fill in with full transparency. + memset(image.GetAlpha(), 0, width * height); + size_t x = 0; + for (const wxBitmapBundle* bmp_bndl : bmps) { + wxBitmap bmp = bmp_bndl->GetBitmap(bmp_bndl->GetDefaultSize()); + if (bmp.GetWidth() > 0) { + if (bmp.GetDepth() == 32) { + wxAlphaPixelData data(bmp); + //FIXME The following method is missing from wxWidgets 3.1.1. + // It looks like the wxWidgets 3.0.3 called the wrapped bitmap's UseAlpha(). + //data.UseAlpha(); + if (data) { + for (int r = 0; r < bmp.GetHeight(); ++r) { + wxAlphaPixelData::Iterator src(data); + src.Offset(data, 0, r); + unsigned char* dst_pixels = image.GetData() + (x + r * width) * 3; + unsigned char* dst_alpha = image.GetAlpha() + x + r * width; + for (int c = 0; c < bmp.GetWidth(); ++c, ++src) { + *dst_pixels++ = src.Red(); + *dst_pixels++ = src.Green(); + *dst_pixels++ = src.Blue(); + *dst_alpha++ = src.Alpha(); + } + } + } + } + else if (bmp.GetDepth() == 24) { + wxNativePixelData data(bmp); + if (data) { + for (int r = 0; r < bmp.GetHeight(); ++r) { + wxNativePixelData::Iterator src(data); + src.Offset(data, 0, r); + unsigned char* dst_pixels = image.GetData() + (x + r * width) * 3; + unsigned char* dst_alpha = image.GetAlpha() + x + r * width; + for (int c = 0; c < bmp.GetWidth(); ++c, ++src) { + *dst_pixels++ = src.Red(); + *dst_pixels++ = src.Green(); + *dst_pixels++ = src.Blue(); + *dst_alpha++ = wxALPHA_OPAQUE; + } + } + } + } + } + x += bmp.GetScaledWidth(); + } + + bitmaps.push_back(* this->insert(bitmap_key, wxImage_to_wxBitmap_with_alpha(std::move(image)))); + +#else + + wxBitmap* bitmap = this->insert(bitmap_key, width, height, scale); + wxMemoryDC memDC; + memDC.SelectObject(*bitmap); + memDC.SetBackground(*wxTRANSPARENT_BRUSH); + memDC.Clear(); + size_t x = 0; + for (const wxBitmapBundle* bmp_bndl : bmps) { + wxBitmap bmp = bmp_bndl->GetBitmap(bmp_bndl->GetPreferredBitmapSizeAtScale(scale)); + + if (bmp.GetWidth() > 0) + memDC.DrawBitmap(bmp, x, 0, true); + // we should "move" with step equal to non-scaled width +#ifdef __APPLE__ + x += bmp.GetScaledWidth(); +#else + x += bmp.GetWidth(); +#endif + } + memDC.SelectObject(wxNullBitmap); + bitmaps.push_back(*bitmap); + +#endif + } + + return insert_bndl(name, bitmaps); +} + +wxBitmapBundle* BitmapCache::insert_bndl(const std::string &bitmap_key, const char* data, size_t width, size_t height) +{ + wxBitmapBundle* bndl = nullptr; + auto it = m_bndl_map.find(bitmap_key); + if (it == m_bndl_map.end()) { + bndl = new wxBitmapBundle(wxBitmapBundle::FromSVG(data, wxSize(width, height))); + m_bndl_map[bitmap_key] = bndl; + } + else { + bndl = it->second; + *bndl = wxBitmapBundle::FromSVG(data, wxSize(width, height)); + } + return bndl; +} + +wxBitmapBundle* BitmapCache::insert_bndl(const std::string& bitmap_key, const wxBitmapBundle& bmp) +{ + wxBitmapBundle* bndl = nullptr; + auto it = m_bndl_map.find(bitmap_key); + if (it == m_bndl_map.end()) { + bndl = new wxBitmapBundle(bmp); + m_bndl_map[bitmap_key] = bndl; + } + else { + bndl = it->second; + *bndl = wxBitmapBundle(bmp); + } + return bndl; +} + +wxBitmapBundle* BitmapCache::insert_bndl(const std::string& bitmap_key, const wxVector& bmps) +{ + wxBitmapBundle* bndl = nullptr; + auto it = m_bndl_map.find(bitmap_key); + if (it == m_bndl_map.end()) { + bndl = new wxBitmapBundle(wxBitmapBundle::FromBitmaps(bmps)); + m_bndl_map[bitmap_key] = bndl; + } + else { + bndl = it->second; + *bndl = wxBitmapBundle::FromBitmaps(bmps); + } + return bndl; +} + +wxBitmap* BitmapCache::insert(const std::string &bitmap_key, size_t width, size_t height, double scale/* = -1.0*/) { wxBitmap *bitmap = nullptr; auto it = m_map.find(bitmap_key); @@ -78,7 +237,7 @@ wxBitmap* BitmapCache::insert(const std::string &bitmap_key, size_t width, size_ // So, We need to let the Mac OS wxBitmap implementation // know that the image may already be scaled appropriately for Retina, // and thereby that it's not supposed to upscale it. - bitmap->CreateScaled(width, height, -1, m_scale); + bitmap->CreateScaled(width, height, -1, scale < 0.0 ? m_scale : scale); #endif m_map[bitmap_key] = bitmap; } else { @@ -107,110 +266,6 @@ wxBitmap* BitmapCache::insert(const std::string &bitmap_key, const wxBitmap &bmp return bitmap; } -wxBitmap* BitmapCache::insert(const std::string &bitmap_key, const wxBitmap &bmp, const wxBitmap &bmp2) -{ - // Copying the wxBitmaps is cheap as the bitmap's content is reference counted. - const wxBitmap bmps[2] = { bmp, bmp2 }; - return this->insert(bitmap_key, bmps, bmps + 2); -} - -wxBitmap* BitmapCache::insert(const std::string &bitmap_key, const wxBitmap &bmp, const wxBitmap &bmp2, const wxBitmap &bmp3) -{ - // Copying the wxBitmaps is cheap as the bitmap's content is reference counted. - const wxBitmap bmps[3] = { bmp, bmp2, bmp3 }; - return this->insert(bitmap_key, bmps, bmps + 3); -} - -wxBitmap* BitmapCache::insert(const std::string &bitmap_key, const wxBitmap *begin, const wxBitmap *end) -{ - size_t width = 0; - size_t height = 0; - for (const wxBitmap *bmp = begin; bmp != end; ++ bmp) { -#ifdef __APPLE__ - width += bmp->GetScaledWidth(); - height = std::max(height, bmp->GetScaledHeight()); -#else - width += bmp->GetWidth(); - height = std::max(height, bmp->GetHeight()); -#endif - } - -#ifdef __WXGTK2__ - // Broken alpha workaround - wxImage image(width, height); - image.InitAlpha(); - // Fill in with a white color. - memset(image.GetData(), 0x0ff, width * height * 3); - // Fill in with full transparency. - memset(image.GetAlpha(), 0, width * height); - size_t x = 0; - for (const wxBitmap *bmp = begin; bmp != end; ++ bmp) { - if (bmp->GetWidth() > 0) { - if (bmp->GetDepth() == 32) { - wxAlphaPixelData data(*const_cast(bmp)); - //FIXME The following method is missing from wxWidgets 3.1.1. - // It looks like the wxWidgets 3.0.3 called the wrapped bitmap's UseAlpha(). - //data.UseAlpha(); - if (data) { - for (int r = 0; r < bmp->GetHeight(); ++ r) { - wxAlphaPixelData::Iterator src(data); - src.Offset(data, 0, r); - unsigned char *dst_pixels = image.GetData() + (x + r * width) * 3; - unsigned char *dst_alpha = image.GetAlpha() + x + r * width; - for (int c = 0; c < bmp->GetWidth(); ++ c, ++ src) { - *dst_pixels ++ = src.Red(); - *dst_pixels ++ = src.Green(); - *dst_pixels ++ = src.Blue(); - *dst_alpha ++ = src.Alpha(); - } - } - } - } else if (bmp->GetDepth() == 24) { - wxNativePixelData data(*const_cast(bmp)); - if (data) { - for (int r = 0; r < bmp->GetHeight(); ++ r) { - wxNativePixelData::Iterator src(data); - src.Offset(data, 0, r); - unsigned char *dst_pixels = image.GetData() + (x + r * width) * 3; - unsigned char *dst_alpha = image.GetAlpha() + x + r * width; - for (int c = 0; c < bmp->GetWidth(); ++ c, ++ src) { - *dst_pixels ++ = src.Red(); - *dst_pixels ++ = src.Green(); - *dst_pixels ++ = src.Blue(); - *dst_alpha ++ = wxALPHA_OPAQUE; - } - } - } - } - } - x += bmp->GetWidth(); - } - return this->insert(bitmap_key, wxImage_to_wxBitmap_with_alpha(std::move(image))); - -#else - - wxBitmap *bitmap = this->insert(bitmap_key, width, height); - wxMemoryDC memDC; - memDC.SelectObject(*bitmap); - memDC.SetBackground(*wxTRANSPARENT_BRUSH); - memDC.Clear(); - size_t x = 0; - for (const wxBitmap *bmp = begin; bmp != end; ++ bmp) { - if (bmp->GetWidth() > 0) - memDC.DrawBitmap(*bmp, x, 0, true); -#ifdef __APPLE__ - // we should "move" with step equal to non-scaled width - x += bmp->GetScaledWidth(); -#else - x += bmp->GetWidth(); -#endif - } - memDC.SelectObject(wxNullBitmap); - return bitmap; - -#endif -} - wxBitmap* BitmapCache::insert_raw_rgba(const std::string &bitmap_key, unsigned width, unsigned height, const unsigned char *raw_data, const bool grayscale/* = false*/) { wxImage image(width, height); @@ -299,6 +354,92 @@ error: return NULL; } +void BitmapCache::nsvgGetDataFromFileWithReplace(const char* filename, std::string& data_str, const std::map& replaces) +{ + FILE* fp = NULL; + size_t size; + char* data = NULL; + + fp = boost::nowide::fopen(filename, "rb"); + if (!fp) goto error; + fseek(fp, 0, SEEK_END); + size = ftell(fp); + fseek(fp, 0, SEEK_SET); + data = (char*)malloc(size + 1); + if (data == NULL) goto error; + if (fread(data, 1, size, fp) != size) goto error; + data[size] = '\0'; // Must be null terminated. + fclose(fp); + + data_str.assign(data); + for (auto val : replaces) + boost::replace_all(data_str, val.first, val.second); + + free(data); + return; + +error: + if (fp) fclose(fp); + if (data) free(data); + return; +} + +wxBitmapBundle* BitmapCache::from_svg(const std::string& bitmap_name, unsigned target_width, unsigned target_height, + const bool dark_mode, const std::string& new_color /*= ""*/) +{ + if (target_width == 0) + target_width = target_height; + std::string bitmap_key = bitmap_name + (target_height != 0 ? + "-h" + std::to_string(target_height) : + "-w" + std::to_string(target_width)) + + (dark_mode ? "-dm" : "") + + new_color; + + auto it = m_bndl_map.find(bitmap_key); + if (it != m_bndl_map.end()) + return it->second; + + // map of color replaces + std::map replaces; + if (dark_mode) + replaces["\"#808080\""] = "\"#FFFFFF\""; + if (!new_color.empty()) + replaces["\"#ED6B21\""] = "\"" + new_color + "\""; + + std::string str; + nsvgGetDataFromFileWithReplace(Slic3r::var(bitmap_name + ".svg").c_str(), str, replaces); + if (str.empty()) + return nullptr; + + return insert_bndl(bitmap_key, str.data(), target_width, target_height); +} + +wxBitmapBundle* BitmapCache::from_png(const std::string& bitmap_name, unsigned width, unsigned height) +{ + std::string bitmap_key = bitmap_name + (height != 0 ? + "-h" + std::to_string(height) : + "-w" + std::to_string(width)); + + auto it = m_bndl_map.find(bitmap_key); + if (it != m_bndl_map.end()) + return it->second; + + wxImage image; + if (!image.LoadFile(Slic3r::GUI::from_u8(Slic3r::var(bitmap_name + ".png")), wxBITMAP_TYPE_PNG) || + image.GetWidth() == 0 || image.GetHeight() == 0) + return nullptr; + + if (height != 0 && unsigned(image.GetHeight()) != height) + width = unsigned(0.5f + float(image.GetWidth()) * height / image.GetHeight()); + else if (width != 0 && unsigned(image.GetWidth()) != width) + height = unsigned(0.5f + float(image.GetHeight()) * width / image.GetWidth()); + + if (height != 0 && width != 0) + image.Rescale(width, height, wxIMAGE_QUALITY_BILINEAR); + + return this->insert_bndl(bitmap_key, wxImage_to_wxBitmap_with_alpha(std::move(image))); +} + wxBitmap* BitmapCache::load_svg(const std::string &bitmap_name, unsigned target_width, unsigned target_height, const bool grayscale/* = false*/, const bool dark_mode/* = false*/, const std::string& new_color /*= ""*/) { @@ -352,9 +493,9 @@ wxBitmap* BitmapCache::load_svg(const std::string &bitmap_name, unsigned target_ return this->insert_raw_rgba(bitmap_key, width, height, data.data(), grayscale); } - +/* //we make scaled solid bitmaps only for the cases, when its will be used with scaled SVG icon in one output bitmap -wxBitmap BitmapCache::mksolid(size_t width, size_t height, unsigned char r, unsigned char g, unsigned char b, unsigned char transparency, bool suppress_scaling/* = false*/, size_t border_width /*= 0*/, bool dark_mode/* = false*/) +wxBitmap BitmapCache::mksolid(size_t width, size_t height, unsigned char r, unsigned char g, unsigned char b, unsigned char transparency, bool suppress_scaling/* = false* /, size_t border_width /*= 0* /, bool dark_mode/* = false* /) { double scale = suppress_scaling ? 1.0f : m_scale; width *= scale; @@ -396,6 +537,89 @@ wxBitmap BitmapCache::mksolid(size_t width, size_t height, unsigned char r, unsi return wxImage_to_wxBitmap_with_alpha(std::move(image), scale); } +*/ +//we make scaled solid bitmaps only for the cases, when its will be used with scaled SVG icon in one output bitmap +wxBitmapBundle BitmapCache::mksolid(size_t width_in, size_t height_in, unsigned char r, unsigned char g, unsigned char b, unsigned char transparency, size_t border_width /*= 0*/, bool dark_mode/* = false*/) +{ + wxVector bitmaps; + + std::set scales = { 1.0 }; +#ifndef __linux__ + +#ifdef __APPLE__ + scales.emplace(m_scale); +#else + size_t disp_cnt = wxDisplay::GetCount(); + for (size_t disp = 0; disp < disp_cnt; ++disp) + scales.emplace(wxDisplay(disp).GetScaleFactor()); +#endif + +#endif // !__linux__ + + for (double scale : scales) { + size_t width = width_in * scale; + size_t height = height_in * scale; + + wxImage image(width, height); + image.InitAlpha(); + unsigned char* imgdata = image.GetData(); + unsigned char* imgalpha = image.GetAlpha(); + for (size_t i = 0; i < width * height; ++i) { + *imgdata++ = r; + *imgdata++ = g; + *imgdata++ = b; + *imgalpha++ = transparency; + } + + // Add border, make white/light spools easier to see + if (border_width > 0) { + + // Restrict to width of image + if (border_width > height) border_width = height - 1; + if (border_width > width) border_width = width - 1; + + auto px_data = (uint8_t*)image.GetData(); + auto a_data = (uint8_t*)image.GetAlpha(); + + for (size_t x = 0; x < width; ++x) { + for (size_t y = 0; y < height; ++y) { + if (x < border_width || y < border_width || + x >= (width - border_width) || y >= (height - border_width)) { + const size_t idx = (x + y * width); + const size_t idx_rgb = (x + y * width) * 3; + px_data[idx_rgb] = px_data[idx_rgb + 1] = px_data[idx_rgb + 2] = dark_mode ? 245u : 110u; + a_data[idx] = 255u; + } + } + } + } + + bitmaps.push_back(wxImage_to_wxBitmap_with_alpha(std::move(image), scale)); + } + return wxBitmapBundle::FromBitmaps(bitmaps); +} + +wxBitmapBundle* BitmapCache::mksolid_bndl(size_t width, size_t height, const std::string& color, size_t border_width, bool dark_mode) +{ + std::string bitmap_key = (color.empty() ? "empty" : color) + "-h" + std::to_string(height) + "-w" + std::to_string(width) + (dark_mode ? "-dm" : ""); + + wxBitmapBundle* bndl = nullptr; + auto it = m_bndl_map.find(bitmap_key); + if (it == m_bndl_map.end()) { + if (color.empty()) + bndl = new wxBitmapBundle(mksolid(width, height, 0, 0, 0, wxALPHA_TRANSPARENT, size_t(0))); + else { + ColorRGB rgb;// [3] ; + decode_color(color, rgb); + bndl = new wxBitmapBundle(mksolid(width, height, rgb.r_uchar(), rgb.g_uchar(), rgb.b_uchar(), wxALPHA_OPAQUE, border_width, dark_mode)); + } + m_bndl_map[bitmap_key] = bndl; + } + else + return it->second; + + return bndl; +} } // namespace GUI } // namespace Slic3r diff --git a/src/slic3r/GUI/BitmapCache.hpp b/src/slic3r/GUI/BitmapCache.hpp index 5af90c5f7..6ba9ae15b 100644 --- a/src/slic3r/GUI/BitmapCache.hpp +++ b/src/slic3r/GUI/BitmapCache.hpp @@ -24,15 +24,19 @@ public: void clear(); double scale() { return m_scale; } + wxBitmapBundle* find_bndl(const std::string &name) { auto it = m_bndl_map.find(name); return (it == m_bndl_map.end()) ? nullptr : it->second; } + const wxBitmapBundle* find_bndl(const std::string &name) const { return const_cast(this)->find_bndl(name); } wxBitmap* find(const std::string &name) { auto it = m_map.find(name); return (it == m_map.end()) ? nullptr : it->second; } const wxBitmap* find(const std::string &name) const { return const_cast(this)->find(name); } - wxBitmap* insert(const std::string &name, size_t width, size_t height); + wxBitmapBundle* insert_bndl(const std::string& bitmap_key, const char* data, size_t width, size_t height); + wxBitmapBundle* insert_bndl(const std::string& bitmap_key, const wxBitmapBundle &bmp); + wxBitmapBundle* insert_bndl(const std::string& bitmap_key, const wxVector& bmps); + wxBitmapBundle* insert_bndl(const std::string& name, const std::vector& bmps); + wxBitmapBundle* insert_raw_rgba_bndl(const std::string &bitmap_key, unsigned width, unsigned height, const unsigned char *raw_data, const bool grayscale = false); + + wxBitmap* insert(const std::string &name, size_t width, size_t height, double scale = -1.0); wxBitmap* insert(const std::string &name, const wxBitmap &bmp); - wxBitmap* insert(const std::string &name, const wxBitmap &bmp, const wxBitmap &bmp2); - wxBitmap* insert(const std::string &name, const wxBitmap &bmp, const wxBitmap &bmp2, const wxBitmap &bmp3); - wxBitmap* insert(const std::string &name, const std::vector &bmps) { return this->insert(name, &bmps.front(), &bmps.front() + bmps.size()); } - wxBitmap* insert(const std::string &name, const wxBitmap *begin, const wxBitmap *end); wxBitmap* insert_raw_rgba(const std::string &bitmap_key, unsigned width, unsigned height, const unsigned char *raw_data, const bool grayscale = false); // Load png from resources/icons. bitmap_key is given without the .png suffix. Bitmap will be rescaled to provided height/width if nonzero. @@ -42,15 +46,21 @@ public: // And makes replases befor parsing // replace_map containes old_value->new_value static NSVGimage* nsvgParseFromFileWithReplace(const char* filename, const char* units, float dpi, const std::map& replaces); + // Gets a data from SVG file and makes replases + // replace_map containes old_value->new_value + static void nsvgGetDataFromFileWithReplace(const char* filename, std::string& data_str, const std::map& replaces); + wxBitmapBundle* from_svg(const std::string& bitmap_name, unsigned target_width, unsigned target_height, const bool dark_mode, const std::string& new_color = ""); + wxBitmapBundle* from_png(const std::string& bitmap_name, unsigned width, unsigned height); // Load svg from resources/icons. bitmap_key is given without the .svg suffix. SVG will be rasterized to provided height/width. wxBitmap* load_svg(const std::string &bitmap_key, unsigned width = 0, unsigned height = 0, const bool grayscale = false, const bool dark_mode = false, const std::string& new_color = ""); - wxBitmap mksolid(size_t width, size_t height, unsigned char r, unsigned char g, unsigned char b, unsigned char transparency, bool suppress_scaling = false, size_t border_width = 0, bool dark_mode = false); - wxBitmap mksolid(size_t width, size_t height, const ColorRGB& rgb, bool suppress_scaling = false, size_t border_width = 0, bool dark_mode = false) { return mksolid(width, height, rgb.r_uchar(), rgb.g_uchar(), rgb.b_uchar(), wxALPHA_OPAQUE, suppress_scaling, border_width, dark_mode); } - wxBitmap mkclear(size_t width, size_t height) { return mksolid(width, height, 0, 0, 0, wxALPHA_TRANSPARENT); } + wxBitmapBundle mksolid(size_t width, size_t height, unsigned char r, unsigned char g, unsigned char b, unsigned char transparency, size_t border_width = 0, bool dark_mode = false); + wxBitmapBundle* mksolid_bndl(size_t width, size_t height, const std::string& color = std::string(), size_t border_width = 0, bool dark_mode = false); + wxBitmapBundle* mkclear_bndl(size_t width, size_t height) { return mksolid_bndl(width, height); } private: std::map m_map; + std::map m_bndl_map; double m_gs = 0.2; // value, used for image.ConvertToGreyscale(m_gs, m_gs, m_gs) double m_scale = 1.0; // value, used for correct scaling of SVG icons on Retina display }; diff --git a/src/slic3r/GUI/BitmapComboBox.cpp b/src/slic3r/GUI/BitmapComboBox.cpp index 3396c627b..70c985cf9 100644 --- a/src/slic3r/GUI/BitmapComboBox.cpp +++ b/src/slic3r/GUI/BitmapComboBox.cpp @@ -54,17 +54,6 @@ using Slic3r::GUI::format_wxstr; namespace Slic3r { namespace GUI { -/* For PresetComboBox we use bitmaps that are created from images that are already scaled appropriately for Retina - * (Contrary to the intuition, the `scale` argument for Bitmap's constructor doesn't mean - * "please scale this to such and such" but rather - * "the wxImage is already sized for backing scale such and such". ) - * Unfortunately, the constructor changes the size of wxBitmap too. - * Thus We need to use unscaled size value for bitmaps that we use - * to avoid scaled size of control items. - * For this purpose control drawing methods and - * control size calculation methods (virtual) are overridden. - **/ - BitmapComboBox::BitmapComboBox(wxWindow* parent, wxWindowID id/* = wxID_ANY*/, const wxString& value/* = wxEmptyString*/, @@ -90,72 +79,6 @@ BitmapComboBox::~BitmapComboBox() { } -#ifdef __APPLE__ -bool BitmapComboBox::OnAddBitmap(const wxBitmap& bitmap) -{ - if (bitmap.IsOk()) - { - // we should use scaled! size values of bitmap - int width = (int)bitmap.GetScaledWidth(); - int height = (int)bitmap.GetScaledHeight(); - - if (m_usedImgSize.x < 0) - { - // If size not yet determined, get it from this image. - m_usedImgSize.x = width; - m_usedImgSize.y = height; - - // Adjust control size to vertically fit the bitmap - wxWindow* ctrl = GetControl(); - ctrl->InvalidateBestSize(); - wxSize newSz = ctrl->GetBestSize(); - wxSize sz = ctrl->GetSize(); - if (newSz.y > sz.y) - ctrl->SetSize(sz.x, newSz.y); - else - DetermineIndent(); - } - - wxCHECK_MSG(width == m_usedImgSize.x && height == m_usedImgSize.y, - false, - "you can only add images of same size"); - - return true; - } - - return false; -} - -void BitmapComboBox::OnDrawItem(wxDC& dc, - const wxRect& rect, - int item, - int flags) const -{ - const wxBitmap& bmp = *(static_cast(m_bitmaps[item])); - if (bmp.IsOk()) - { - // we should use scaled! size values of bitmap - wxCoord w = bmp.GetScaledWidth(); - wxCoord h = bmp.GetScaledHeight(); - - const int imgSpacingLeft = 4; - - // Draw the image centered - dc.DrawBitmap(bmp, - rect.x + (m_usedImgSize.x - w) / 2 + imgSpacingLeft, - rect.y + (rect.height - h) / 2, - true); - } - - wxString text = GetString(item); - if (!text.empty()) - dc.DrawText(text, - rect.x + m_imgAreaWidth + 1, - rect.y + (rect.height - dc.GetCharHeight()) / 2); -} -#endif - - #ifdef _WIN32 int BitmapComboBox::Append(const wxString& item) @@ -166,18 +89,11 @@ int BitmapComboBox::Append(const wxString& item) //2. But then set width to 0 value for no using of bitmap left and right spacing //3. Set this empty bitmap to the at list one item and BitmapCombobox will be recreated correct - wxBitmap bitmap(1, int(1.6 * wxGetApp().em_unit() + 1)); - { - // bitmap.SetWidth(0); is depricated now - // so, use next code - bitmap.UnShare();// AllocExclusive(); - bitmap.GetGDIImageData()->m_width = 0; - } - + wxBitmapBundle bitmap = *get_empty_bmp_bundle(1, 16); OnAddBitmap(bitmap); + const int n = wxComboBox::Append(item); - if (n != wxNOT_FOUND) - DoSetItemBitmap(n, bitmap); + return n; } diff --git a/src/slic3r/GUI/BitmapComboBox.hpp b/src/slic3r/GUI/BitmapComboBox.hpp index a77bf401d..545213fc3 100644 --- a/src/slic3r/GUI/BitmapComboBox.hpp +++ b/src/slic3r/GUI/BitmapComboBox.hpp @@ -29,28 +29,13 @@ BitmapComboBox(wxWindow* parent, #ifdef _WIN32 int Append(const wxString& item); #endif - int Append(const wxString& item, const wxBitmap& bitmap) + int Append(const wxString& item, const wxBitmapBundle& bitmap) { return wxBitmapComboBox::Append(item, bitmap); } protected: -#ifdef __APPLE__ -/* For PresetComboBox we use bitmaps that are created from images that are already scaled appropriately for Retina - * (Contrary to the intuition, the `scale` argument for Bitmap's constructor doesn't mean - * "please scale this to such and such" but rather - * "the wxImage is already sized for backing scale such and such". ) - * Unfortunately, the constructor changes the size of wxBitmap too. - * Thus We need to use unscaled size value for bitmaps that we use - * to avoid scaled size of control items. - * For this purpose control drawing methods and - * control size calculation methods (virtual) are overridden. - **/ -bool OnAddBitmap(const wxBitmap& bitmap) override; -void OnDrawItem(wxDC& dc, const wxRect& rect, int item, int flags) const override; -#endif - #ifdef _WIN32 bool MSWOnDraw(WXDRAWITEMSTRUCT* item) override; void DrawBackground_(wxDC& dc, const wxRect& rect, int WXUNUSED(item), int flags) const; diff --git a/src/slic3r/GUI/ButtonsDescription.cpp b/src/slic3r/GUI/ButtonsDescription.cpp index 2c5262d47..37daffd9d 100644 --- a/src/slic3r/GUI/ButtonsDescription.cpp +++ b/src/slic3r/GUI/ButtonsDescription.cpp @@ -17,9 +17,6 @@ void ButtonsDescription::FillSizerWithTextColorDescriptions(wxSizer* sizer, wxWi wxFlexGridSizer* grid_sizer = new wxFlexGridSizer(3, 5, 5); sizer->Add(grid_sizer, 0, wxEXPAND); - ScalableBitmap bmp_delete = ScalableBitmap(parent, "cross"); - ScalableBitmap bmp_delete_focus = ScalableBitmap(parent, "cross_focus"); - auto add_color = [grid_sizer, parent](wxColourPickerCtrl** color_picker, const wxColour& color, const wxColour& def_color, wxString label_text) { // wrap the label_text to the max 80 characters if (label_text.Len() > 80) { diff --git a/src/slic3r/GUI/ConfigWizard.cpp b/src/slic3r/GUI/ConfigWizard.cpp index 86768dfc5..b5da80e90 100644 --- a/src/slic3r/GUI/ConfigWizard.cpp +++ b/src/slic3r/GUI/ConfigWizard.cpp @@ -1,3075 +1,3074 @@ -// FIXME: extract absolute units -> em - -#include "ConfigWizard_private.hpp" - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#ifdef _MSW_DARK_MODE -#include -#endif // _MSW_DARK_MODE - -#include "libslic3r/Platform.hpp" -#include "libslic3r/Utils.hpp" -#include "libslic3r/Config.hpp" -#include "libslic3r/libslic3r.h" -#include "libslic3r/Model.hpp" -#include "libslic3r/Color.hpp" -#include "GUI.hpp" -#include "GUI_App.hpp" -#include "GUI_Utils.hpp" -#include "GUI_ObjectManipulation.hpp" -#include "Field.hpp" -#include "DesktopIntegrationDialog.hpp" -#include "slic3r/Config/Snapshot.hpp" -#include "slic3r/Utils/PresetUpdater.hpp" -#include "format.hpp" -#include "MsgDialog.hpp" -#include "UnsavedChangesDialog.hpp" - -#if defined(__linux__) && defined(__WXGTK3__) -#define wxLinux_gtk3 true -#else -#define wxLinux_gtk3 false -#endif //defined(__linux__) && defined(__WXGTK3__) - -namespace Slic3r { -namespace GUI { - - -using Config::Snapshot; -using Config::SnapshotDB; - - -// Configuration data structures extensions needed for the wizard - -bool Bundle::load(fs::path source_path, bool ais_in_resources, bool ais_prusa_bundle) -{ - this->preset_bundle = std::make_unique(); - this->is_in_resources = ais_in_resources; - this->is_prusa_bundle = ais_prusa_bundle; - - std::string path_string = source_path.string(); - // Throw when parsing invalid configuration. Only valid configuration is supposed to be provided over the air. - auto [config_substitutions, presets_loaded] = preset_bundle->load_configbundle( - path_string, PresetBundle::LoadConfigBundleAttribute::LoadSystem, ForwardCompatibilitySubstitutionRule::Disable); - UNUSED(config_substitutions); - // No substitutions shall be reported when loading a system config bundle, no substitutions are allowed. - assert(config_substitutions.empty()); - auto first_vendor = preset_bundle->vendors.begin(); - if (first_vendor == preset_bundle->vendors.end()) { - BOOST_LOG_TRIVIAL(error) << boost::format("Vendor bundle: `%1%`: No vendor information defined, cannot install.") % path_string; - return false; - } - if (presets_loaded == 0) { - BOOST_LOG_TRIVIAL(error) << boost::format("Vendor bundle: `%1%`: No profile loaded.") % path_string; - return false; - } - - BOOST_LOG_TRIVIAL(trace) << boost::format("Vendor bundle: `%1%`: %2% profiles loaded.") % path_string % presets_loaded; - this->vendor_profile = &first_vendor->second; - return true; -} - -Bundle::Bundle(Bundle &&other) - : preset_bundle(std::move(other.preset_bundle)) - , vendor_profile(other.vendor_profile) - , is_in_resources(other.is_in_resources) - , is_prusa_bundle(other.is_prusa_bundle) -{ - other.vendor_profile = nullptr; -} - -BundleMap BundleMap::load() -{ - BundleMap res; - - const auto vendor_dir = (boost::filesystem::path(Slic3r::data_dir()) / "vendor").make_preferred(); - const auto rsrc_vendor_dir = (boost::filesystem::path(resources_dir()) / "profiles").make_preferred(); - - auto prusa_bundle_path = (vendor_dir / PresetBundle::PRUSA_BUNDLE).replace_extension(".ini"); - auto prusa_bundle_rsrc = false; - if (! boost::filesystem::exists(prusa_bundle_path)) { - prusa_bundle_path = (rsrc_vendor_dir / PresetBundle::PRUSA_BUNDLE).replace_extension(".ini"); - prusa_bundle_rsrc = true; - } - { - Bundle prusa_bundle; - if (prusa_bundle.load(std::move(prusa_bundle_path), prusa_bundle_rsrc, true)) - res.emplace(PresetBundle::PRUSA_BUNDLE, std::move(prusa_bundle)); - } - - // Load the other bundles in the datadir/vendor directory - // and then additionally from resources/profiles. - bool is_in_resources = false; - for (auto dir : { &vendor_dir, &rsrc_vendor_dir }) { - for (const auto &dir_entry : boost::filesystem::directory_iterator(*dir)) { - if (Slic3r::is_ini_file(dir_entry)) { - std::string id = dir_entry.path().stem().string(); // stem() = filename() without the trailing ".ini" part - - // Don't load this bundle if we've already loaded it. - if (res.find(id) != res.end()) { continue; } - - Bundle bundle; - if (bundle.load(dir_entry.path(), is_in_resources)) - res.emplace(std::move(id), std::move(bundle)); - } - } - - is_in_resources = true; - } - - return res; -} - -Bundle& BundleMap::prusa_bundle() -{ - auto it = find(PresetBundle::PRUSA_BUNDLE); - if (it == end()) { - throw Slic3r::RuntimeError("ConfigWizard: Internal error in BundleMap: PRUSA_BUNDLE not loaded"); - } - - return it->second; -} - -const Bundle& BundleMap::prusa_bundle() const -{ - return const_cast(this)->prusa_bundle(); -} - - -// Printer model picker GUI control - -struct PrinterPickerEvent : public wxEvent -{ - std::string vendor_id; - std::string model_id; - std::string variant_name; - bool enable; - - PrinterPickerEvent(wxEventType eventType, int winid, std::string vendor_id, std::string model_id, std::string variant_name, bool enable) - : wxEvent(winid, eventType) - , vendor_id(std::move(vendor_id)) - , model_id(std::move(model_id)) - , variant_name(std::move(variant_name)) - , enable(enable) - {} - - virtual wxEvent *Clone() const - { - return new PrinterPickerEvent(*this); - } -}; - -wxDEFINE_EVENT(EVT_PRINTER_PICK, PrinterPickerEvent); - -const std::string PrinterPicker::PRINTER_PLACEHOLDER = "printer_placeholder.png"; - -PrinterPicker::PrinterPicker(wxWindow *parent, const VendorProfile &vendor, wxString title, size_t max_cols, const AppConfig &appconfig, const ModelFilter &filter) - : wxPanel(parent) - , vendor_id(vendor.id) - , width(0) -{ - wxGetApp().UpdateDarkUI(this); - const auto &models = vendor.models; - - auto *sizer = new wxBoxSizer(wxVERTICAL); - - const auto font_title = GetFont().MakeBold().Scaled(1.3f); - const auto font_name = GetFont().MakeBold(); - const auto font_alt_nozzle = GetFont().Scaled(0.9f); - - // wxGrid appends widgets by rows, but we need to construct them in columns. - // These vectors are used to hold the elements so that they can be appended in the right order. - std::vector titles; - std::vector bitmaps; - std::vector variants_panels; - - int max_row_width = 0; - int current_row_width = 0; - - bool is_variants = false; - - for (const auto &model : models) { - if (! filter(model)) { continue; } - - wxBitmap bitmap; - int bitmap_width = 0; - auto load_bitmap = [](const wxString& bitmap_file, wxBitmap& bitmap, int& bitmap_width)->bool { - if (wxFileExists(bitmap_file)) { - bitmap.LoadFile(bitmap_file, wxBITMAP_TYPE_PNG); - bitmap_width = bitmap.GetWidth(); - return true; - } - return false; - }; - if (!load_bitmap(GUI::from_u8(Slic3r::data_dir() + "/vendor/" + vendor.id + "/" + model.id + "_thumbnail.png"), bitmap, bitmap_width)) { - if (!load_bitmap(GUI::from_u8(Slic3r::resources_dir() + "/profiles/" + vendor.id + "/" + model.id + "_thumbnail.png"), bitmap, bitmap_width)) { - BOOST_LOG_TRIVIAL(warning) << boost::format("Can't find bitmap file `%1%` for vendor `%2%`, printer `%3%`, using placeholder icon instead") - % (Slic3r::resources_dir() + "/profiles/" + vendor.id + "/" + model.id + "_thumbnail.png") - % vendor.id - % model.id; - load_bitmap(Slic3r::var(PRINTER_PLACEHOLDER), bitmap, bitmap_width); - } - } - auto *title = new wxStaticText(this, wxID_ANY, from_u8(model.name), wxDefaultPosition, wxDefaultSize, wxALIGN_LEFT); - title->SetFont(font_name); - const int wrap_width = std::max((int)MODEL_MIN_WRAP, bitmap_width); - title->Wrap(wrap_width); - - current_row_width += wrap_width; - if (titles.size() % max_cols == max_cols - 1) { - max_row_width = std::max(max_row_width, current_row_width); - current_row_width = 0; - } - - titles.push_back(title); - - auto *bitmap_widget = new wxStaticBitmap(this, wxID_ANY, bitmap); - bitmaps.push_back(bitmap_widget); - - auto *variants_panel = new wxPanel(this); - wxGetApp().UpdateDarkUI(variants_panel); - auto *variants_sizer = new wxBoxSizer(wxVERTICAL); - variants_panel->SetSizer(variants_sizer); - const auto model_id = model.id; - - for (size_t i = 0; i < model.variants.size(); i++) { - const auto &variant = model.variants[i]; - - const auto label = model.technology == ptFFF - ? from_u8((boost::format("%1% %2% %3%") % variant.name % _utf8(L("mm")) % _utf8(L("nozzle"))).str()) - : from_u8(model.name); - - if (i == 1) { - auto *alt_label = new wxStaticText(variants_panel, wxID_ANY, _L("Alternate nozzles:")); - alt_label->SetFont(font_alt_nozzle); - variants_sizer->Add(alt_label, 0, wxBOTTOM, 3); - is_variants = true; - } - - auto *cbox = new Checkbox(variants_panel, label, model_id, variant.name); - i == 0 ? cboxes.push_back(cbox) : cboxes_alt.push_back(cbox); - - const bool enabled = appconfig.get_variant(vendor.id, model_id, variant.name); - cbox->SetValue(enabled); - - variants_sizer->Add(cbox, 0, wxBOTTOM, 3); - - cbox->Bind(wxEVT_CHECKBOX, [this, cbox](wxCommandEvent &event) { - on_checkbox(cbox, event.IsChecked()); - }); - } - - variants_panels.push_back(variants_panel); - } - - width = std::max(max_row_width, current_row_width); - - const size_t cols = std::min(max_cols, titles.size()); - - auto *printer_grid = new wxFlexGridSizer(cols, 0, 20); - printer_grid->SetFlexibleDirection(wxVERTICAL | wxHORIZONTAL); - - if (titles.size() > 0) { - const size_t odd_items = titles.size() % cols; - - for (size_t i = 0; i < titles.size() - odd_items; i += cols) { - for (size_t j = i; j < i + cols; j++) { printer_grid->Add(bitmaps[j], 0, wxBOTTOM, 20); } - for (size_t j = i; j < i + cols; j++) { printer_grid->Add(titles[j], 0, wxBOTTOM, 3); } - for (size_t j = i; j < i + cols; j++) { printer_grid->Add(variants_panels[j]); } - - // Add separator space to multiliners - if (titles.size() > cols) { - for (size_t j = i; j < i + cols; j++) { printer_grid->Add(1, 30); } - } - } - if (odd_items > 0) { - const size_t rem = titles.size() - odd_items; - - for (size_t i = rem; i < titles.size(); i++) { printer_grid->Add(bitmaps[i], 0, wxBOTTOM, 20); } - for (size_t i = 0; i < cols - odd_items; i++) { printer_grid->AddSpacer(1); } - for (size_t i = rem; i < titles.size(); i++) { printer_grid->Add(titles[i], 0, wxBOTTOM, 3); } - for (size_t i = 0; i < cols - odd_items; i++) { printer_grid->AddSpacer(1); } - for (size_t i = rem; i < titles.size(); i++) { printer_grid->Add(variants_panels[i]); } - } - } - - auto *title_sizer = new wxBoxSizer(wxHORIZONTAL); - if (! title.IsEmpty()) { - auto *title_widget = new wxStaticText(this, wxID_ANY, title); - title_widget->SetFont(font_title); - title_sizer->Add(title_widget); - } - title_sizer->AddStretchSpacer(); - - if (titles.size() > 1 || is_variants) { - // It only makes sense to add the All / None buttons if there's multiple printers - // All Standard button is added when there are more variants for at least one printer - auto *sel_all_std = new wxButton(this, wxID_ANY, titles.size() > 1 ? _L("All standard") : _L("Standard")); - auto *sel_all = new wxButton(this, wxID_ANY, _L("All")); - auto *sel_none = new wxButton(this, wxID_ANY, _L("None")); - if (is_variants) - sel_all_std->Bind(wxEVT_BUTTON, [this](const wxCommandEvent& event) { this->select_all(true, false); }); - sel_all->Bind(wxEVT_BUTTON, [this](const wxCommandEvent &event) { this->select_all(true, true); }); - sel_none->Bind(wxEVT_BUTTON, [this](const wxCommandEvent &event) { this->select_all(false); }); - if (is_variants) - title_sizer->Add(sel_all_std, 0, wxRIGHT, BTN_SPACING); - title_sizer->Add(sel_all, 0, wxRIGHT, BTN_SPACING); - title_sizer->Add(sel_none); - - wxGetApp().UpdateDarkUI(sel_all_std); - wxGetApp().UpdateDarkUI(sel_all); - wxGetApp().UpdateDarkUI(sel_none); - - // fill button indexes used later for buttons rescaling - if (is_variants) - m_button_indexes = { sel_all_std->GetId(), sel_all->GetId(), sel_none->GetId() }; - else { - sel_all_std->Destroy(); - m_button_indexes = { sel_all->GetId(), sel_none->GetId() }; - } - } - - sizer->Add(title_sizer, 0, wxEXPAND | wxBOTTOM, BTN_SPACING); - sizer->Add(printer_grid); - - SetSizer(sizer); -} - -PrinterPicker::PrinterPicker(wxWindow *parent, const VendorProfile &vendor, wxString title, size_t max_cols, const AppConfig &appconfig) - : PrinterPicker(parent, vendor, std::move(title), max_cols, appconfig, [](const VendorProfile::PrinterModel&) { return true; }) -{} - -void PrinterPicker::select_all(bool select, bool alternates) -{ - for (const auto &cb : cboxes) { - if (cb->GetValue() != select) { - cb->SetValue(select); - on_checkbox(cb, select); - } - } - - if (! select) { alternates = false; } - - for (const auto &cb : cboxes_alt) { - if (cb->GetValue() != alternates) { - cb->SetValue(alternates); - on_checkbox(cb, alternates); - } - } -} - -void PrinterPicker::select_one(size_t i, bool select) -{ - if (i < cboxes.size() && cboxes[i]->GetValue() != select) { - cboxes[i]->SetValue(select); - on_checkbox(cboxes[i], select); - } -} - -bool PrinterPicker::any_selected() const -{ - for (const auto &cb : cboxes) { - if (cb->GetValue()) { return true; } - } - - for (const auto &cb : cboxes_alt) { - if (cb->GetValue()) { return true; } - } - - return false; -} - -std::set PrinterPicker::get_selected_models() const -{ - std::set ret_set; - - for (const auto& cb : cboxes) - if (cb->GetValue()) - ret_set.emplace(cb->model); - - for (const auto& cb : cboxes_alt) - if (cb->GetValue()) - ret_set.emplace(cb->model); - - return ret_set; -} - -void PrinterPicker::on_checkbox(const Checkbox *cbox, bool checked) -{ - PrinterPickerEvent evt(EVT_PRINTER_PICK, GetId(), vendor_id, cbox->model, cbox->variant, checked); - AddPendingEvent(evt); -} - - -// Wizard page base - -ConfigWizardPage::ConfigWizardPage(ConfigWizard *parent, wxString title, wxString shortname, unsigned indent) - : wxPanel(parent->p->hscroll) - , parent(parent) - , shortname(std::move(shortname)) - , indent(indent) -{ - wxGetApp().UpdateDarkUI(this); - - auto *sizer = new wxBoxSizer(wxVERTICAL); - - auto *text = new wxStaticText(this, wxID_ANY, std::move(title), wxDefaultPosition, wxDefaultSize, wxALIGN_LEFT); - const auto font = GetFont().MakeBold().Scaled(1.5); - text->SetFont(font); - sizer->Add(text, 0, wxALIGN_LEFT, 0); - sizer->AddSpacer(10); - - content = new wxBoxSizer(wxVERTICAL); - sizer->Add(content, 1, wxEXPAND); - - SetSizer(sizer); - - // There is strange layout on Linux with GTK3, - // see https://github.com/prusa3d/PrusaSlicer/issues/5103 and https://github.com/prusa3d/PrusaSlicer/issues/4861 - // So, non-active pages will be hidden later, on wxEVT_SHOW, after completed Layout() for all pages - if (!wxLinux_gtk3) - this->Hide(); - - Bind(wxEVT_SIZE, [this](wxSizeEvent &event) { - this->Layout(); - event.Skip(); - }); -} - -ConfigWizardPage::~ConfigWizardPage() {} - -wxStaticText* ConfigWizardPage::append_text(wxString text) -{ - auto *widget = new wxStaticText(this, wxID_ANY, text, wxDefaultPosition, wxDefaultSize, wxALIGN_LEFT); - widget->Wrap(WRAP_WIDTH); - widget->SetMinSize(wxSize(WRAP_WIDTH, -1)); - append(widget); - return widget; -} - -void ConfigWizardPage::append_spacer(int space) -{ - // FIXME: scaling - content->AddSpacer(space); -} - -// Wizard pages - -PageWelcome::PageWelcome(ConfigWizard *parent) - : ConfigWizardPage(parent, from_u8((boost::format( -#ifdef __APPLE__ - _utf8(L("Welcome to the %s Configuration Assistant")) -#else - _utf8(L("Welcome to the %s Configuration Wizard")) -#endif - ) % SLIC3R_APP_NAME).str()), _L("Welcome")) - , welcome_text(append_text(from_u8((boost::format( - _utf8(L("Hello, welcome to %s! This %s helps you with the initial configuration; just a few settings and you will be ready to print."))) - % SLIC3R_APP_NAME - % _utf8(ConfigWizard::name())).str()) - )) - , cbox_reset(append( - new wxCheckBox(this, wxID_ANY, _L("Remove user profiles (a snapshot will be taken beforehand)")) - )) - , cbox_integrate(append( - new wxCheckBox(this, wxID_ANY, _L("Perform desktop integration (Sets this binary to be searchable by the system).")) - )) -{ - welcome_text->Hide(); - cbox_reset->Hide(); - cbox_integrate->Hide(); -} - -void PageWelcome::set_run_reason(ConfigWizard::RunReason run_reason) -{ - const bool data_empty = run_reason == ConfigWizard::RR_DATA_EMPTY; - welcome_text->Show(data_empty); - cbox_reset->Show(!data_empty); -#if defined(__linux__) && defined(SLIC3R_DESKTOP_INTEGRATION) - if (!DesktopIntegrationDialog::is_integrated()) - cbox_integrate->Show(true); - else - cbox_integrate->Hide(); -#else - cbox_integrate->Hide(); -#endif -} - - -PagePrinters::PagePrinters(ConfigWizard *parent, - wxString title, - wxString shortname, - const VendorProfile &vendor, - unsigned indent, - Technology technology) - : ConfigWizardPage(parent, std::move(title), std::move(shortname), indent) - , technology(technology) - , install(false) // only used for 3rd party vendors -{ - enum { - COL_SIZE = 200, - }; - - AppConfig *appconfig = &this->wizard_p()->appconfig_new; - - const auto families = vendor.families(); - for (const auto &family : families) { - const auto filter = [&](const VendorProfile::PrinterModel &model) { - return ((model.technology == ptFFF && technology & T_FFF) - || (model.technology == ptSLA && technology & T_SLA)) - && model.family == family; - }; - - if (std::find_if(vendor.models.begin(), vendor.models.end(), filter) == vendor.models.end()) { - continue; - } - - const auto picker_title = family.empty() ? wxString() : from_u8((boost::format(_utf8(L("%s Family"))) % family).str()); - auto *picker = new PrinterPicker(this, vendor, picker_title, MAX_COLS, *appconfig, filter); - - picker->Bind(EVT_PRINTER_PICK, [this, appconfig](const PrinterPickerEvent &evt) { - appconfig->set_variant(evt.vendor_id, evt.model_id, evt.variant_name, evt.enable); - wizard_p()->on_printer_pick(this, evt); - }); - - append(new StaticLine(this)); - - append(picker); - printer_pickers.push_back(picker); - has_printers = true; - } - -} - -void PagePrinters::select_all(bool select, bool alternates) -{ - for (auto picker : printer_pickers) { - picker->select_all(select, alternates); - } -} - -int PagePrinters::get_width() const -{ - return std::accumulate(printer_pickers.begin(), printer_pickers.end(), 0, - [](int acc, const PrinterPicker *picker) { return std::max(acc, picker->get_width()); }); -} - -bool PagePrinters::any_selected() const -{ - for (const auto *picker : printer_pickers) { - if (picker->any_selected()) { return true; } - } - - return false; -} - -std::set PagePrinters::get_selected_models() -{ - std::set ret_set; - - for (const auto *picker : printer_pickers) - { - std::set tmp_models = picker->get_selected_models(); - ret_set.insert(tmp_models.begin(), tmp_models.end()); - } - - return ret_set; -} - -void PagePrinters::set_run_reason(ConfigWizard::RunReason run_reason) -{ - if (is_primary_printer_page - && (run_reason == ConfigWizard::RR_DATA_EMPTY || run_reason == ConfigWizard::RR_DATA_LEGACY) - && printer_pickers.size() > 0 - && printer_pickers[0]->vendor_id == PresetBundle::PRUSA_BUNDLE) { - printer_pickers[0]->select_one(0, true); - } -} - - -const std::string PageMaterials::EMPTY; - -PageMaterials::PageMaterials(ConfigWizard *parent, Materials *materials, wxString title, wxString shortname, wxString list1name) - : ConfigWizardPage(parent, std::move(title), std::move(shortname)) - , materials(materials) - , list_printer(new StringList(this, wxLB_MULTIPLE)) - , list_type(new StringList(this)) - , list_vendor(new StringList(this)) - , list_profile(new PresetList(this)) -{ - append_spacer(VERTICAL_SPACING); - - const int em = parent->em_unit(); - const int list_h = 30*em; - - - list_printer->SetMinSize(wxSize(23*em, list_h)); - list_type->SetMinSize(wxSize(13*em, list_h)); - list_vendor->SetMinSize(wxSize(13*em, list_h)); - list_profile->SetMinSize(wxSize(23*em, list_h)); - - - - grid = new wxFlexGridSizer(4, em/2, em); - grid->AddGrowableCol(3, 1); - grid->AddGrowableRow(1, 1); - - grid->Add(new wxStaticText(this, wxID_ANY, _L("Printer:"))); - grid->Add(new wxStaticText(this, wxID_ANY, list1name)); - grid->Add(new wxStaticText(this, wxID_ANY, _L("Vendor:"))); - grid->Add(new wxStaticText(this, wxID_ANY, _L("Profile:"))); - - grid->Add(list_printer, 0, wxEXPAND); - grid->Add(list_type, 0, wxEXPAND); - grid->Add(list_vendor, 0, wxEXPAND); - grid->Add(list_profile, 1, wxEXPAND); - - auto *btn_sizer = new wxBoxSizer(wxHORIZONTAL); - auto *sel_all = new wxButton(this, wxID_ANY, _L("All")); - auto *sel_none = new wxButton(this, wxID_ANY, _L("None")); - btn_sizer->Add(sel_all, 0, wxRIGHT, em / 2); - btn_sizer->Add(sel_none); - - wxGetApp().UpdateDarkUI(list_printer); - wxGetApp().UpdateDarkUI(list_type); - wxGetApp().UpdateDarkUI(list_vendor); - wxGetApp().UpdateDarkUI(sel_all); - wxGetApp().UpdateDarkUI(sel_none); - - grid->Add(new wxBoxSizer(wxHORIZONTAL)); - grid->Add(new wxBoxSizer(wxHORIZONTAL)); - grid->Add(new wxBoxSizer(wxHORIZONTAL)); - grid->Add(btn_sizer, 0, wxALIGN_RIGHT); - - append(grid, 1, wxEXPAND); - - append_spacer(VERTICAL_SPACING); - - html_window = new wxHtmlWindow(this, wxID_ANY, wxDefaultPosition, - wxSize(60 * em, 20 * em), wxHW_SCROLLBAR_AUTO); - append(html_window, 0, wxEXPAND); - - list_printer->Bind(wxEVT_LISTBOX, [this](wxCommandEvent& evt) { - update_lists(list_type->GetSelection(), list_vendor->GetSelection(), evt.GetInt()); - }); - list_type->Bind(wxEVT_LISTBOX, [this](wxCommandEvent &) { - update_lists(list_type->GetSelection(), list_vendor->GetSelection()); - }); - list_vendor->Bind(wxEVT_LISTBOX, [this](wxCommandEvent &) { - update_lists(list_type->GetSelection(), list_vendor->GetSelection()); - }); - - list_profile->Bind(wxEVT_CHECKLISTBOX, [this](wxCommandEvent &evt) { select_material(evt.GetInt()); }); - list_profile->Bind(wxEVT_LISTBOX, [this](wxCommandEvent& evt) { on_material_highlighted(evt.GetInt()); }); - - sel_all->Bind(wxEVT_BUTTON, [this](wxCommandEvent &) { select_all(true); }); - sel_none->Bind(wxEVT_BUTTON, [this](wxCommandEvent &) { select_all(false); }); - /* - Bind(wxEVT_PAINT, [this](wxPaintEvent& evt) {on_paint();}); - - list_profile->Bind(wxEVT_MOTION, [this](wxMouseEvent& evt) { on_mouse_move_on_profiles(evt); }); - list_profile->Bind(wxEVT_ENTER_WINDOW, [this](wxMouseEvent& evt) { on_mouse_enter_profiles(evt); }); - list_profile->Bind(wxEVT_LEAVE_WINDOW, [this](wxMouseEvent& evt) { on_mouse_leave_profiles(evt); }); - */ - reload_presets(); - set_compatible_printers_html_window(std::vector(), false); -} -void PageMaterials::on_paint() -{ -} -void PageMaterials::on_mouse_move_on_profiles(wxMouseEvent& evt) -{ - const wxClientDC dc(list_profile); - const wxPoint pos = evt.GetLogicalPosition(dc); - int item = list_profile->HitTest(pos); - on_material_hovered(item); -} -void PageMaterials::on_mouse_enter_profiles(wxMouseEvent& evt) -{} -void PageMaterials::on_mouse_leave_profiles(wxMouseEvent& evt) -{ - on_material_hovered(-1); -} -void PageMaterials::reload_presets() -{ - clear(); - - list_printer->append(_L("(All)"), &EMPTY); - //list_printer->SetLabelMarkup("bald"); - for (const Preset* printer : materials->printers) { - list_printer->append(printer->name, &printer->name); - } - sort_list_data(list_printer, true, false); - if (list_printer->GetCount() > 0) { - list_printer->SetSelection(0); - sel_printers_prev.Clear(); - sel_type_prev = wxNOT_FOUND; - sel_vendor_prev = wxNOT_FOUND; - update_lists(0, 0, 0); - } - - presets_loaded = true; -} - -void PageMaterials::set_compatible_printers_html_window(const std::vector& printer_names, bool all_printers) -{ - const auto bgr_clr = -#if defined(__APPLE__) - html_window->GetParent()->GetBackgroundColour(); -#else -#if defined(_WIN32) - wxGetApp().get_window_default_clr(); -#else - wxSystemSettings::GetColour(wxSYS_COLOUR_MENU); -#endif -#endif - const auto text_clr = wxGetApp().get_label_clr_default(); - const auto bgr_clr_str = encode_color(ColorRGB(bgr_clr.Red(), bgr_clr.Green(), bgr_clr.Blue())); - const auto text_clr_str = encode_color(ColorRGB(text_clr.Red(), text_clr.Green(), text_clr.Blue())); - wxString first_line = format_wxstr(_L("%1% marked with * are not compatible with some installed printers."), materials->technology == T_FFF ? _L("Filaments") : _L("SLA materials")); - wxString text; - if (all_printers) { - wxString second_line = format_wxstr(_L("All installed printers are compatible with the selected %1%."), materials->technology == T_FFF ? _L("filament") : _L("SLA material")); - text = wxString::Format( - "" - "" - "" - "" - "" - "%s

%s" - "
" - "
" - "" - "" - , bgr_clr_str - , text_clr_str - , first_line - , second_line - ); - } else { - wxString second_line; - if (!printer_names.empty()) - second_line = (materials->technology == T_FFF ? - _L("Only the following installed printers are compatible with the selected filaments") : - _L("Only the following installed printers are compatible with the selected SLA materials")) + ":"; - text = wxString::Format( - "" - "" - "" - "" - "" - "%s

%s" - "" - "" - , bgr_clr_str - , text_clr_str - , first_line - , second_line); - for (size_t i = 0; i < printer_names.size(); ++i) - { - text += wxString::Format("", boost::nowide::widen(printer_names[i])); - if (i % 3 == 2) { - text += wxString::Format( - "" - ""); - } - } - text += wxString::Format( - "" - "
%s
" - "
" - "
" - "" - "" - ); - } - - wxFont font = get_default_font_for_dpi(this, get_dpi_for_window(this)); - const int fs = font.GetPointSize(); - int size[] = { fs,fs,fs,fs,fs,fs,fs }; - html_window->SetFonts(font.GetFaceName(), font.GetFaceName(), size); - html_window->SetPage(text); -} - -void PageMaterials::clear_compatible_printers_label() -{ - set_compatible_printers_html_window(std::vector(), false); -} - -void PageMaterials::on_material_hovered(int sel_material) -{ - -} - -void PageMaterials::on_material_highlighted(int sel_material) -{ - if (sel_material == last_hovered_item) - return; - if (sel_material == -1) { - clear_compatible_printers_label(); - return; - } - last_hovered_item = sel_material; - std::vector tabs; - tabs.push_back(std::string()); - tabs.push_back(std::string()); - tabs.push_back(std::string()); - //selected material string - std::string material_name = list_profile->get_data(sel_material); - // get material preset - const std::vector matching_materials = materials->get_presets_by_alias(material_name); - if (matching_materials.empty()) - { - clear_compatible_printers_label(); - return; - } - //find matching printers - std::vector names; - for (const Preset* printer : materials->printers) { - for (const Preset* material : matching_materials) { - if (is_compatible_with_printer(PresetWithVendorProfile(*material, material->vendor), PresetWithVendorProfile(*printer, printer->vendor))) { - names.push_back(printer->name); - break; - } - } - } - set_compatible_printers_html_window(names, names.size() == materials->printers.size()); -} - -void PageMaterials::update_lists(int sel_type, int sel_vendor, int last_selected_printer/* = -1*/) -{ - wxWindowUpdateLocker freeze_guard(this); - (void)freeze_guard; - - wxArrayInt sel_printers; - int sel_printers_count = list_printer->GetSelections(sel_printers); - - // Does our wxWidgets version support operator== for wxArrayInt ? - // https://github.com/prusa3d/PrusaSlicer/issues/5152#issuecomment-787208614 -#if wxCHECK_VERSION(3, 1, 1) - if (sel_printers != sel_printers_prev) { -#else - auto are_equal = [](const wxArrayInt& arr_first, const wxArrayInt& arr_second) { - if (arr_first.GetCount() != arr_second.GetCount()) - return false; - for (size_t i = 0; i < arr_first.GetCount(); i++) - if (arr_first[i] != arr_second[i]) - return false; - return true; - }; - if (!are_equal(sel_printers, sel_printers_prev)) { -#endif - - // Refresh type list - list_type->Clear(); - list_type->append(_L("(All)"), &EMPTY); - if (sel_printers_count > 0) { - // If all is selected with other printers - // unselect "all" or all printers depending on last value - if (sel_printers[0] == 0 && sel_printers_count > 1) { - if (last_selected_printer == 0) { - list_printer->SetSelection(wxNOT_FOUND); - list_printer->SetSelection(0); - } else { - list_printer->SetSelection(0, false); - sel_printers_count = list_printer->GetSelections(sel_printers); - } - } - if (sel_printers[0] != 0) { - for (int i = 0; i < sel_printers_count; i++) { - const std::string& printer_name = list_printer->get_data(sel_printers[i]); - const Preset* printer = nullptr; - for (const Preset* it : materials->printers) { - if (it->name == printer_name) { - printer = it; - break; - } - } - materials->filter_presets(printer, EMPTY, EMPTY, [this](const Preset* p) { - const std::string& type = this->materials->get_type(p); - if (list_type->find(type) == wxNOT_FOUND) { - list_type->append(type, &type); - } - }); - } - } else { - //clear selection except "ALL" - list_printer->SetSelection(wxNOT_FOUND); - list_printer->SetSelection(0); - sel_printers_count = list_printer->GetSelections(sel_printers); - - materials->filter_presets(nullptr, EMPTY, EMPTY, [this](const Preset* p) { - const std::string& type = this->materials->get_type(p); - if (list_type->find(type) == wxNOT_FOUND) { - list_type->append(type, &type); - } - }); - } - sort_list_data(list_type, true, true); - } - - sel_printers_prev = sel_printers; - sel_type = 0; - sel_type_prev = wxNOT_FOUND; - list_type->SetSelection(sel_type); - list_profile->Clear(); - } - - if (sel_type != sel_type_prev) { - // Refresh vendor list - - // XXX: The vendor list is created with quadratic complexity here, - // but the number of vendors is going to be very small this shouldn't be a problem. - - list_vendor->Clear(); - list_vendor->append(_L("(All)"), &EMPTY); - if (sel_printers_count != 0 && sel_type != wxNOT_FOUND) { - const std::string& type = list_type->get_data(sel_type); - // find printer preset - for (int i = 0; i < sel_printers_count; i++) { - const std::string& printer_name = list_printer->get_data(sel_printers[i]); - const Preset* printer = nullptr; - for (const Preset* it : materials->printers) { - if (it->name == printer_name) { - printer = it; - break; - } - } - materials->filter_presets(printer, type, EMPTY, [this](const Preset* p) { - const std::string& vendor = this->materials->get_vendor(p); - if (list_vendor->find(vendor) == wxNOT_FOUND) { - list_vendor->append(vendor, &vendor); - } - }); - } - sort_list_data(list_vendor, true, false); - } - - sel_type_prev = sel_type; - sel_vendor = 0; - sel_vendor_prev = wxNOT_FOUND; - list_vendor->SetSelection(sel_vendor); - list_profile->Clear(); - } - - if (sel_vendor != sel_vendor_prev) { - // Refresh material list - - list_profile->Clear(); - clear_compatible_printers_label(); - if (sel_printers_count != 0 && sel_type != wxNOT_FOUND && sel_vendor != wxNOT_FOUND) { - const std::string& type = list_type->get_data(sel_type); - const std::string& vendor = list_vendor->get_data(sel_vendor); - // finst printer preset - std::vector to_list; - for (int i = 0; i < sel_printers_count; i++) { - const std::string& printer_name = list_printer->get_data(sel_printers[i]); - const Preset* printer = nullptr; - for (const Preset* it : materials->printers) { - if (it->name == printer_name) { - printer = it; - break; - } - } - - materials->filter_presets(printer, type, vendor, [this, &to_list](const Preset* p) { - const std::string& section = materials->appconfig_section(); - bool checked = wizard_p()->appconfig_new.has(section, p->name); - bool was_checked = false; - - int cur_i = list_profile->find(p->alias); - if (cur_i == wxNOT_FOUND) { - cur_i = list_profile->append(p->alias + (materials->get_omnipresent(p) ? "" : " *"), &p->alias); - to_list.emplace_back(p->alias, materials->get_omnipresent(p), checked); - } - else { - was_checked = list_profile->IsChecked(cur_i); - to_list[cur_i].checked = checked || was_checked; - } - list_profile->Check(cur_i, checked || was_checked); - - /* Update preset selection in config. - * If one preset from aliases bundle is selected, - * than mark all presets with this aliases as selected - * */ - if (checked && !was_checked) - wizard_p()->update_presets_in_config(section, p->alias, true); - else if (!checked && was_checked) - wizard_p()->appconfig_new.set(section, p->name, "1"); - }); - } - sort_list_data(list_profile, to_list); - } - - sel_vendor_prev = sel_vendor; - } - wxGetApp().UpdateDarkUI(list_profile); -} - -void PageMaterials::sort_list_data(StringList* list, bool add_All_item, bool material_type_ordering) -{ -// get data from list -// sort data -// first should be -// then prusa profiles -// then the rest -// in alphabetical order - - std::vector> prusa_profiles; - std::vector> other_profiles; - for (int i = 0 ; i < list->size(); ++i) { - const std::string& data = list->get_data(i); - if (data == EMPTY) // do not sort item - continue; - if (!material_type_ordering && data.find("Prusa") != std::string::npos) - prusa_profiles.push_back(data); - else - other_profiles.push_back(data); - } - if(material_type_ordering) { - - const ConfigOptionDef* def = print_config_def.get("filament_type"); - std::vectorenum_values = def->enum_values; - size_t end_of_sorted = 0; - for (size_t vals = 0; vals < enum_values.size(); vals++) { - for (size_t profs = end_of_sorted; profs < other_profiles.size(); profs++) - { - // find instead compare because PET vs PETG - if (other_profiles[profs].get().find(enum_values[vals]) != std::string::npos) { - //swap - if(profs != end_of_sorted) { - std::reference_wrapper aux = other_profiles[end_of_sorted]; - other_profiles[end_of_sorted] = other_profiles[profs]; - other_profiles[profs] = aux; - } - end_of_sorted++; - break; - } - } - } - } else { - std::sort(prusa_profiles.begin(), prusa_profiles.end(), [](std::reference_wrapper a, std::reference_wrapper b) { - return a.get() < b.get(); - }); - std::sort(other_profiles.begin(), other_profiles.end(), [](std::reference_wrapper a, std::reference_wrapper b) { - return a.get() < b.get(); - }); - } - - list->Clear(); - if (add_All_item) - list->append(_L("(All)"), &EMPTY); - for (const auto& item : prusa_profiles) - list->append(item, &const_cast(item.get())); - for (const auto& item : other_profiles) - list->append(item, &const_cast(item.get())); -} - -void PageMaterials::sort_list_data(PresetList* list, const std::vector& data) -{ - // sort data - // then prusa profiles - // then the rest - // in alphabetical order - std::vector prusa_profiles; - std::vector other_profiles; - //for (int i = 0; i < data.size(); ++i) { - for (const auto& item : data) { - const std::string& name = item.name; - if (name.find("Prusa") != std::string::npos) - prusa_profiles.emplace_back(item); - else - other_profiles.emplace_back(item); - } - std::sort(prusa_profiles.begin(), prusa_profiles.end(), [](ProfilePrintData a, ProfilePrintData b) { - return a.name.get() < b.name.get(); - }); - std::sort(other_profiles.begin(), other_profiles.end(), [](ProfilePrintData a, ProfilePrintData b) { - return a.name.get() < b.name.get(); - }); - list->Clear(); - for (size_t i = 0; i < prusa_profiles.size(); ++i) { - list->append(std::string(prusa_profiles[i].name) + (prusa_profiles[i].omnipresent ? "" : " *"), &const_cast(prusa_profiles[i].name.get())); - list->Check(i, prusa_profiles[i].checked); - } - for (size_t i = 0; i < other_profiles.size(); ++i) { - list->append(std::string(other_profiles[i].name) + (other_profiles[i].omnipresent ? "" : " *"), &const_cast(other_profiles[i].name.get())); - list->Check(i + prusa_profiles.size(), other_profiles[i].checked); - } -} - -void PageMaterials::select_material(int i) -{ - const bool checked = list_profile->IsChecked(i); - - const std::string& alias_key = list_profile->get_data(i); - wizard_p()->update_presets_in_config(materials->appconfig_section(), alias_key, checked); -} - -void PageMaterials::select_all(bool select) -{ - wxWindowUpdateLocker freeze_guard(this); - (void)freeze_guard; - - for (unsigned i = 0; i < list_profile->GetCount(); i++) { - const bool current = list_profile->IsChecked(i); - if (current != select) { - list_profile->Check(i, select); - select_material(i); - } - } -} - -void PageMaterials::clear() -{ - list_printer->Clear(); - list_type->Clear(); - list_vendor->Clear(); - list_profile->Clear(); - sel_printers_prev.Clear(); - sel_type_prev = wxNOT_FOUND; - sel_vendor_prev = wxNOT_FOUND; - presets_loaded = false; -} - -void PageMaterials::on_activate() -{ - if (! presets_loaded) { - wizard_p()->update_materials(materials->technology); - reload_presets(); - } - first_paint = true; -} - - -const char *PageCustom::default_profile_name = "My Settings"; - -PageCustom::PageCustom(ConfigWizard *parent) - : ConfigWizardPage(parent, _L("Custom Printer Setup"), _L("Custom Printer")) -{ - cb_custom = new wxCheckBox(this, wxID_ANY, _L("Define a custom printer profile")); - tc_profile_name = new wxTextCtrl(this, wxID_ANY, default_profile_name); - auto *label = new wxStaticText(this, wxID_ANY, _L("Custom profile name:")); - - wxGetApp().UpdateDarkUI(tc_profile_name); - - tc_profile_name->Enable(false); - tc_profile_name->Bind(wxEVT_KILL_FOCUS, [this](wxFocusEvent &evt) { - if (tc_profile_name->GetValue().IsEmpty()) { - if (profile_name_prev.IsEmpty()) { tc_profile_name->SetValue(default_profile_name); } - else { tc_profile_name->SetValue(profile_name_prev); } - } else { - profile_name_prev = tc_profile_name->GetValue(); - } - evt.Skip(); - }); - - cb_custom->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent &event) { - tc_profile_name->Enable(custom_wanted()); - wizard_p()->on_custom_setup(custom_wanted()); - - }); - - append(cb_custom); - append(label); - append(tc_profile_name); -} - -PageUpdate::PageUpdate(ConfigWizard *parent) - : ConfigWizardPage(parent, _L("Automatic updates"), _L("Updates")) - , version_check(true) - , preset_update(true) -{ - const AppConfig *app_config = wxGetApp().app_config; - auto boldfont = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT); - boldfont.SetWeight(wxFONTWEIGHT_BOLD); - - auto *box_slic3r = new wxCheckBox(this, wxID_ANY, _L("Check for application updates")); - box_slic3r->SetValue(app_config->get("notify_release") != "none"); - append(box_slic3r); - append_text(wxString::Format(_L( - "If enabled, %s checks for new application versions online. When a new version becomes available, " - "a notification is displayed at the next application startup (never during program usage). " - "This is only a notification mechanisms, no automatic installation is done."), SLIC3R_APP_NAME)); - - append_spacer(VERTICAL_SPACING); - - auto *box_presets = new wxCheckBox(this, wxID_ANY, _L("Update built-in Presets automatically")); - box_presets->SetValue(app_config->get("preset_update") == "1"); - append(box_presets); - append_text(wxString::Format(_L( - "If enabled, %s downloads updates of built-in system presets in the background." - "These updates are downloaded into a separate temporary location." - "When a new preset version becomes available it is offered at application startup."), SLIC3R_APP_NAME)); - const auto text_bold = _L("Updates are never applied without user's consent and never overwrite user's customized settings."); - auto *label_bold = new wxStaticText(this, wxID_ANY, text_bold); - label_bold->SetFont(boldfont); - label_bold->Wrap(WRAP_WIDTH); - append(label_bold); - append_text(_L("Additionally a backup snapshot of the whole configuration is created before an update is applied.")); - - box_slic3r->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent &event) { this->version_check = event.IsChecked(); }); - box_presets->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent &event) { this->preset_update = event.IsChecked(); }); -} - -PageReloadFromDisk::PageReloadFromDisk(ConfigWizard* parent) - : ConfigWizardPage(parent, _L("Reload from disk"), _L("Reload from disk")) - , full_pathnames(false) -{ - auto* box_pathnames = new wxCheckBox(this, wxID_ANY, _L("Export full pathnames of models and parts sources into 3mf and amf files")); - box_pathnames->SetValue(wxGetApp().app_config->get("export_sources_full_pathnames") == "1"); - append(box_pathnames); - append_text(_L( - "If enabled, allows the Reload from disk command to automatically find and load the files when invoked.\n" - "If not enabled, the Reload from disk command will ask to select each file using an open file dialog." - )); - - box_pathnames->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent& event) { this->full_pathnames = event.IsChecked(); }); -} - -#ifdef _WIN32 -PageFilesAssociation::PageFilesAssociation(ConfigWizard* parent) - : ConfigWizardPage(parent, _L("Files association"), _L("Files association")) -{ - cb_3mf = new wxCheckBox(this, wxID_ANY, _L("Associate .3mf files to PrusaSlicer")); - cb_stl = new wxCheckBox(this, wxID_ANY, _L("Associate .stl files to PrusaSlicer")); -// cb_gcode = new wxCheckBox(this, wxID_ANY, _L("Associate .gcode files to PrusaSlicer G-code Viewer")); - - append(cb_3mf); - append(cb_stl); -// append(cb_gcode); -} -#endif // _WIN32 - -PageMode::PageMode(ConfigWizard *parent) - : ConfigWizardPage(parent, _L("View mode"), _L("View mode")) -{ - append_text(_L("PrusaSlicer's user interfaces comes in three variants:\nSimple, Advanced, and Expert.\n" - "The Simple mode shows only the most frequently used settings relevant for regular 3D printing. " - "The other two offer progressively more sophisticated fine-tuning, " - "they are suitable for advanced and expert users, respectively.")); - - radio_simple = new wxRadioButton(this, wxID_ANY, _L("Simple mode")); - radio_advanced = new wxRadioButton(this, wxID_ANY, _L("Advanced mode")); - radio_expert = new wxRadioButton(this, wxID_ANY, _L("Expert mode")); - - std::string mode { "simple" }; - wxGetApp().app_config->get("", "view_mode", mode); - - if (mode == "advanced") { radio_advanced->SetValue(true); } - else if (mode == "expert") { radio_expert->SetValue(true); } - else { radio_simple->SetValue(true); } - - append(radio_simple); - append(radio_advanced); - append(radio_expert); - - append_text("\n" + _L("The size of the object can be specified in inches")); - check_inch = new wxCheckBox(this, wxID_ANY, _L("Use inches")); - check_inch->SetValue(wxGetApp().app_config->get("use_inches") == "1"); - append(check_inch); - - on_activate(); -} - -void PageMode::serialize_mode(AppConfig *app_config) const -{ - std::string mode = ""; - - if (radio_simple->GetValue()) { mode = "simple"; } - if (radio_advanced->GetValue()) { mode = "advanced"; } - if (radio_expert->GetValue()) { mode = "expert"; } - - app_config->set("view_mode", mode); - app_config->set("use_inches", check_inch->GetValue() ? "1" : "0"); -} - -PageVendors::PageVendors(ConfigWizard *parent) - : ConfigWizardPage(parent, _L("Other Vendors"), _L("Other Vendors")) -{ - const AppConfig &appconfig = this->wizard_p()->appconfig_new; - - append_text(wxString::Format(_L("Pick another vendor supported by %s"), SLIC3R_APP_NAME) + ":"); - - auto boldfont = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT); - boldfont.SetWeight(wxFONTWEIGHT_BOLD); - - for (const auto &pair : wizard_p()->bundles) { - const VendorProfile *vendor = pair.second.vendor_profile; - if (vendor->id == PresetBundle::PRUSA_BUNDLE) { continue; } - - auto *cbox = new wxCheckBox(this, wxID_ANY, vendor->name); - cbox->Bind(wxEVT_CHECKBOX, [=](wxCommandEvent &event) { - wizard_p()->on_3rdparty_install(vendor, cbox->IsChecked()); - }); - - const auto &vendors = appconfig.vendors(); - const bool enabled = vendors.find(pair.first) != vendors.end(); - if (enabled) { - cbox->SetValue(true); - - auto pages = wizard_p()->pages_3rdparty.find(vendor->id); - wxCHECK_RET(pages != wizard_p()->pages_3rdparty.end(), "Internal error: 3rd party vendor printers page not created"); - - for (PagePrinters* page : { pages->second.first, pages->second.second }) - if (page) page->install = true; - } - - append(cbox); - } -} - -PageFirmware::PageFirmware(ConfigWizard *parent) - : ConfigWizardPage(parent, _L("Firmware Type"), _L("Firmware"), 1) - , gcode_opt(*print_config_def.get("gcode_flavor")) - , gcode_picker(nullptr) -{ - append_text(_L("Choose the type of firmware used by your printer.")); - append_text(_(gcode_opt.tooltip)); - - wxArrayString choices; - choices.Alloc(gcode_opt.enum_labels.size()); - for (const auto &label : gcode_opt.enum_labels) { - choices.Add(label); - } - - gcode_picker = new wxChoice(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, choices); - wxGetApp().UpdateDarkUI(gcode_picker); - const auto &enum_values = gcode_opt.enum_values; - auto needle = enum_values.cend(); - if (gcode_opt.default_value) { - needle = std::find(enum_values.cbegin(), enum_values.cend(), gcode_opt.default_value->serialize()); - } - if (needle != enum_values.cend()) { - gcode_picker->SetSelection(needle - enum_values.cbegin()); - } else { - gcode_picker->SetSelection(0); - } - - append(gcode_picker); -} - -void PageFirmware::apply_custom_config(DynamicPrintConfig &config) -{ - auto sel = gcode_picker->GetSelection(); - if (sel >= 0 && (size_t)sel < gcode_opt.enum_labels.size()) { - auto *opt = new ConfigOptionEnum(static_cast(sel)); - config.set_key_value("gcode_flavor", opt); - } -} - -PageBedShape::PageBedShape(ConfigWizard *parent) - : ConfigWizardPage(parent, _L("Bed Shape and Size"), _L("Bed Shape"), 1) - , shape_panel(new BedShapePanel(this)) -{ - append_text(_L("Set the shape of your printer's bed.")); - - shape_panel->build_panel(*wizard_p()->custom_config->option("bed_shape"), - *wizard_p()->custom_config->option("bed_custom_texture"), - *wizard_p()->custom_config->option("bed_custom_model")); - - append(shape_panel); -} - -void PageBedShape::apply_custom_config(DynamicPrintConfig &config) -{ - const std::vector& points = shape_panel->get_shape(); - const std::string& custom_texture = shape_panel->get_custom_texture(); - const std::string& custom_model = shape_panel->get_custom_model(); - config.set_key_value("bed_shape", new ConfigOptionPoints(points)); - config.set_key_value("bed_custom_texture", new ConfigOptionString(custom_texture)); - config.set_key_value("bed_custom_model", new ConfigOptionString(custom_model)); -} - -static void focus_event(wxFocusEvent& e, wxTextCtrl* ctrl, double def_value) -{ - e.Skip(); - wxString str = ctrl->GetValue(); - - const char dec_sep = is_decimal_separator_point() ? '.' : ','; - const char dec_sep_alt = dec_sep == '.' ? ',' : '.'; - // Replace the first incorrect separator in decimal number. - bool was_replaced = str.Replace(dec_sep_alt, dec_sep, false) != 0; - - double val = 0.0; - if (!str.ToDouble(&val)) { - if (val == 0.0) - val = def_value; - ctrl->SetValue(double_to_string(val)); - show_error(nullptr, _L("Invalid numeric input.")); - ctrl->SetFocus(); - } - else if (was_replaced) - ctrl->SetValue(double_to_string(val)); -} - -class DiamTextCtrl : public wxTextCtrl -{ -public: - DiamTextCtrl(wxWindow* parent) - { -#ifdef _WIN32 - long style = wxBORDER_SIMPLE; -#else - long style = 0; -#endif - Create(parent, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(Field::def_width_thinner() * wxGetApp().em_unit(), wxDefaultCoord), style); - wxGetApp().UpdateDarkUI(this); - } - ~DiamTextCtrl() {} -}; - -PageDiameters::PageDiameters(ConfigWizard *parent) - : ConfigWizardPage(parent, _L("Filament and Nozzle Diameters"), _L("Print Diameters"), 1) - , diam_nozzle(new DiamTextCtrl(this)) - , diam_filam (new DiamTextCtrl(this)) -{ - auto *default_nozzle = print_config_def.get("nozzle_diameter")->get_default_value(); - wxString value = double_to_string(default_nozzle != nullptr && default_nozzle->size() > 0 ? default_nozzle->get_at(0) : 0.5); - diam_nozzle->SetValue(value); - - auto *default_filam = print_config_def.get("filament_diameter")->get_default_value(); - value = double_to_string(default_filam != nullptr && default_filam->size() > 0 ? default_filam->get_at(0) : 3.0); - diam_filam->SetValue(value); - - diam_nozzle->Bind(wxEVT_KILL_FOCUS, [this](wxFocusEvent& e) { focus_event(e, diam_nozzle, 0.5); }, diam_nozzle->GetId()); - diam_filam ->Bind(wxEVT_KILL_FOCUS, [this](wxFocusEvent& e) { focus_event(e, diam_filam , 3.0); }, diam_filam->GetId()); - - append_text(_L("Enter the diameter of your printer's hot end nozzle.")); - - auto *sizer_nozzle = new wxFlexGridSizer(3, 5, 5); - auto *text_nozzle = new wxStaticText(this, wxID_ANY, _L("Nozzle Diameter:")); - auto *unit_nozzle = new wxStaticText(this, wxID_ANY, _L("mm")); - sizer_nozzle->AddGrowableCol(0, 1); - sizer_nozzle->Add(text_nozzle, 0, wxALIGN_CENTRE_VERTICAL); - sizer_nozzle->Add(diam_nozzle); - sizer_nozzle->Add(unit_nozzle, 0, wxALIGN_CENTRE_VERTICAL); - append(sizer_nozzle); - - append_spacer(VERTICAL_SPACING); - - append_text(_L("Enter the diameter of your filament.")); - append_text(_L("Good precision is required, so use a caliper and do multiple measurements along the filament, then compute the average.")); - - auto *sizer_filam = new wxFlexGridSizer(3, 5, 5); - auto *text_filam = new wxStaticText(this, wxID_ANY, _L("Filament Diameter:")); - auto *unit_filam = new wxStaticText(this, wxID_ANY, _L("mm")); - sizer_filam->AddGrowableCol(0, 1); - sizer_filam->Add(text_filam, 0, wxALIGN_CENTRE_VERTICAL); - sizer_filam->Add(diam_filam); - sizer_filam->Add(unit_filam, 0, wxALIGN_CENTRE_VERTICAL); - append(sizer_filam); -} - -void PageDiameters::apply_custom_config(DynamicPrintConfig &config) -{ - double val = 0.0; - diam_nozzle->GetValue().ToDouble(&val); - auto *opt_nozzle = new ConfigOptionFloats(1, val); - config.set_key_value("nozzle_diameter", opt_nozzle); - - val = 0.0; - diam_filam->GetValue().ToDouble(&val); - auto * opt_filam = new ConfigOptionFloats(1, val); - config.set_key_value("filament_diameter", opt_filam); - - auto set_extrusion_width = [&config, opt_nozzle](const char *key, double dmr) { - char buf[64]; // locales don't matter here (sprintf/atof) - sprintf(buf, "%.2lf", dmr * opt_nozzle->values.front() / 0.4); - config.set_key_value(key, new ConfigOptionFloatOrPercent(atof(buf), false)); - }; - - set_extrusion_width("support_material_extrusion_width", 0.35); - set_extrusion_width("top_infill_extrusion_width", 0.40); - set_extrusion_width("first_layer_extrusion_width", 0.42); - - set_extrusion_width("extrusion_width", 0.45); - set_extrusion_width("perimeter_extrusion_width", 0.45); - set_extrusion_width("external_perimeter_extrusion_width", 0.45); - set_extrusion_width("infill_extrusion_width", 0.45); - set_extrusion_width("solid_infill_extrusion_width", 0.45); -} - -class SpinCtrlDouble: public wxSpinCtrlDouble -{ -public: - SpinCtrlDouble(wxWindow* parent) - { -#ifdef _WIN32 - long style = wxSP_ARROW_KEYS | wxBORDER_SIMPLE; -#else - long style = wxSP_ARROW_KEYS; -#endif - Create(parent, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, style); -#ifdef _WIN32 - wxGetApp().UpdateDarkUI(this->GetText()); -#endif - this->Refresh(); - } - ~SpinCtrlDouble() {} -}; - -PageTemperatures::PageTemperatures(ConfigWizard *parent) - : ConfigWizardPage(parent, _L("Nozzle and Bed Temperatures"), _L("Temperatures"), 1) - , spin_extr(new SpinCtrlDouble(this)) - , spin_bed (new SpinCtrlDouble(this)) -{ - spin_extr->SetIncrement(5.0); - const auto &def_extr = *print_config_def.get("temperature"); - spin_extr->SetRange(def_extr.min, def_extr.max); - auto *default_extr = def_extr.get_default_value(); - spin_extr->SetValue(default_extr != nullptr && default_extr->size() > 0 ? default_extr->get_at(0) : 200); - - spin_bed->SetIncrement(5.0); - const auto &def_bed = *print_config_def.get("bed_temperature"); - spin_bed->SetRange(def_bed.min, def_bed.max); - auto *default_bed = def_bed.get_default_value(); - spin_bed->SetValue(default_bed != nullptr && default_bed->size() > 0 ? default_bed->get_at(0) : 0); - - append_text(_L("Enter the temperature needed for extruding your filament.")); - append_text(_L("A rule of thumb is 160 to 230 °C for PLA, and 215 to 250 °C for ABS.")); - - auto *sizer_extr = new wxFlexGridSizer(3, 5, 5); - auto *text_extr = new wxStaticText(this, wxID_ANY, _L("Extrusion Temperature:")); - auto *unit_extr = new wxStaticText(this, wxID_ANY, _L("°C")); - sizer_extr->AddGrowableCol(0, 1); - sizer_extr->Add(text_extr, 0, wxALIGN_CENTRE_VERTICAL); - sizer_extr->Add(spin_extr); - sizer_extr->Add(unit_extr, 0, wxALIGN_CENTRE_VERTICAL); - append(sizer_extr); - - append_spacer(VERTICAL_SPACING); - - append_text(_L("Enter the bed temperature needed for getting your filament to stick to your heated bed.")); - append_text(_L("A rule of thumb is 60 °C for PLA and 110 °C for ABS. Leave zero if you have no heated bed.")); - - auto *sizer_bed = new wxFlexGridSizer(3, 5, 5); - auto *text_bed = new wxStaticText(this, wxID_ANY, _L("Bed Temperature:")); - auto *unit_bed = new wxStaticText(this, wxID_ANY, _L("°C")); - sizer_bed->AddGrowableCol(0, 1); - sizer_bed->Add(text_bed, 0, wxALIGN_CENTRE_VERTICAL); - sizer_bed->Add(spin_bed); - sizer_bed->Add(unit_bed, 0, wxALIGN_CENTRE_VERTICAL); - append(sizer_bed); -} - -void PageTemperatures::apply_custom_config(DynamicPrintConfig &config) -{ - auto *opt_extr = new ConfigOptionInts(1, spin_extr->GetValue()); - config.set_key_value("temperature", opt_extr); - auto *opt_extr1st = new ConfigOptionInts(1, spin_extr->GetValue()); - config.set_key_value("first_layer_temperature", opt_extr1st); - auto *opt_bed = new ConfigOptionInts(1, spin_bed->GetValue()); - config.set_key_value("bed_temperature", opt_bed); - auto *opt_bed1st = new ConfigOptionInts(1, spin_bed->GetValue()); - config.set_key_value("first_layer_bed_temperature", opt_bed1st); -} - - -// Index - -ConfigWizardIndex::ConfigWizardIndex(wxWindow *parent) - : wxPanel(parent) - , bg(ScalableBitmap(parent, "PrusaSlicer_192px_transparent.png", 192)) - , bullet_black(ScalableBitmap(parent, "bullet_black.png")) - , bullet_blue(ScalableBitmap(parent, "bullet_blue.png")) - , bullet_white(ScalableBitmap(parent, "bullet_white.png")) - , item_active(NO_ITEM) - , item_hover(NO_ITEM) - , last_page((size_t)-1) -{ -#ifndef __WXOSX__ - SetDoubleBuffered(true);// SetDoubleBuffered exists on Win and Linux/GTK, but is missing on OSX -#endif //__WXOSX__ - SetMinSize(bg.bmp().GetSize()); - - const wxSize size = GetTextExtent("m"); - em_w = size.x; - em_h = size.y; - - Bind(wxEVT_PAINT, &ConfigWizardIndex::on_paint, this); - Bind(wxEVT_SIZE, [this](wxEvent& e) { e.Skip(); Refresh(); }); - Bind(wxEVT_MOTION, &ConfigWizardIndex::on_mouse_move, this); - - Bind(wxEVT_LEAVE_WINDOW, [this](wxMouseEvent &evt) { - if (item_hover != -1) { - item_hover = -1; - Refresh(); - } - evt.Skip(); - }); - - Bind(wxEVT_LEFT_UP, [this](wxMouseEvent &evt) { - if (item_hover >= 0) { go_to(item_hover); } - }); -} - -wxDECLARE_EVENT(EVT_INDEX_PAGE, wxCommandEvent); - -void ConfigWizardIndex::add_page(ConfigWizardPage *page) -{ - last_page = items.size(); - items.emplace_back(Item { page->shortname, page->indent, page }); - Refresh(); -} - -void ConfigWizardIndex::add_label(wxString label, unsigned indent) -{ - items.emplace_back(Item { std::move(label), indent, nullptr }); - Refresh(); -} - -ConfigWizardPage* ConfigWizardIndex::active_page() const -{ - if (item_active >= items.size()) { return nullptr; } - - return items[item_active].page; -} - -void ConfigWizardIndex::go_prev() -{ - // Search for a preceiding item that is a page (not a label, ie. page != nullptr) - - if (item_active == NO_ITEM) { return; } - - for (size_t i = item_active; i > 0; i--) { - if (items[i - 1].page != nullptr) { - go_to(i - 1); - return; - } - } -} - -void ConfigWizardIndex::go_next() -{ - // Search for a next item that is a page (not a label, ie. page != nullptr) - - if (item_active == NO_ITEM) { return; } - - for (size_t i = item_active + 1; i < items.size(); i++) { - if (items[i].page != nullptr) { - go_to(i); - return; - } - } -} - -// This one actually performs the go-to op -void ConfigWizardIndex::go_to(size_t i) -{ - if (i != item_active - && i < items.size() - && items[i].page != nullptr) { - auto *new_active = items[i].page; - auto *former_active = active_page(); - if (former_active != nullptr) { - former_active->Hide(); - } - - item_active = i; - new_active->Show(); - - wxCommandEvent evt(EVT_INDEX_PAGE, GetId()); - AddPendingEvent(evt); - - Refresh(); - - new_active->on_activate(); - } -} - -void ConfigWizardIndex::go_to(const ConfigWizardPage *page) -{ - if (page == nullptr) { return; } - - for (size_t i = 0; i < items.size(); i++) { - if (items[i].page == page) { - go_to(i); - return; - } - } -} - -void ConfigWizardIndex::clear() -{ - auto *former_active = active_page(); - if (former_active != nullptr) { former_active->Hide(); } - - items.clear(); - item_active = NO_ITEM; -} - -void ConfigWizardIndex::on_paint(wxPaintEvent & evt) -{ - const auto size = GetClientSize(); - if (size.GetHeight() == 0 || size.GetWidth() == 0) { return; } - - wxPaintDC dc(this); - - const auto bullet_w = bullet_black.bmp().GetSize().GetWidth(); - const auto bullet_h = bullet_black.bmp().GetSize().GetHeight(); - const int yoff_icon = bullet_h < em_h ? (em_h - bullet_h) / 2 : 0; - const int yoff_text = bullet_h > em_h ? (bullet_h - em_h) / 2 : 0; - const int yinc = item_height(); - - int index_width = 0; - - unsigned y = 0; - for (size_t i = 0; i < items.size(); i++) { - const Item& item = items[i]; - unsigned x = em_w/2 + item.indent * em_w; - - if (i == item_active || (item_hover >= 0 && i == (size_t)item_hover)) { - dc.DrawBitmap(bullet_blue.bmp(), x, y + yoff_icon, false); - } - else if (i < item_active) { dc.DrawBitmap(bullet_black.bmp(), x, y + yoff_icon, false); } - else if (i > item_active) { dc.DrawBitmap(bullet_white.bmp(), x, y + yoff_icon, false); } - - x += + bullet_w + em_w/2; - const auto text_size = dc.GetTextExtent(item.label); - dc.SetTextForeground(wxGetApp().get_label_clr_default()); - dc.DrawText(item.label, x, y + yoff_text); - - y += yinc; - index_width = std::max(index_width, (int)x + text_size.x); - } - - //draw logo - if (int y = size.y - bg.GetBmpHeight(); y>=0) { - dc.DrawBitmap(bg.bmp(), 0, y, false); - index_width = std::max(index_width, bg.GetBmpWidth() + em_w / 2); - } - - if (GetMinSize().x < index_width) { - CallAfter([this, index_width]() { - SetMinSize(wxSize(index_width, GetMinSize().y)); - Refresh(); - }); - } -} - -void ConfigWizardIndex::on_mouse_move(wxMouseEvent &evt) -{ - const wxClientDC dc(this); - const wxPoint pos = evt.GetLogicalPosition(dc); - - const ssize_t item_hover_new = pos.y / item_height(); - - if (item_hover_new < ssize_t(items.size()) && item_hover_new != item_hover) { - item_hover = item_hover_new; - Refresh(); - } - - evt.Skip(); -} - -void ConfigWizardIndex::msw_rescale() -{ - const wxSize size = GetTextExtent("m"); - em_w = size.x; - em_h = size.y; - - bg.msw_rescale(); - SetMinSize(bg.bmp().GetSize()); - - bullet_black.msw_rescale(); - bullet_blue.msw_rescale(); - bullet_white.msw_rescale(); - Refresh(); -} - - -// Materials - -const std::string Materials::UNKNOWN = "(Unknown)"; - -void Materials::push(const Preset *preset) -{ - presets.emplace_back(preset); - types.insert(technology & T_FFF - ? Materials::get_filament_type(preset) - : Materials::get_material_type(preset)); -} - -void Materials::add_printer(const Preset* preset) -{ - printers.insert(preset); -} - -void Materials::clear() -{ - presets.clear(); - types.clear(); - printers.clear(); - compatibility_counter.clear(); -} - -const std::string& Materials::appconfig_section() const -{ - return (technology & T_FFF) ? AppConfig::SECTION_FILAMENTS : AppConfig::SECTION_MATERIALS; -} - -const std::string& Materials::get_type(const Preset *preset) const -{ - return (technology & T_FFF) ? get_filament_type(preset) : get_material_type(preset); -} - -const std::string& Materials::get_vendor(const Preset *preset) const -{ - return (technology & T_FFF) ? get_filament_vendor(preset) : get_material_vendor(preset); -} - -const std::string& Materials::get_filament_type(const Preset *preset) -{ - const auto *opt = preset->config.opt("filament_type"); - if (opt != nullptr && opt->values.size() > 0) { - return opt->values[0]; - } else { - return UNKNOWN; - } -} - -const std::string& Materials::get_filament_vendor(const Preset *preset) -{ - const auto *opt = preset->config.opt("filament_vendor"); - return opt != nullptr ? opt->value : UNKNOWN; -} - -const std::string& Materials::get_material_type(const Preset *preset) -{ - const auto *opt = preset->config.opt("material_type"); - if (opt != nullptr) { - return opt->value; - } else { - return UNKNOWN; - } -} - -const std::string& Materials::get_material_vendor(const Preset *preset) -{ - const auto *opt = preset->config.opt("material_vendor"); - return opt != nullptr ? opt->value : UNKNOWN; -} - -// priv - -static const std::unordered_map> legacy_preset_map {{ - { "Original Prusa i3 MK2.ini", std::make_pair("MK2S", "0.4") }, - { "Original Prusa i3 MK2 MM Single Mode.ini", std::make_pair("MK2SMM", "0.4") }, - { "Original Prusa i3 MK2 MM Single Mode 0.6 nozzle.ini", std::make_pair("MK2SMM", "0.6") }, - { "Original Prusa i3 MK2 MultiMaterial.ini", std::make_pair("MK2SMM", "0.4") }, - { "Original Prusa i3 MK2 MultiMaterial 0.6 nozzle.ini", std::make_pair("MK2SMM", "0.6") }, - { "Original Prusa i3 MK2 0.25 nozzle.ini", std::make_pair("MK2S", "0.25") }, - { "Original Prusa i3 MK2 0.6 nozzle.ini", std::make_pair("MK2S", "0.6") }, - { "Original Prusa i3 MK3.ini", std::make_pair("MK3", "0.4") }, -}}; - -void ConfigWizard::priv::load_pages() -{ - wxWindowUpdateLocker freeze_guard(q); - (void)freeze_guard; - - const ConfigWizardPage *former_active = index->active_page(); - - index->clear(); - - index->add_page(page_welcome); - - // Printers - if (!only_sla_mode) - index->add_page(page_fff); - index->add_page(page_msla); - if (!only_sla_mode) { - index->add_page(page_vendors); - for (const auto &pages : pages_3rdparty) { - for ( PagePrinters* page : { pages.second.first, pages.second.second }) - if (page && page->install) - index->add_page(page); - } - - index->add_page(page_custom); - if (page_custom->custom_wanted()) { - index->add_page(page_firmware); - index->add_page(page_bed); - index->add_page(page_diams); - index->add_page(page_temps); - } - - // Filaments & Materials - if (any_fff_selected) { index->add_page(page_filaments); } - } - if (any_sla_selected) { index->add_page(page_sla_materials); } - - // there should to be selected at least one printer - btn_finish->Enable(any_fff_selected || any_sla_selected || custom_printer_selected); - - index->add_page(page_update); - index->add_page(page_reload_from_disk); -#ifdef _WIN32 - index->add_page(page_files_association); -#endif // _WIN32 - index->add_page(page_mode); - - index->go_to(former_active); // Will restore the active item/page if possible - - q->Layout(); -// This Refresh() is needed to avoid ugly artifacts after printer selection, when no one vendor was selected from the very beginnig - q->Refresh(); -} - -void ConfigWizard::priv::init_dialog_size() -{ - // Clamp the Wizard size based on screen dimensions - - const auto idx = wxDisplay::GetFromWindow(q); - wxDisplay display(idx != wxNOT_FOUND ? idx : 0u); - - const auto disp_rect = display.GetClientArea(); - wxRect window_rect( - disp_rect.x + disp_rect.width / 20, - disp_rect.y + disp_rect.height / 20, - 9*disp_rect.width / 10, - 9*disp_rect.height / 10); - - const int width_hint = index->GetSize().GetWidth() + std::max(90 * em(), (only_sla_mode ? page_msla->get_width() : page_fff->get_width()) + 30 * em()); // XXX: magic constant, I found no better solution - if (width_hint < window_rect.width) { - window_rect.x += (window_rect.width - width_hint) / 2; - window_rect.width = width_hint; - } - - q->SetSize(window_rect); -} - -void ConfigWizard::priv::load_vendors() -{ - bundles = BundleMap::load(); - - // Load up the set of vendors / models / variants the user has had enabled up till now - AppConfig *app_config = wxGetApp().app_config; - if (! app_config->legacy_datadir()) { - appconfig_new.set_vendors(*app_config); - } else { - // In case of legacy datadir, try to guess the preference based on the printer preset files that are present - const auto printer_dir = fs::path(Slic3r::data_dir()) / "printer"; - for (auto &dir_entry : boost::filesystem::directory_iterator(printer_dir)) - if (Slic3r::is_ini_file(dir_entry)) { - auto needle = legacy_preset_map.find(dir_entry.path().filename().string()); - if (needle == legacy_preset_map.end()) { continue; } - - const auto &model = needle->second.first; - const auto &variant = needle->second.second; - appconfig_new.set_variant("PrusaResearch", model, variant, true); - } - } - - // Initialize the is_visible flag in printer Presets - for (auto &pair : bundles) { - pair.second.preset_bundle->load_installed_printers(appconfig_new); - } - - // Copy installed filaments and SLA material names from app_config to appconfig_new - // while resolving current names of profiles, which were renamed in the meantime. - for (PrinterTechnology technology : { ptFFF, ptSLA }) { - const std::string §ion_name = (technology == ptFFF) ? AppConfig::SECTION_FILAMENTS : AppConfig::SECTION_MATERIALS; - std::map section_new; - if (app_config->has_section(section_name)) { - const std::map §ion_old = app_config->get_section(section_name); - for (const auto& material_name_and_installed : section_old) - if (material_name_and_installed.second == "1") { - // Material is installed. Resolve it in bundles. - size_t num_found = 0; - const std::string &material_name = material_name_and_installed.first; - for (auto &bundle : bundles) { - const PresetCollection &materials = bundle.second.preset_bundle->materials(technology); - const Preset *preset = materials.find_preset(material_name); - if (preset == nullptr) { - // Not found. Maybe the material preset is there, bu it was was renamed? - const std::string *new_name = materials.get_preset_name_renamed(material_name); - if (new_name != nullptr) - preset = materials.find_preset(*new_name); - } - if (preset != nullptr) { - // Materal preset was found, mark it as installed. - section_new[preset->name] = "1"; - ++ num_found; - } - } - if (num_found == 0) - BOOST_LOG_TRIVIAL(error) << boost::format("Profile %1% was not found in installed vendor Preset Bundles.") % material_name; - else if (num_found > 1) - BOOST_LOG_TRIVIAL(error) << boost::format("Profile %1% was found in %2% vendor Preset Bundles.") % material_name % num_found; - } - } - appconfig_new.set_section(section_name, section_new); - }; -} - -void ConfigWizard::priv::add_page(ConfigWizardPage *page) -{ - const int proportion = (page->shortname == _L("Filaments")) || (page->shortname == _L("SLA Materials")) ? 1 : 0; - hscroll_sizer->Add(page, proportion, wxEXPAND); - all_pages.push_back(page); -} - -void ConfigWizard::priv::enable_next(bool enable) -{ - btn_next->Enable(enable); - btn_finish->Enable(enable); -} - -void ConfigWizard::priv::set_start_page(ConfigWizard::StartPage start_page) -{ - switch (start_page) { - case ConfigWizard::SP_PRINTERS: - index->go_to(page_fff); - btn_next->SetFocus(); - break; - case ConfigWizard::SP_FILAMENTS: - index->go_to(page_filaments); - btn_finish->SetFocus(); - break; - case ConfigWizard::SP_MATERIALS: - index->go_to(page_sla_materials); - btn_finish->SetFocus(); - break; - default: - index->go_to(page_welcome); - btn_next->SetFocus(); - break; - } -} - -void ConfigWizard::priv::create_3rdparty_pages() -{ - for (const auto &pair : bundles) { - const VendorProfile *vendor = pair.second.vendor_profile; - if (vendor->id == PresetBundle::PRUSA_BUNDLE) { continue; } - - bool is_fff_technology = false; - bool is_sla_technology = false; - - for (auto& model: vendor->models) - { - if (!is_fff_technology && model.technology == ptFFF) - is_fff_technology = true; - if (!is_sla_technology && model.technology == ptSLA) - is_sla_technology = true; - } - - PagePrinters* pageFFF = nullptr; - PagePrinters* pageSLA = nullptr; - - if (is_fff_technology) { - pageFFF = new PagePrinters(q, vendor->name + " " +_L("FFF Technology Printers"), vendor->name+" FFF", *vendor, 1, T_FFF); - add_page(pageFFF); - } - - if (is_sla_technology) { - pageSLA = new PagePrinters(q, vendor->name + " " + _L("SLA Technology Printers"), vendor->name+" MSLA", *vendor, 1, T_SLA); - add_page(pageSLA); - } - - pages_3rdparty.insert({vendor->id, {pageFFF, pageSLA}}); - } -} - -void ConfigWizard::priv::set_run_reason(RunReason run_reason) -{ - this->run_reason = run_reason; - for (auto &page : all_pages) { - page->set_run_reason(run_reason); - } -} - -void ConfigWizard::priv::update_materials(Technology technology) -{ - if (any_fff_selected && (technology & T_FFF)) { - filaments.clear(); - aliases_fff.clear(); - // Iterate filaments in all bundles - for (const auto &pair : bundles) { - for (const auto &filament : pair.second.preset_bundle->filaments) { - // Check if filament is already added - if (filaments.containts(&filament)) - continue; - // Iterate printers in all bundles - for (const auto &printer : pair.second.preset_bundle->printers) { - if (!printer.is_visible || printer.printer_technology() != ptFFF) - continue; - // Filter out inapplicable printers - if (is_compatible_with_printer(PresetWithVendorProfile(filament, filament.vendor), PresetWithVendorProfile(printer, printer.vendor))) { - if (!filaments.containts(&filament)) { - filaments.push(&filament); - if (!filament.alias.empty()) - aliases_fff[filament.alias].insert(filament.name); - } - filaments.add_printer(&printer); - } - } - - } - } - // count compatible printers - for (const auto& preset : filaments.presets) { - - const auto filter = [preset](const std::pair element) { - return preset->alias == element.first; - }; - if (std::find_if(filaments.compatibility_counter.begin(), filaments.compatibility_counter.end(), filter) != filaments.compatibility_counter.end()) { - continue; - } - std::vector idx_with_same_alias; - for (size_t i = 0; i < filaments.presets.size(); ++i) { - if (preset->alias == filaments.presets[i]->alias) - idx_with_same_alias.push_back(i); - } - size_t counter = 0; - for (const auto& printer : filaments.printers) { - if (!(*printer).is_visible || (*printer).printer_technology() != ptFFF) - continue; - bool compatible = false; - // Test otrher materials with same alias - for (size_t i = 0; i < idx_with_same_alias.size() && !compatible; ++i) { - const Preset& prst = *(filaments.presets[idx_with_same_alias[i]]); - const Preset& prntr = *printer; - if (is_compatible_with_printer(PresetWithVendorProfile(prst, prst.vendor), PresetWithVendorProfile(prntr, prntr.vendor))) { - compatible = true; - break; - } - } - if (compatible) - counter++; - } - filaments.compatibility_counter.emplace_back(preset->alias, counter); - } - } - - if (any_sla_selected && (technology & T_SLA)) { - sla_materials.clear(); - aliases_sla.clear(); - - // Iterate SLA materials in all bundles - for (const auto &pair : bundles) { - for (const auto &material : pair.second.preset_bundle->sla_materials) { - // Check if material is already added - if (sla_materials.containts(&material)) - continue; - // Iterate printers in all bundles - // For now, we only allow the profiles to be compatible with another profiles inside the same bundle. - for (const auto& printer : pair.second.preset_bundle->printers) { - if(!printer.is_visible || printer.printer_technology() != ptSLA) - continue; - // Filter out inapplicable printers - if (is_compatible_with_printer(PresetWithVendorProfile(material, nullptr), PresetWithVendorProfile(printer, nullptr))) { - // Check if material is already added - if(!sla_materials.containts(&material)) { - sla_materials.push(&material); - if (!material.alias.empty()) - aliases_sla[material.alias].insert(material.name); - } - sla_materials.add_printer(&printer); - } - } - } - } - // count compatible printers - for (const auto& preset : sla_materials.presets) { - - const auto filter = [preset](const std::pair element) { - return preset->alias == element.first; - }; - if (std::find_if(sla_materials.compatibility_counter.begin(), sla_materials.compatibility_counter.end(), filter) != sla_materials.compatibility_counter.end()) { - continue; - } - std::vector idx_with_same_alias; - for (size_t i = 0; i < sla_materials.presets.size(); ++i) { - if(preset->alias == sla_materials.presets[i]->alias) - idx_with_same_alias.push_back(i); - } - size_t counter = 0; - for (const auto& printer : sla_materials.printers) { - if (!(*printer).is_visible || (*printer).printer_technology() != ptSLA) - continue; - bool compatible = false; - // Test otrher materials with same alias - for (size_t i = 0; i < idx_with_same_alias.size() && !compatible; ++i) { - const Preset& prst = *(sla_materials.presets[idx_with_same_alias[i]]); - const Preset& prntr = *printer; - if (is_compatible_with_printer(PresetWithVendorProfile(prst, prst.vendor), PresetWithVendorProfile(prntr, prntr.vendor))) { - compatible = true; - break; - } - } - if (compatible) - counter++; - } - sla_materials.compatibility_counter.emplace_back(preset->alias, counter); - } - } -} - -void ConfigWizard::priv::on_custom_setup(const bool custom_wanted) -{ - custom_printer_selected = custom_wanted; - load_pages(); -} - -void ConfigWizard::priv::on_printer_pick(PagePrinters *page, const PrinterPickerEvent &evt) -{ - if (check_sla_selected() != any_sla_selected || - check_fff_selected() != any_fff_selected) { - any_fff_selected = check_fff_selected(); - any_sla_selected = check_sla_selected(); - - load_pages(); - } - - // Update the is_visible flag on relevant printer profiles - for (auto &pair : bundles) { - if (pair.first != evt.vendor_id) { continue; } - - for (auto &preset : pair.second.preset_bundle->printers) { - if (preset.config.opt_string("printer_model") == evt.model_id - && preset.config.opt_string("printer_variant") == evt.variant_name) { - preset.is_visible = evt.enable; - } - } - - // When a printer model is picked, but there is no material installed compatible with this printer model, - // install default materials for selected printer model silently. - check_and_install_missing_materials(page->technology, evt.model_id); - } - - if (page->technology & T_FFF) { - page_filaments->clear(); - } else if (page->technology & T_SLA) { - page_sla_materials->clear(); - } -} - -void ConfigWizard::priv::select_default_materials_for_printer_model(const VendorProfile::PrinterModel &printer_model, Technology technology) -{ - PageMaterials* page_materials = technology & T_FFF ? page_filaments : page_sla_materials; - for (const std::string& material : printer_model.default_materials) - appconfig_new.set(page_materials->materials->appconfig_section(), material, "1"); -} - -void ConfigWizard::priv::select_default_materials_for_printer_models(Technology technology, const std::set &printer_models) -{ - PageMaterials *page_materials = technology & T_FFF ? page_filaments : page_sla_materials; - const std::string &appconfig_section = page_materials->materials->appconfig_section(); - - // Following block was unnecessary. Its enough to iterate printer_models once. Not for every vendor printer page. - // Filament is selected on same page for all printers of same technology. - /* - auto select_default_materials_for_printer_page = [this, appconfig_section, printer_models, technology](PagePrinters *page_printers, Technology technology) - { - const std::string vendor_id = page_printers->get_vendor_id(); - for (auto& pair : bundles) - if (pair.first == vendor_id) - for (const VendorProfile::PrinterModel *printer_model : printer_models) - for (const std::string &material : printer_model->default_materials) - appconfig_new.set(appconfig_section, material, "1"); - }; - - PagePrinters* page_printers = technology & T_FFF ? page_fff : page_msla; - select_default_materials_for_printer_page(page_printers, technology); - - for (const auto& printer : pages_3rdparty) - { - page_printers = technology & T_FFF ? printer.second.first : printer.second.second; - if (page_printers) - select_default_materials_for_printer_page(page_printers, technology); - } - */ - - // Iterate printer_models and select default materials. If none available -> msg to user. - std::vector models_without_default; - for (const VendorProfile::PrinterModel* printer_model : printer_models) { - if (printer_model->default_materials.empty()) { - models_without_default.emplace_back(printer_model); - } else { - for (const std::string& material : printer_model->default_materials) - appconfig_new.set(appconfig_section, material, "1"); - } - } - - if (!models_without_default.empty()) { - std::string printer_names = "\n\n"; - for (const VendorProfile::PrinterModel* printer_model : models_without_default) { - printer_names += printer_model->name + "\n"; - } - printer_names += "\n\n"; - std::string message = (technology & T_FFF ? - GUI::format(_L("Following printer profiles has no default filament: %1%Please select one manually."), printer_names) : - GUI::format(_L("Following printer profiles has no default material: %1%Please select one manually."), printer_names)); - MessageDialog msg(q, message, _L("Notice"), wxOK); - msg.ShowModal(); - } - - update_materials(technology); - ((technology & T_FFF) ? page_filaments : page_sla_materials)->reload_presets(); -} - -void ConfigWizard::priv::on_3rdparty_install(const VendorProfile *vendor, bool install) -{ - auto it = pages_3rdparty.find(vendor->id); - wxCHECK_RET(it != pages_3rdparty.end(), "Internal error: GUI page not found for 3rd party vendor profile"); - - for (PagePrinters* page : { it->second.first, it->second.second }) - if (page) { - if (page->install && !install) - page->select_all(false); - page->install = install; - // if some 3rd vendor is selected, select first printer for them - if (install) - page->printer_pickers[0]->select_one(0, true); - page->Layout(); - } - - load_pages(); -} - -bool ConfigWizard::priv::on_bnt_finish() -{ - wxBusyCursor wait; - /* When Filaments or Sla Materials pages are activated, - * materials for this pages are automaticaly updated and presets are reloaded. - * - * But, if _Finish_ button was clicked without activation of those pages - * (for example, just some printers were added/deleted), - * than last changes wouldn't be updated for filaments/materials. - * SO, do that before close of Wizard - */ - update_materials(T_ANY); - if (any_fff_selected) - page_filaments->reload_presets(); - if (any_sla_selected) - page_sla_materials->reload_presets(); - - // theres no need to check that filament is selected if we have only custom printer - if (custom_printer_selected && !any_fff_selected && !any_sla_selected) return true; - // check, that there is selected at least one filament/material - return check_and_install_missing_materials(T_ANY); -} - -// This allmighty method verifies, whether there is at least a single compatible filament or SLA material installed -// for each Printer preset of each Printer Model installed. -// -// In case only_for_model_id is set, then the test is done for that particular printer model only, and the default materials are installed silently. -// Otherwise the user is quieried whether to install the missing default materials or not. -// -// Return true if the tested Printer Models already had materials installed. -// Return false if there were some Printer Models with missing materials, independent from whether the defaults were installed for these -// respective Printer Models or not. -bool ConfigWizard::priv::check_and_install_missing_materials(Technology technology, const std::string &only_for_model_id) -{ - // Walk over all installed Printer presets and verify whether there is a filament or SLA material profile installed at the same PresetBundle, - // which is compatible with it. - const auto printer_models_missing_materials = [this, only_for_model_id](PrinterTechnology technology, const std::string §ion) - { - const std::map &appconfig_presets = appconfig_new.has_section(section) ? appconfig_new.get_section(section) : std::map(); - std::set printer_models_without_material; - for (const auto &pair : bundles) { - const PresetCollection &materials = pair.second.preset_bundle->materials(technology); - for (const auto &printer : pair.second.preset_bundle->printers) { - if (printer.is_visible && printer.printer_technology() == technology) { - const VendorProfile::PrinterModel *printer_model = PresetUtils::system_printer_model(printer); - assert(printer_model != nullptr); - if ((only_for_model_id.empty() || only_for_model_id == printer_model->id) && - printer_models_without_material.find(printer_model) == printer_models_without_material.end()) { - bool has_material = false; - for (const auto& preset : appconfig_presets) { - if (preset.second == "1") { - const Preset *material = materials.find_preset(preset.first, false); - if (material != nullptr && is_compatible_with_printer(PresetWithVendorProfile(*material, nullptr), PresetWithVendorProfile(printer, nullptr))) { - has_material = true; - break; - } - } - } - if (! has_material) - printer_models_without_material.insert(printer_model); - } - } - } - } - assert(printer_models_without_material.empty() || only_for_model_id.empty() || only_for_model_id == (*printer_models_without_material.begin())->id); - return printer_models_without_material; - }; - - const auto ask_and_select_default_materials = [this](const wxString &message, const std::set &printer_models, Technology technology) - { - //wxMessageDialog msg(q, message, _L("Notice"), wxYES_NO); - MessageDialog msg(q, message, _L("Notice"), wxYES_NO); - if (msg.ShowModal() == wxID_YES) - select_default_materials_for_printer_models(technology, printer_models); - }; - - const auto printer_model_list = [](const std::set &printer_models) -> wxString { - wxString out; - for (const VendorProfile::PrinterModel *printer_model : printer_models) { - wxString name = from_u8(printer_model->name); - out += "\t\t"; - out += name; - out += "\n"; - } - return out; - }; - - if (any_fff_selected && (technology & T_FFF)) { - std::set printer_models_without_material = printer_models_missing_materials(ptFFF, AppConfig::SECTION_FILAMENTS); - if (! printer_models_without_material.empty()) { - if (only_for_model_id.empty()) - ask_and_select_default_materials( - _L("The following FFF printer models have no filament selected:") + - "\n\n\t" + - printer_model_list(printer_models_without_material) + - "\n\n\t" + - _L("Do you want to select default filaments for these FFF printer models?"), - printer_models_without_material, - T_FFF); - else - select_default_materials_for_printer_model(**printer_models_without_material.begin(), T_FFF); - return false; - } - } - - if (any_sla_selected && (technology & T_SLA)) { - std::set printer_models_without_material = printer_models_missing_materials(ptSLA, AppConfig::SECTION_MATERIALS); - if (! printer_models_without_material.empty()) { - if (only_for_model_id.empty()) - ask_and_select_default_materials( - _L("The following SLA printer models have no materials selected:") + - "\n\n\t" + - printer_model_list(printer_models_without_material) + - "\n\n\t" + - _L("Do you want to select default SLA materials for these printer models?"), - printer_models_without_material, - T_SLA); - else - select_default_materials_for_printer_model(**printer_models_without_material.begin(), T_SLA); - return false; - } - } - - return true; -} - -static std::set get_new_added_presets(const std::map& old_data, const std::map& new_data) -{ - auto get_aliases = [](const std::map& data) { - std::set old_aliases; - for (auto item : data) { - const std::string& name = item.first; - size_t pos = name.find("@"); - old_aliases.emplace(pos == std::string::npos ? name : name.substr(0, pos-1)); - } - return old_aliases; - }; - - std::set old_aliases = get_aliases(old_data); - std::set new_aliases = get_aliases(new_data); - std::set diff; - std::set_difference(new_aliases.begin(), new_aliases.end(), old_aliases.begin(), old_aliases.end(), std::inserter(diff, diff.begin())); - - return diff; -} - -static std::string get_first_added_preset(const std::map& old_data, const std::map& new_data) -{ - std::set diff = get_new_added_presets(old_data, new_data); - if (diff.empty()) - return std::string(); - return *diff.begin(); -} - -bool ConfigWizard::priv::apply_config(AppConfig *app_config, PresetBundle *preset_bundle, const PresetUpdater *updater, bool& apply_keeped_changes) -{ - wxString header, caption = _L("Configuration is edited in ConfigWizard"); - const auto enabled_vendors = appconfig_new.vendors(); - const auto enabled_vendors_old = app_config->vendors(); - - bool suppress_sla_printer = model_has_multi_part_objects(wxGetApp().model()); - PrinterTechnology preferred_pt = ptAny; - auto get_preferred_printer_technology = [enabled_vendors, enabled_vendors_old, suppress_sla_printer](const std::string& bundle_name, const Bundle& bundle) { - const auto config = enabled_vendors.find(bundle_name); - PrinterTechnology pt = ptAny; - if (config != enabled_vendors.end()) { - for (const auto& model : bundle.vendor_profile->models) { - if (const auto model_it = config->second.find(model.id); - model_it != config->second.end() && model_it->second.size() > 0) { - pt = model.technology; - const auto config_old = enabled_vendors_old.find(bundle_name); - if (config_old == enabled_vendors_old.end() || config_old->second.find(model.id) == config_old->second.end()) { - // if preferred printer model has SLA printer technology it's important to check the model for multi-part state - if (pt == ptSLA && suppress_sla_printer) - continue; - return pt; - } - - if (const auto model_it_old = config_old->second.find(model.id); - model_it_old == config_old->second.end() || model_it_old->second != model_it->second) { - // if preferred printer model has SLA printer technology it's important to check the model for multi-part state - if (pt == ptSLA && suppress_sla_printer) - continue; - return pt; - } - } - } - } - return pt; - }; - // Prusa printers are considered first, then 3rd party. - if (preferred_pt = get_preferred_printer_technology("PrusaResearch", bundles.prusa_bundle()); - preferred_pt == ptAny || (preferred_pt == ptSLA && suppress_sla_printer)) { - for (const auto& bundle : bundles) { - if (bundle.second.is_prusa_bundle) { continue; } - if (PrinterTechnology pt = get_preferred_printer_technology(bundle.first, bundle.second); pt == ptAny) - continue; - else if (preferred_pt == ptAny) - preferred_pt = pt; - if(!(preferred_pt == ptAny || (preferred_pt == ptSLA && suppress_sla_printer))) - break; - } - } - - if (preferred_pt == ptSLA && !wxGetApp().may_switch_to_SLA_preset(caption)) - return false; - - bool check_unsaved_preset_changes = page_welcome->reset_user_profile(); - if (check_unsaved_preset_changes) - header = _L("All user presets will be deleted."); - int act_btns = UnsavedChangesDialog::ActionButtons::KEEP; - if (!check_unsaved_preset_changes) - act_btns |= UnsavedChangesDialog::ActionButtons::SAVE; - - // Install bundles from resources if needed: - std::vector install_bundles; - for (const auto &pair : bundles) { - if (! pair.second.is_in_resources) { continue; } - - if (pair.second.is_prusa_bundle) { - // Always install Prusa bundle, because it has a lot of filaments/materials - // likely to be referenced by other profiles. - install_bundles.emplace_back(pair.first); - continue; - } - - const auto vendor = enabled_vendors.find(pair.first); - if (vendor == enabled_vendors.end()) { continue; } - - size_t size_sum = 0; - for (const auto &model : vendor->second) { size_sum += model.second.size(); } - - if (size_sum > 0) { - // This vendor needs to be installed - install_bundles.emplace_back(pair.first); - } - } - if (!check_unsaved_preset_changes) - if ((check_unsaved_preset_changes = install_bundles.size() > 0)) - header = _L_PLURAL("A new vendor was installed and one of its printers will be activated", "New vendors were installed and one of theirs printers will be activated", install_bundles.size()); - -#ifdef __linux__ - // Desktop integration on Linux - if (page_welcome->integrate_desktop()) - DesktopIntegrationDialog::perform_desktop_integration(); -#endif - - // Decide whether to create snapshot based on run_reason and the reset profile checkbox - bool snapshot = true; - Snapshot::Reason snapshot_reason = Snapshot::SNAPSHOT_UPGRADE; - switch (run_reason) { - case ConfigWizard::RR_DATA_EMPTY: - snapshot = false; - break; - case ConfigWizard::RR_DATA_LEGACY: - snapshot = true; - break; - case ConfigWizard::RR_DATA_INCOMPAT: - // In this case snapshot has already been taken by - // PresetUpdater with the appropriate reason - snapshot = false; - break; - case ConfigWizard::RR_USER: - snapshot = page_welcome->reset_user_profile(); - snapshot_reason = Snapshot::SNAPSHOT_USER; - break; - } - - if (snapshot && ! take_config_snapshot_cancel_on_error(*app_config, snapshot_reason, "", _u8L("Do you want to continue changing the configuration?"))) - return false; - - if (check_unsaved_preset_changes && - !wxGetApp().check_and_keep_current_preset_changes(caption, header, act_btns, &apply_keeped_changes)) - return false; - - if (install_bundles.size() > 0) { - // Install bundles from resources. - // Don't create snapshot - we've already done that above if applicable. - if (! updater->install_bundles_rsrc(std::move(install_bundles), false)) - return false; - } else { - BOOST_LOG_TRIVIAL(info) << "No bundles need to be installed from resources"; - } - - if (page_welcome->reset_user_profile()) { - BOOST_LOG_TRIVIAL(info) << "Resetting user profiles..."; - preset_bundle->reset(true); - } - - std::string preferred_model; - std::string preferred_variant; - auto get_preferred_printer_model = [enabled_vendors, enabled_vendors_old, preferred_pt](const std::string& bundle_name, const Bundle& bundle, std::string& variant) { - const auto config = enabled_vendors.find(bundle_name); - if (config == enabled_vendors.end()) - return std::string(); - for (const auto& model : bundle.vendor_profile->models) { - if (const auto model_it = config->second.find(model.id); - model_it != config->second.end() && model_it->second.size() > 0 && - preferred_pt == model.technology) { - variant = *model_it->second.begin(); - const auto config_old = enabled_vendors_old.find(bundle_name); - if (config_old == enabled_vendors_old.end()) - return model.id; - const auto model_it_old = config_old->second.find(model.id); - if (model_it_old == config_old->second.end()) - return model.id; - else if (model_it_old->second != model_it->second) { - for (const auto& var : model_it->second) - if (model_it_old->second.find(var) == model_it_old->second.end()) { - variant = var; - return model.id; - } - } - } - } - if (!variant.empty()) - variant.clear(); - return std::string(); - }; - // Prusa printers are considered first, then 3rd party. - if (preferred_model = get_preferred_printer_model("PrusaResearch", bundles.prusa_bundle(), preferred_variant); - preferred_model.empty()) { - for (const auto& bundle : bundles) { - if (bundle.second.is_prusa_bundle) { continue; } - if (preferred_model = get_preferred_printer_model(bundle.first, bundle.second, preferred_variant); - !preferred_model.empty()) - break; - } - } - - // if unsaved changes was not cheched till this moment - if (!check_unsaved_preset_changes) { - if ((check_unsaved_preset_changes = !preferred_model.empty())) { - header = _L("A new Printer was installed and it will be activated."); - if (!wxGetApp().check_and_keep_current_preset_changes(caption, header, act_btns, &apply_keeped_changes)) - return false; - } - else if ((check_unsaved_preset_changes = enabled_vendors_old != enabled_vendors)) { - header = _L("Some Printers were uninstalled."); - if (!wxGetApp().check_and_keep_current_preset_changes(caption, header, act_btns, &apply_keeped_changes)) - return false; - } - } - - std::string first_added_filament, first_added_sla_material; - auto get_first_added_material_preset = [this, app_config](const std::string& section_name, std::string& first_added_preset) { - if (appconfig_new.has_section(section_name)) { - // get first of new added preset names - const std::map& old_presets = app_config->has_section(section_name) ? app_config->get_section(section_name) : std::map(); - first_added_preset = get_first_added_preset(old_presets, appconfig_new.get_section(section_name)); - } - }; - get_first_added_material_preset(AppConfig::SECTION_FILAMENTS, first_added_filament); - get_first_added_material_preset(AppConfig::SECTION_MATERIALS, first_added_sla_material); - - // if unsaved changes was not cheched till this moment - if (!check_unsaved_preset_changes) { - if ((check_unsaved_preset_changes = !first_added_filament.empty() || !first_added_sla_material.empty())) { - header = !first_added_filament.empty() ? - _L("A new filament was installed and it will be activated.") : - _L("A new SLA material was installed and it will be activated."); - if (!wxGetApp().check_and_keep_current_preset_changes(caption, header, act_btns, &apply_keeped_changes)) - return false; - } - else { - auto changed = [app_config, &appconfig_new = std::as_const(this->appconfig_new)](const std::string& section_name) { - return (app_config->has_section(section_name) ? app_config->get_section(section_name) : std::map()) != appconfig_new.get_section(section_name); - }; - bool is_filaments_changed = changed(AppConfig::SECTION_FILAMENTS); - bool is_sla_materials_changed = changed(AppConfig::SECTION_MATERIALS); - if ((check_unsaved_preset_changes = is_filaments_changed || is_sla_materials_changed)) { - header = is_filaments_changed ? _L("Some filaments were uninstalled.") : _L("Some SLA materials were uninstalled."); - if (!wxGetApp().check_and_keep_current_preset_changes(caption, header, act_btns, &apply_keeped_changes)) - return false; - } - } - } - - // apply materials in app_config - for (const std::string& section_name : {AppConfig::SECTION_FILAMENTS, AppConfig::SECTION_MATERIALS}) - app_config->set_section(section_name, appconfig_new.get_section(section_name)); - - app_config->set_vendors(appconfig_new); - - app_config->set("notify_release", page_update->version_check ? "all" : "none"); - app_config->set("preset_update", page_update->preset_update ? "1" : "0"); - app_config->set("export_sources_full_pathnames", page_reload_from_disk->full_pathnames ? "1" : "0"); - -#ifdef _WIN32 - app_config->set("associate_3mf", page_files_association->associate_3mf() ? "1" : "0"); - app_config->set("associate_stl", page_files_association->associate_stl() ? "1" : "0"); -// app_config->set("associate_gcode", page_files_association->associate_gcode() ? "1" : "0"); - - if (wxGetApp().is_editor()) { - if (page_files_association->associate_3mf()) - wxGetApp().associate_3mf_files(); - if (page_files_association->associate_stl()) - wxGetApp().associate_stl_files(); - } -// else { -// if (page_files_association->associate_gcode()) -// wxGetApp().associate_gcode_files(); -// } - -#endif // _WIN32 - - page_mode->serialize_mode(app_config); - - if (check_unsaved_preset_changes) - preset_bundle->load_presets(*app_config, ForwardCompatibilitySubstitutionRule::EnableSilentDisableSystem, - {preferred_model, preferred_variant, first_added_filament, first_added_sla_material}); - - if (!only_sla_mode && page_custom->custom_wanted()) { - // if unsaved changes was not cheched till this moment - if (!check_unsaved_preset_changes && - !wxGetApp().check_and_keep_current_preset_changes(caption, _L("Custom printer was installed and it will be activated."), act_btns, &apply_keeped_changes)) - return false; - - page_firmware->apply_custom_config(*custom_config); - page_bed->apply_custom_config(*custom_config); - page_diams->apply_custom_config(*custom_config); - page_temps->apply_custom_config(*custom_config); - - const std::string profile_name = page_custom->profile_name(); - preset_bundle->load_config_from_wizard(profile_name, *custom_config); - } - - // Update the selections from the compatibilty. - preset_bundle->export_selections(*app_config); - - return true; -} -void ConfigWizard::priv::update_presets_in_config(const std::string& section, const std::string& alias_key, bool add) -{ - const PresetAliases& aliases = section == AppConfig::SECTION_FILAMENTS ? aliases_fff : aliases_sla; - - auto update = [this, add](const std::string& s, const std::string& key) { - assert(! s.empty()); - if (add) - appconfig_new.set(s, key, "1"); - else - appconfig_new.erase(s, key); - }; - - // add or delete presets had a same alias - auto it = aliases.find(alias_key); - if (it != aliases.end()) - for (const std::string& name : it->second) - update(section, name); -} - -bool ConfigWizard::priv::check_fff_selected() -{ - bool ret = page_fff->any_selected(); - for (const auto& printer: pages_3rdparty) - if (printer.second.first) // FFF page - ret |= printer.second.first->any_selected(); - return ret; -} - -bool ConfigWizard::priv::check_sla_selected() -{ - bool ret = page_msla->any_selected(); - for (const auto& printer: pages_3rdparty) - if (printer.second.second) // SLA page - ret |= printer.second.second->any_selected(); - return ret; -} - - -// Public - -ConfigWizard::ConfigWizard(wxWindow *parent) - : DPIDialog(parent, wxID_ANY, wxString(SLIC3R_APP_NAME) + " - " + _(name()), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) - , p(new priv(this)) -{ - this->SetFont(wxGetApp().normal_font()); - - p->load_vendors(); - p->custom_config.reset(DynamicPrintConfig::new_from_defaults_keys({ - "gcode_flavor", "bed_shape", "bed_custom_texture", "bed_custom_model", "nozzle_diameter", "filament_diameter", "temperature", "bed_temperature", - })); - - p->index = new ConfigWizardIndex(this); - - auto *vsizer = new wxBoxSizer(wxVERTICAL); - auto *topsizer = new wxBoxSizer(wxHORIZONTAL); - auto* hline = new StaticLine(this); - p->btnsizer = new wxBoxSizer(wxHORIZONTAL); - - // Initially we _do not_ SetScrollRate in order to figure out the overall width of the Wizard without scrolling. - // Later, we compare that to the size of the current screen and set minimum width based on that (see below). - p->hscroll = new wxScrolledWindow(this); - p->hscroll_sizer = new wxBoxSizer(wxHORIZONTAL); - p->hscroll->SetSizer(p->hscroll_sizer); - - topsizer->Add(p->index, 0, wxEXPAND); - topsizer->AddSpacer(INDEX_MARGIN); - topsizer->Add(p->hscroll, 1, wxEXPAND); - - p->btn_sel_all = new wxButton(this, wxID_ANY, _L("Select all standard printers")); - p->btnsizer->Add(p->btn_sel_all); - - p->btn_prev = new wxButton(this, wxID_ANY, _L("< &Back")); - p->btn_next = new wxButton(this, wxID_ANY, _L("&Next >")); - p->btn_finish = new wxButton(this, wxID_APPLY, _L("&Finish")); - p->btn_cancel = new wxButton(this, wxID_CANCEL, _L("Cancel")); // Note: The label needs to be present, otherwise we get accelerator bugs on Mac - p->btnsizer->AddStretchSpacer(); - p->btnsizer->Add(p->btn_prev, 0, wxLEFT, BTN_SPACING); - p->btnsizer->Add(p->btn_next, 0, wxLEFT, BTN_SPACING); - p->btnsizer->Add(p->btn_finish, 0, wxLEFT, BTN_SPACING); - p->btnsizer->Add(p->btn_cancel, 0, wxLEFT, BTN_SPACING); - - wxGetApp().UpdateDarkUI(p->btn_sel_all); - wxGetApp().UpdateDarkUI(p->btn_prev); - wxGetApp().UpdateDarkUI(p->btn_next); - wxGetApp().UpdateDarkUI(p->btn_finish); - wxGetApp().UpdateDarkUI(p->btn_cancel); - - const auto prusa_it = p->bundles.find("PrusaResearch"); - wxCHECK_RET(prusa_it != p->bundles.cend(), "Vendor PrusaResearch not found"); - const VendorProfile *vendor_prusa = prusa_it->second.vendor_profile; - - p->add_page(p->page_welcome = new PageWelcome(this)); - - - p->page_fff = new PagePrinters(this, _L("Prusa FFF Technology Printers"), "Prusa FFF", *vendor_prusa, 0, T_FFF); - p->only_sla_mode = !p->page_fff->has_printers; - if (!p->only_sla_mode) { - p->add_page(p->page_fff); - p->page_fff->is_primary_printer_page = true; - } - - - p->page_msla = new PagePrinters(this, _L("Prusa MSLA Technology Printers"), "Prusa MSLA", *vendor_prusa, 0, T_SLA); - p->add_page(p->page_msla); - if (p->only_sla_mode) { - p->page_msla->is_primary_printer_page = true; - } - - if (!p->only_sla_mode) { - // Pages for 3rd party vendors - p->create_3rdparty_pages(); // Needs to be done _before_ creating PageVendors - p->add_page(p->page_vendors = new PageVendors(this)); - p->add_page(p->page_custom = new PageCustom(this)); - p->custom_printer_selected = p->page_custom->custom_wanted(); - } - - p->any_sla_selected = p->check_sla_selected(); - p->any_fff_selected = ! p->only_sla_mode && p->check_fff_selected(); - - p->update_materials(T_ANY); - if (!p->only_sla_mode) - p->add_page(p->page_filaments = new PageMaterials(this, &p->filaments, - _L("Filament Profiles Selection"), _L("Filaments"), _L("Type:") )); - - p->add_page(p->page_sla_materials = new PageMaterials(this, &p->sla_materials, - _L("SLA Material Profiles Selection") + " ", _L("SLA Materials"), _L("Type:") )); - - - p->add_page(p->page_update = new PageUpdate(this)); - p->add_page(p->page_reload_from_disk = new PageReloadFromDisk(this)); -#ifdef _WIN32 - p->add_page(p->page_files_association = new PageFilesAssociation(this)); -#endif // _WIN32 - p->add_page(p->page_mode = new PageMode(this)); - p->add_page(p->page_firmware = new PageFirmware(this)); - p->add_page(p->page_bed = new PageBedShape(this)); - p->add_page(p->page_diams = new PageDiameters(this)); - p->add_page(p->page_temps = new PageTemperatures(this)); - - p->load_pages(); - p->index->go_to(size_t{0}); - - vsizer->Add(topsizer, 1, wxEXPAND | wxALL, DIALOG_MARGIN); - vsizer->Add(hline, 0, wxEXPAND | wxLEFT | wxRIGHT, VERTICAL_SPACING); - vsizer->Add(p->btnsizer, 0, wxEXPAND | wxALL, DIALOG_MARGIN); - - SetSizer(vsizer); - SetSizerAndFit(vsizer); - - // We can now enable scrolling on hscroll - p->hscroll->SetScrollRate(30, 30); - - on_window_geometry(this, [this]() { - p->init_dialog_size(); - }); - - p->btn_prev->Bind(wxEVT_BUTTON, [this](const wxCommandEvent &) { this->p->index->go_prev(); }); - - p->btn_next->Bind(wxEVT_BUTTON, [this](const wxCommandEvent &) - { - // check, that there is selected at least one filament/material - ConfigWizardPage* active_page = this->p->index->active_page(); - if (// Leaving the filaments or SLA materials page and - (active_page == p->page_filaments || active_page == p->page_sla_materials) && - // some Printer models had no filament or SLA material selected. - ! p->check_and_install_missing_materials(dynamic_cast(active_page)->materials->technology)) - // In that case don't leave the page and the function above queried the user whether to install default materials. - return; - this->p->index->go_next(); - }); - - p->btn_finish->Bind(wxEVT_BUTTON, [this](const wxCommandEvent &) - { - if (p->on_bnt_finish()) - this->EndModal(wxID_OK); - }); - - p->btn_sel_all->Bind(wxEVT_BUTTON, [this](const wxCommandEvent &) { - p->any_sla_selected = true; - p->load_pages(); - p->page_fff->select_all(true, false); - p->page_msla->select_all(true, false); - p->index->go_to(p->page_mode); - }); - - p->index->Bind(EVT_INDEX_PAGE, [this](const wxCommandEvent &) { - const bool is_last = p->index->active_is_last(); - p->btn_next->Show(! is_last); - if (is_last) - p->btn_finish->SetFocus(); - - Layout(); - }); - - if (wxLinux_gtk3) - this->Bind(wxEVT_SHOW, [this, vsizer](const wxShowEvent& e) { - ConfigWizardPage* active_page = p->index->active_page(); - if (!active_page) - return; - for (auto page : p->all_pages) - if (page != active_page) - page->Hide(); - // update best size for the dialog after hiding of the non-active pages - vsizer->SetSizeHints(this); - // set initial dialog size - p->init_dialog_size(); - }); -} - -ConfigWizard::~ConfigWizard() {} - -bool ConfigWizard::run(RunReason reason, StartPage start_page) -{ - BOOST_LOG_TRIVIAL(info) << boost::format("Running ConfigWizard, reason: %1%, start_page: %2%") % reason % start_page; - - GUI_App &app = wxGetApp(); - - p->set_run_reason(reason); - p->set_start_page(start_page); - - if (ShowModal() == wxID_OK) { - bool apply_keeped_changes = false; - if (! p->apply_config(app.app_config, app.preset_bundle, app.preset_updater, apply_keeped_changes)) - return false; - - if (apply_keeped_changes) - app.apply_keeped_preset_modifications(); - - app.app_config->set_legacy_datadir(false); - app.update_mode(); - app.obj_manipul()->update_ui_from_settings(); - BOOST_LOG_TRIVIAL(info) << "ConfigWizard applied"; - return true; - } else { - BOOST_LOG_TRIVIAL(info) << "ConfigWizard cancelled"; - return false; - } -} - -const wxString& ConfigWizard::name(const bool from_menu/* = false*/) -{ - // A different naming convention is used for the Wizard on Windows & GTK vs. OSX. - // Note: Don't call _() macro here. - // This function just return the current name according to the OS. - // Translation is implemented inside GUI_App::add_config_menu() -#if __APPLE__ - static const wxString config_wizard_name = L("Configuration Assistant"); - static const wxString config_wizard_name_menu = L("Configuration &Assistant"); -#else - static const wxString config_wizard_name = L("Configuration Wizard"); - static const wxString config_wizard_name_menu = L("Configuration &Wizard"); -#endif - return from_menu ? config_wizard_name_menu : config_wizard_name; -} - -void ConfigWizard::on_dpi_changed(const wxRect &suggested_rect) -{ - p->index->msw_rescale(); - - const int em = em_unit(); - - msw_buttons_rescale(this, em, { wxID_APPLY, - wxID_CANCEL, - p->btn_sel_all->GetId(), - p->btn_next->GetId(), - p->btn_prev->GetId() }); - - for (auto printer_picker: p->page_fff->printer_pickers) - msw_buttons_rescale(this, em, printer_picker->get_button_indexes()); - - p->init_dialog_size(); - - Refresh(); -} - -void ConfigWizard::on_sys_color_changed() -{ - wxGetApp().UpdateDlgDarkUI(this); - Refresh(); -} - -} -} +// FIXME: extract absolute units -> em + +#include "ConfigWizard_private.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef _MSW_DARK_MODE +#include +#endif // _MSW_DARK_MODE + +#include "libslic3r/Platform.hpp" +#include "libslic3r/Utils.hpp" +#include "libslic3r/Config.hpp" +#include "libslic3r/libslic3r.h" +#include "libslic3r/Model.hpp" +#include "libslic3r/Color.hpp" +#include "GUI.hpp" +#include "GUI_App.hpp" +#include "GUI_Utils.hpp" +#include "GUI_ObjectManipulation.hpp" +#include "Field.hpp" +#include "DesktopIntegrationDialog.hpp" +#include "slic3r/Config/Snapshot.hpp" +#include "slic3r/Utils/PresetUpdater.hpp" +#include "format.hpp" +#include "MsgDialog.hpp" +#include "UnsavedChangesDialog.hpp" + +#if defined(__linux__) && defined(__WXGTK3__) +#define wxLinux_gtk3 true +#else +#define wxLinux_gtk3 false +#endif //defined(__linux__) && defined(__WXGTK3__) + +namespace Slic3r { +namespace GUI { + + +using Config::Snapshot; +using Config::SnapshotDB; + + +// Configuration data structures extensions needed for the wizard + +bool Bundle::load(fs::path source_path, bool ais_in_resources, bool ais_prusa_bundle) +{ + this->preset_bundle = std::make_unique(); + this->is_in_resources = ais_in_resources; + this->is_prusa_bundle = ais_prusa_bundle; + + std::string path_string = source_path.string(); + // Throw when parsing invalid configuration. Only valid configuration is supposed to be provided over the air. + auto [config_substitutions, presets_loaded] = preset_bundle->load_configbundle( + path_string, PresetBundle::LoadConfigBundleAttribute::LoadSystem, ForwardCompatibilitySubstitutionRule::Disable); + UNUSED(config_substitutions); + // No substitutions shall be reported when loading a system config bundle, no substitutions are allowed. + assert(config_substitutions.empty()); + auto first_vendor = preset_bundle->vendors.begin(); + if (first_vendor == preset_bundle->vendors.end()) { + BOOST_LOG_TRIVIAL(error) << boost::format("Vendor bundle: `%1%`: No vendor information defined, cannot install.") % path_string; + return false; + } + if (presets_loaded == 0) { + BOOST_LOG_TRIVIAL(error) << boost::format("Vendor bundle: `%1%`: No profile loaded.") % path_string; + return false; + } + + BOOST_LOG_TRIVIAL(trace) << boost::format("Vendor bundle: `%1%`: %2% profiles loaded.") % path_string % presets_loaded; + this->vendor_profile = &first_vendor->second; + return true; +} + +Bundle::Bundle(Bundle &&other) + : preset_bundle(std::move(other.preset_bundle)) + , vendor_profile(other.vendor_profile) + , is_in_resources(other.is_in_resources) + , is_prusa_bundle(other.is_prusa_bundle) +{ + other.vendor_profile = nullptr; +} + +BundleMap BundleMap::load() +{ + BundleMap res; + + const auto vendor_dir = (boost::filesystem::path(Slic3r::data_dir()) / "vendor").make_preferred(); + const auto rsrc_vendor_dir = (boost::filesystem::path(resources_dir()) / "profiles").make_preferred(); + + auto prusa_bundle_path = (vendor_dir / PresetBundle::PRUSA_BUNDLE).replace_extension(".ini"); + auto prusa_bundle_rsrc = false; + if (! boost::filesystem::exists(prusa_bundle_path)) { + prusa_bundle_path = (rsrc_vendor_dir / PresetBundle::PRUSA_BUNDLE).replace_extension(".ini"); + prusa_bundle_rsrc = true; + } + { + Bundle prusa_bundle; + if (prusa_bundle.load(std::move(prusa_bundle_path), prusa_bundle_rsrc, true)) + res.emplace(PresetBundle::PRUSA_BUNDLE, std::move(prusa_bundle)); + } + + // Load the other bundles in the datadir/vendor directory + // and then additionally from resources/profiles. + bool is_in_resources = false; + for (auto dir : { &vendor_dir, &rsrc_vendor_dir }) { + for (const auto &dir_entry : boost::filesystem::directory_iterator(*dir)) { + if (Slic3r::is_ini_file(dir_entry)) { + std::string id = dir_entry.path().stem().string(); // stem() = filename() without the trailing ".ini" part + + // Don't load this bundle if we've already loaded it. + if (res.find(id) != res.end()) { continue; } + + Bundle bundle; + if (bundle.load(dir_entry.path(), is_in_resources)) + res.emplace(std::move(id), std::move(bundle)); + } + } + + is_in_resources = true; + } + + return res; +} + +Bundle& BundleMap::prusa_bundle() +{ + auto it = find(PresetBundle::PRUSA_BUNDLE); + if (it == end()) { + throw Slic3r::RuntimeError("ConfigWizard: Internal error in BundleMap: PRUSA_BUNDLE not loaded"); + } + + return it->second; +} + +const Bundle& BundleMap::prusa_bundle() const +{ + return const_cast(this)->prusa_bundle(); +} + + +// Printer model picker GUI control + +struct PrinterPickerEvent : public wxEvent +{ + std::string vendor_id; + std::string model_id; + std::string variant_name; + bool enable; + + PrinterPickerEvent(wxEventType eventType, int winid, std::string vendor_id, std::string model_id, std::string variant_name, bool enable) + : wxEvent(winid, eventType) + , vendor_id(std::move(vendor_id)) + , model_id(std::move(model_id)) + , variant_name(std::move(variant_name)) + , enable(enable) + {} + + virtual wxEvent *Clone() const + { + return new PrinterPickerEvent(*this); + } +}; + +wxDEFINE_EVENT(EVT_PRINTER_PICK, PrinterPickerEvent); + +const std::string PrinterPicker::PRINTER_PLACEHOLDER = "printer_placeholder.png"; + +PrinterPicker::PrinterPicker(wxWindow *parent, const VendorProfile &vendor, wxString title, size_t max_cols, const AppConfig &appconfig, const ModelFilter &filter) + : wxPanel(parent) + , vendor_id(vendor.id) + , width(0) +{ + wxGetApp().UpdateDarkUI(this); + const auto &models = vendor.models; + + auto *sizer = new wxBoxSizer(wxVERTICAL); + + const auto font_title = GetFont().MakeBold().Scaled(1.3f); + const auto font_name = GetFont().MakeBold(); + const auto font_alt_nozzle = GetFont().Scaled(0.9f); + + // wxGrid appends widgets by rows, but we need to construct them in columns. + // These vectors are used to hold the elements so that they can be appended in the right order. + std::vector titles; + std::vector bitmaps; + std::vector variants_panels; + + int max_row_width = 0; + int current_row_width = 0; + + bool is_variants = false; + + for (const auto &model : models) { + if (! filter(model)) { continue; } + + wxBitmap bitmap; + int bitmap_width = 0; + auto load_bitmap = [](const wxString& bitmap_file, wxBitmap& bitmap, int& bitmap_width)->bool { + if (wxFileExists(bitmap_file)) { + bitmap.LoadFile(bitmap_file, wxBITMAP_TYPE_PNG); + bitmap_width = bitmap.GetWidth(); + return true; + } + return false; + }; + if (!load_bitmap(GUI::from_u8(Slic3r::data_dir() + "/vendor/" + vendor.id + "/" + model.id + "_thumbnail.png"), bitmap, bitmap_width)) { + if (!load_bitmap(GUI::from_u8(Slic3r::resources_dir() + "/profiles/" + vendor.id + "/" + model.id + "_thumbnail.png"), bitmap, bitmap_width)) { + BOOST_LOG_TRIVIAL(warning) << boost::format("Can't find bitmap file `%1%` for vendor `%2%`, printer `%3%`, using placeholder icon instead") + % (Slic3r::resources_dir() + "/profiles/" + vendor.id + "/" + model.id + "_thumbnail.png") + % vendor.id + % model.id; + load_bitmap(Slic3r::var(PRINTER_PLACEHOLDER), bitmap, bitmap_width); + } + } + auto *title = new wxStaticText(this, wxID_ANY, from_u8(model.name), wxDefaultPosition, wxDefaultSize, wxALIGN_LEFT); + title->SetFont(font_name); + const int wrap_width = std::max((int)MODEL_MIN_WRAP, bitmap_width); + title->Wrap(wrap_width); + + current_row_width += wrap_width; + if (titles.size() % max_cols == max_cols - 1) { + max_row_width = std::max(max_row_width, current_row_width); + current_row_width = 0; + } + + titles.push_back(title); + + auto *bitmap_widget = new wxStaticBitmap(this, wxID_ANY, bitmap); + bitmaps.push_back(bitmap_widget); + + auto *variants_panel = new wxPanel(this); + wxGetApp().UpdateDarkUI(variants_panel); + auto *variants_sizer = new wxBoxSizer(wxVERTICAL); + variants_panel->SetSizer(variants_sizer); + const auto model_id = model.id; + + for (size_t i = 0; i < model.variants.size(); i++) { + const auto &variant = model.variants[i]; + + const auto label = model.technology == ptFFF + ? from_u8((boost::format("%1% %2% %3%") % variant.name % _utf8(L("mm")) % _utf8(L("nozzle"))).str()) + : from_u8(model.name); + + if (i == 1) { + auto *alt_label = new wxStaticText(variants_panel, wxID_ANY, _L("Alternate nozzles:")); + alt_label->SetFont(font_alt_nozzle); + variants_sizer->Add(alt_label, 0, wxBOTTOM, 3); + is_variants = true; + } + + auto *cbox = new Checkbox(variants_panel, label, model_id, variant.name); + i == 0 ? cboxes.push_back(cbox) : cboxes_alt.push_back(cbox); + + const bool enabled = appconfig.get_variant(vendor.id, model_id, variant.name); + cbox->SetValue(enabled); + + variants_sizer->Add(cbox, 0, wxBOTTOM, 3); + + cbox->Bind(wxEVT_CHECKBOX, [this, cbox](wxCommandEvent &event) { + on_checkbox(cbox, event.IsChecked()); + }); + } + + variants_panels.push_back(variants_panel); + } + + width = std::max(max_row_width, current_row_width); + + const size_t cols = std::min(max_cols, titles.size()); + + auto *printer_grid = new wxFlexGridSizer(cols, 0, 20); + printer_grid->SetFlexibleDirection(wxVERTICAL | wxHORIZONTAL); + + if (titles.size() > 0) { + const size_t odd_items = titles.size() % cols; + + for (size_t i = 0; i < titles.size() - odd_items; i += cols) { + for (size_t j = i; j < i + cols; j++) { printer_grid->Add(bitmaps[j], 0, wxBOTTOM, 20); } + for (size_t j = i; j < i + cols; j++) { printer_grid->Add(titles[j], 0, wxBOTTOM, 3); } + for (size_t j = i; j < i + cols; j++) { printer_grid->Add(variants_panels[j]); } + + // Add separator space to multiliners + if (titles.size() > cols) { + for (size_t j = i; j < i + cols; j++) { printer_grid->Add(1, 30); } + } + } + if (odd_items > 0) { + const size_t rem = titles.size() - odd_items; + + for (size_t i = rem; i < titles.size(); i++) { printer_grid->Add(bitmaps[i], 0, wxBOTTOM, 20); } + for (size_t i = 0; i < cols - odd_items; i++) { printer_grid->AddSpacer(1); } + for (size_t i = rem; i < titles.size(); i++) { printer_grid->Add(titles[i], 0, wxBOTTOM, 3); } + for (size_t i = 0; i < cols - odd_items; i++) { printer_grid->AddSpacer(1); } + for (size_t i = rem; i < titles.size(); i++) { printer_grid->Add(variants_panels[i]); } + } + } + + auto *title_sizer = new wxBoxSizer(wxHORIZONTAL); + if (! title.IsEmpty()) { + auto *title_widget = new wxStaticText(this, wxID_ANY, title); + title_widget->SetFont(font_title); + title_sizer->Add(title_widget); + } + title_sizer->AddStretchSpacer(); + + if (titles.size() > 1 || is_variants) { + // It only makes sense to add the All / None buttons if there's multiple printers + // All Standard button is added when there are more variants for at least one printer + auto *sel_all_std = new wxButton(this, wxID_ANY, titles.size() > 1 ? _L("All standard") : _L("Standard")); + auto *sel_all = new wxButton(this, wxID_ANY, _L("All")); + auto *sel_none = new wxButton(this, wxID_ANY, _L("None")); + if (is_variants) + sel_all_std->Bind(wxEVT_BUTTON, [this](const wxCommandEvent& event) { this->select_all(true, false); }); + sel_all->Bind(wxEVT_BUTTON, [this](const wxCommandEvent &event) { this->select_all(true, true); }); + sel_none->Bind(wxEVT_BUTTON, [this](const wxCommandEvent &event) { this->select_all(false); }); + if (is_variants) + title_sizer->Add(sel_all_std, 0, wxRIGHT, BTN_SPACING); + title_sizer->Add(sel_all, 0, wxRIGHT, BTN_SPACING); + title_sizer->Add(sel_none); + + wxGetApp().UpdateDarkUI(sel_all_std); + wxGetApp().UpdateDarkUI(sel_all); + wxGetApp().UpdateDarkUI(sel_none); + + // fill button indexes used later for buttons rescaling + if (is_variants) + m_button_indexes = { sel_all_std->GetId(), sel_all->GetId(), sel_none->GetId() }; + else { + sel_all_std->Destroy(); + m_button_indexes = { sel_all->GetId(), sel_none->GetId() }; + } + } + + sizer->Add(title_sizer, 0, wxEXPAND | wxBOTTOM, BTN_SPACING); + sizer->Add(printer_grid); + + SetSizer(sizer); +} + +PrinterPicker::PrinterPicker(wxWindow *parent, const VendorProfile &vendor, wxString title, size_t max_cols, const AppConfig &appconfig) + : PrinterPicker(parent, vendor, std::move(title), max_cols, appconfig, [](const VendorProfile::PrinterModel&) { return true; }) +{} + +void PrinterPicker::select_all(bool select, bool alternates) +{ + for (const auto &cb : cboxes) { + if (cb->GetValue() != select) { + cb->SetValue(select); + on_checkbox(cb, select); + } + } + + if (! select) { alternates = false; } + + for (const auto &cb : cboxes_alt) { + if (cb->GetValue() != alternates) { + cb->SetValue(alternates); + on_checkbox(cb, alternates); + } + } +} + +void PrinterPicker::select_one(size_t i, bool select) +{ + if (i < cboxes.size() && cboxes[i]->GetValue() != select) { + cboxes[i]->SetValue(select); + on_checkbox(cboxes[i], select); + } +} + +bool PrinterPicker::any_selected() const +{ + for (const auto &cb : cboxes) { + if (cb->GetValue()) { return true; } + } + + for (const auto &cb : cboxes_alt) { + if (cb->GetValue()) { return true; } + } + + return false; +} + +std::set PrinterPicker::get_selected_models() const +{ + std::set ret_set; + + for (const auto& cb : cboxes) + if (cb->GetValue()) + ret_set.emplace(cb->model); + + for (const auto& cb : cboxes_alt) + if (cb->GetValue()) + ret_set.emplace(cb->model); + + return ret_set; +} + +void PrinterPicker::on_checkbox(const Checkbox *cbox, bool checked) +{ + PrinterPickerEvent evt(EVT_PRINTER_PICK, GetId(), vendor_id, cbox->model, cbox->variant, checked); + AddPendingEvent(evt); +} + + +// Wizard page base + +ConfigWizardPage::ConfigWizardPage(ConfigWizard *parent, wxString title, wxString shortname, unsigned indent) + : wxPanel(parent->p->hscroll) + , parent(parent) + , shortname(std::move(shortname)) + , indent(indent) +{ + wxGetApp().UpdateDarkUI(this); + + auto *sizer = new wxBoxSizer(wxVERTICAL); + + auto *text = new wxStaticText(this, wxID_ANY, std::move(title), wxDefaultPosition, wxDefaultSize, wxALIGN_LEFT); + const auto font = GetFont().MakeBold().Scaled(1.5); + text->SetFont(font); + sizer->Add(text, 0, wxALIGN_LEFT, 0); + sizer->AddSpacer(10); + + content = new wxBoxSizer(wxVERTICAL); + sizer->Add(content, 1, wxEXPAND); + + SetSizer(sizer); + + // There is strange layout on Linux with GTK3, + // see https://github.com/prusa3d/PrusaSlicer/issues/5103 and https://github.com/prusa3d/PrusaSlicer/issues/4861 + // So, non-active pages will be hidden later, on wxEVT_SHOW, after completed Layout() for all pages + if (!wxLinux_gtk3) + this->Hide(); + + Bind(wxEVT_SIZE, [this](wxSizeEvent &event) { + this->Layout(); + event.Skip(); + }); +} + +ConfigWizardPage::~ConfigWizardPage() {} + +wxStaticText* ConfigWizardPage::append_text(wxString text) +{ + auto *widget = new wxStaticText(this, wxID_ANY, text, wxDefaultPosition, wxDefaultSize, wxALIGN_LEFT); + widget->Wrap(WRAP_WIDTH); + widget->SetMinSize(wxSize(WRAP_WIDTH, -1)); + append(widget); + return widget; +} + +void ConfigWizardPage::append_spacer(int space) +{ + // FIXME: scaling + content->AddSpacer(space); +} + +// Wizard pages + +PageWelcome::PageWelcome(ConfigWizard *parent) + : ConfigWizardPage(parent, from_u8((boost::format( +#ifdef __APPLE__ + _utf8(L("Welcome to the %s Configuration Assistant")) +#else + _utf8(L("Welcome to the %s Configuration Wizard")) +#endif + ) % SLIC3R_APP_NAME).str()), _L("Welcome")) + , welcome_text(append_text(from_u8((boost::format( + _utf8(L("Hello, welcome to %s! This %s helps you with the initial configuration; just a few settings and you will be ready to print."))) + % SLIC3R_APP_NAME + % _utf8(ConfigWizard::name())).str()) + )) + , cbox_reset(append( + new wxCheckBox(this, wxID_ANY, _L("Remove user profiles (a snapshot will be taken beforehand)")) + )) + , cbox_integrate(append( + new wxCheckBox(this, wxID_ANY, _L("Perform desktop integration (Sets this binary to be searchable by the system).")) + )) +{ + welcome_text->Hide(); + cbox_reset->Hide(); + cbox_integrate->Hide(); +} + +void PageWelcome::set_run_reason(ConfigWizard::RunReason run_reason) +{ + const bool data_empty = run_reason == ConfigWizard::RR_DATA_EMPTY; + welcome_text->Show(data_empty); + cbox_reset->Show(!data_empty); +#if defined(__linux__) && defined(SLIC3R_DESKTOP_INTEGRATION) + if (!DesktopIntegrationDialog::is_integrated()) + cbox_integrate->Show(true); + else + cbox_integrate->Hide(); +#else + cbox_integrate->Hide(); +#endif +} + + +PagePrinters::PagePrinters(ConfigWizard *parent, + wxString title, + wxString shortname, + const VendorProfile &vendor, + unsigned indent, + Technology technology) + : ConfigWizardPage(parent, std::move(title), std::move(shortname), indent) + , technology(technology) + , install(false) // only used for 3rd party vendors +{ + enum { + COL_SIZE = 200, + }; + + AppConfig *appconfig = &this->wizard_p()->appconfig_new; + + const auto families = vendor.families(); + for (const auto &family : families) { + const auto filter = [&](const VendorProfile::PrinterModel &model) { + return ((model.technology == ptFFF && technology & T_FFF) + || (model.technology == ptSLA && technology & T_SLA)) + && model.family == family; + }; + + if (std::find_if(vendor.models.begin(), vendor.models.end(), filter) == vendor.models.end()) { + continue; + } + + const auto picker_title = family.empty() ? wxString() : from_u8((boost::format(_utf8(L("%s Family"))) % family).str()); + auto *picker = new PrinterPicker(this, vendor, picker_title, MAX_COLS, *appconfig, filter); + + picker->Bind(EVT_PRINTER_PICK, [this, appconfig](const PrinterPickerEvent &evt) { + appconfig->set_variant(evt.vendor_id, evt.model_id, evt.variant_name, evt.enable); + wizard_p()->on_printer_pick(this, evt); + }); + + append(new StaticLine(this)); + + append(picker); + printer_pickers.push_back(picker); + has_printers = true; + } + +} + +void PagePrinters::select_all(bool select, bool alternates) +{ + for (auto picker : printer_pickers) { + picker->select_all(select, alternates); + } +} + +int PagePrinters::get_width() const +{ + return std::accumulate(printer_pickers.begin(), printer_pickers.end(), 0, + [](int acc, const PrinterPicker *picker) { return std::max(acc, picker->get_width()); }); +} + +bool PagePrinters::any_selected() const +{ + for (const auto *picker : printer_pickers) { + if (picker->any_selected()) { return true; } + } + + return false; +} + +std::set PagePrinters::get_selected_models() +{ + std::set ret_set; + + for (const auto *picker : printer_pickers) + { + std::set tmp_models = picker->get_selected_models(); + ret_set.insert(tmp_models.begin(), tmp_models.end()); + } + + return ret_set; +} + +void PagePrinters::set_run_reason(ConfigWizard::RunReason run_reason) +{ + if (is_primary_printer_page + && (run_reason == ConfigWizard::RR_DATA_EMPTY || run_reason == ConfigWizard::RR_DATA_LEGACY) + && printer_pickers.size() > 0 + && printer_pickers[0]->vendor_id == PresetBundle::PRUSA_BUNDLE) { + printer_pickers[0]->select_one(0, true); + } +} + + +const std::string PageMaterials::EMPTY; + +PageMaterials::PageMaterials(ConfigWizard *parent, Materials *materials, wxString title, wxString shortname, wxString list1name) + : ConfigWizardPage(parent, std::move(title), std::move(shortname)) + , materials(materials) + , list_printer(new StringList(this, wxLB_MULTIPLE)) + , list_type(new StringList(this)) + , list_vendor(new StringList(this)) + , list_profile(new PresetList(this)) +{ + append_spacer(VERTICAL_SPACING); + + const int em = parent->em_unit(); + const int list_h = 30*em; + + + list_printer->SetMinSize(wxSize(23*em, list_h)); + list_type->SetMinSize(wxSize(13*em, list_h)); + list_vendor->SetMinSize(wxSize(13*em, list_h)); + list_profile->SetMinSize(wxSize(23*em, list_h)); + + + + grid = new wxFlexGridSizer(4, em/2, em); + grid->AddGrowableCol(3, 1); + grid->AddGrowableRow(1, 1); + + grid->Add(new wxStaticText(this, wxID_ANY, _L("Printer:"))); + grid->Add(new wxStaticText(this, wxID_ANY, list1name)); + grid->Add(new wxStaticText(this, wxID_ANY, _L("Vendor:"))); + grid->Add(new wxStaticText(this, wxID_ANY, _L("Profile:"))); + + grid->Add(list_printer, 0, wxEXPAND); + grid->Add(list_type, 0, wxEXPAND); + grid->Add(list_vendor, 0, wxEXPAND); + grid->Add(list_profile, 1, wxEXPAND); + + auto *btn_sizer = new wxBoxSizer(wxHORIZONTAL); + auto *sel_all = new wxButton(this, wxID_ANY, _L("All")); + auto *sel_none = new wxButton(this, wxID_ANY, _L("None")); + btn_sizer->Add(sel_all, 0, wxRIGHT, em / 2); + btn_sizer->Add(sel_none); + + wxGetApp().UpdateDarkUI(list_printer); + wxGetApp().UpdateDarkUI(list_type); + wxGetApp().UpdateDarkUI(list_vendor); + wxGetApp().UpdateDarkUI(sel_all); + wxGetApp().UpdateDarkUI(sel_none); + + grid->Add(new wxBoxSizer(wxHORIZONTAL)); + grid->Add(new wxBoxSizer(wxHORIZONTAL)); + grid->Add(new wxBoxSizer(wxHORIZONTAL)); + grid->Add(btn_sizer, 0, wxALIGN_RIGHT); + + append(grid, 1, wxEXPAND); + + append_spacer(VERTICAL_SPACING); + + html_window = new wxHtmlWindow(this, wxID_ANY, wxDefaultPosition, + wxSize(60 * em, 20 * em), wxHW_SCROLLBAR_AUTO); + append(html_window, 0, wxEXPAND); + + list_printer->Bind(wxEVT_LISTBOX, [this](wxCommandEvent& evt) { + update_lists(list_type->GetSelection(), list_vendor->GetSelection(), evt.GetInt()); + }); + list_type->Bind(wxEVT_LISTBOX, [this](wxCommandEvent &) { + update_lists(list_type->GetSelection(), list_vendor->GetSelection()); + }); + list_vendor->Bind(wxEVT_LISTBOX, [this](wxCommandEvent &) { + update_lists(list_type->GetSelection(), list_vendor->GetSelection()); + }); + + list_profile->Bind(wxEVT_CHECKLISTBOX, [this](wxCommandEvent &evt) { select_material(evt.GetInt()); }); + list_profile->Bind(wxEVT_LISTBOX, [this](wxCommandEvent& evt) { on_material_highlighted(evt.GetInt()); }); + + sel_all->Bind(wxEVT_BUTTON, [this](wxCommandEvent &) { select_all(true); }); + sel_none->Bind(wxEVT_BUTTON, [this](wxCommandEvent &) { select_all(false); }); + /* + Bind(wxEVT_PAINT, [this](wxPaintEvent& evt) {on_paint();}); + + list_profile->Bind(wxEVT_MOTION, [this](wxMouseEvent& evt) { on_mouse_move_on_profiles(evt); }); + list_profile->Bind(wxEVT_ENTER_WINDOW, [this](wxMouseEvent& evt) { on_mouse_enter_profiles(evt); }); + list_profile->Bind(wxEVT_LEAVE_WINDOW, [this](wxMouseEvent& evt) { on_mouse_leave_profiles(evt); }); + */ + reload_presets(); + set_compatible_printers_html_window(std::vector(), false); +} +void PageMaterials::on_paint() +{ +} +void PageMaterials::on_mouse_move_on_profiles(wxMouseEvent& evt) +{ + const wxClientDC dc(list_profile); + const wxPoint pos = evt.GetLogicalPosition(dc); + int item = list_profile->HitTest(pos); + on_material_hovered(item); +} +void PageMaterials::on_mouse_enter_profiles(wxMouseEvent& evt) +{} +void PageMaterials::on_mouse_leave_profiles(wxMouseEvent& evt) +{ + on_material_hovered(-1); +} +void PageMaterials::reload_presets() +{ + clear(); + + list_printer->append(_L("(All)"), &EMPTY); + //list_printer->SetLabelMarkup("bald"); + for (const Preset* printer : materials->printers) { + list_printer->append(printer->name, &printer->name); + } + sort_list_data(list_printer, true, false); + if (list_printer->GetCount() > 0) { + list_printer->SetSelection(0); + sel_printers_prev.Clear(); + sel_type_prev = wxNOT_FOUND; + sel_vendor_prev = wxNOT_FOUND; + update_lists(0, 0, 0); + } + + presets_loaded = true; +} + +void PageMaterials::set_compatible_printers_html_window(const std::vector& printer_names, bool all_printers) +{ + const auto bgr_clr = +#if defined(__APPLE__) + html_window->GetParent()->GetBackgroundColour(); +#else +#if defined(_WIN32) + wxGetApp().get_window_default_clr(); +#else + wxSystemSettings::GetColour(wxSYS_COLOUR_MENU); +#endif +#endif + const auto text_clr = wxGetApp().get_label_clr_default(); + const auto bgr_clr_str = encode_color(ColorRGB(bgr_clr.Red(), bgr_clr.Green(), bgr_clr.Blue())); + const auto text_clr_str = encode_color(ColorRGB(text_clr.Red(), text_clr.Green(), text_clr.Blue())); + wxString first_line = format_wxstr(_L("%1% marked with * are not compatible with some installed printers."), materials->technology == T_FFF ? _L("Filaments") : _L("SLA materials")); + wxString text; + if (all_printers) { + wxString second_line = format_wxstr(_L("All installed printers are compatible with the selected %1%."), materials->technology == T_FFF ? _L("filament") : _L("SLA material")); + text = wxString::Format( + "" + "" + "" + "" + "" + "%s

%s" + "
" + "
" + "" + "" + , bgr_clr_str + , text_clr_str + , first_line + , second_line + ); + } else { + wxString second_line; + if (!printer_names.empty()) + second_line = (materials->technology == T_FFF ? + _L("Only the following installed printers are compatible with the selected filaments") : + _L("Only the following installed printers are compatible with the selected SLA materials")) + ":"; + text = wxString::Format( + "" + "" + "" + "" + "" + "%s

%s" + "" + "" + , bgr_clr_str + , text_clr_str + , first_line + , second_line); + for (size_t i = 0; i < printer_names.size(); ++i) + { + text += wxString::Format("", boost::nowide::widen(printer_names[i])); + if (i % 3 == 2) { + text += wxString::Format( + "" + ""); + } + } + text += wxString::Format( + "" + "
%s
" + "
" + "
" + "" + "" + ); + } + + wxFont font = get_default_font_for_dpi(this, get_dpi_for_window(this)); + const int fs = font.GetPointSize(); + int size[] = { fs,fs,fs,fs,fs,fs,fs }; + html_window->SetFonts(font.GetFaceName(), font.GetFaceName(), size); + html_window->SetPage(text); +} + +void PageMaterials::clear_compatible_printers_label() +{ + set_compatible_printers_html_window(std::vector(), false); +} + +void PageMaterials::on_material_hovered(int sel_material) +{ + +} + +void PageMaterials::on_material_highlighted(int sel_material) +{ + if (sel_material == last_hovered_item) + return; + if (sel_material == -1) { + clear_compatible_printers_label(); + return; + } + last_hovered_item = sel_material; + std::vector tabs; + tabs.push_back(std::string()); + tabs.push_back(std::string()); + tabs.push_back(std::string()); + //selected material string + std::string material_name = list_profile->get_data(sel_material); + // get material preset + const std::vector matching_materials = materials->get_presets_by_alias(material_name); + if (matching_materials.empty()) + { + clear_compatible_printers_label(); + return; + } + //find matching printers + std::vector names; + for (const Preset* printer : materials->printers) { + for (const Preset* material : matching_materials) { + if (is_compatible_with_printer(PresetWithVendorProfile(*material, material->vendor), PresetWithVendorProfile(*printer, printer->vendor))) { + names.push_back(printer->name); + break; + } + } + } + set_compatible_printers_html_window(names, names.size() == materials->printers.size()); +} + +void PageMaterials::update_lists(int sel_type, int sel_vendor, int last_selected_printer/* = -1*/) +{ + wxWindowUpdateLocker freeze_guard(this); + (void)freeze_guard; + + wxArrayInt sel_printers; + int sel_printers_count = list_printer->GetSelections(sel_printers); + + // Does our wxWidgets version support operator== for wxArrayInt ? + // https://github.com/prusa3d/PrusaSlicer/issues/5152#issuecomment-787208614 +#if wxCHECK_VERSION(3, 1, 1) + if (sel_printers != sel_printers_prev) { +#else + auto are_equal = [](const wxArrayInt& arr_first, const wxArrayInt& arr_second) { + if (arr_first.GetCount() != arr_second.GetCount()) + return false; + for (size_t i = 0; i < arr_first.GetCount(); i++) + if (arr_first[i] != arr_second[i]) + return false; + return true; + }; + if (!are_equal(sel_printers, sel_printers_prev)) { +#endif + + // Refresh type list + list_type->Clear(); + list_type->append(_L("(All)"), &EMPTY); + if (sel_printers_count > 0) { + // If all is selected with other printers + // unselect "all" or all printers depending on last value + if (sel_printers[0] == 0 && sel_printers_count > 1) { + if (last_selected_printer == 0) { + list_printer->SetSelection(wxNOT_FOUND); + list_printer->SetSelection(0); + } else { + list_printer->SetSelection(0, false); + sel_printers_count = list_printer->GetSelections(sel_printers); + } + } + if (sel_printers[0] != 0) { + for (int i = 0; i < sel_printers_count; i++) { + const std::string& printer_name = list_printer->get_data(sel_printers[i]); + const Preset* printer = nullptr; + for (const Preset* it : materials->printers) { + if (it->name == printer_name) { + printer = it; + break; + } + } + materials->filter_presets(printer, EMPTY, EMPTY, [this](const Preset* p) { + const std::string& type = this->materials->get_type(p); + if (list_type->find(type) == wxNOT_FOUND) { + list_type->append(type, &type); + } + }); + } + } else { + //clear selection except "ALL" + list_printer->SetSelection(wxNOT_FOUND); + list_printer->SetSelection(0); + sel_printers_count = list_printer->GetSelections(sel_printers); + + materials->filter_presets(nullptr, EMPTY, EMPTY, [this](const Preset* p) { + const std::string& type = this->materials->get_type(p); + if (list_type->find(type) == wxNOT_FOUND) { + list_type->append(type, &type); + } + }); + } + sort_list_data(list_type, true, true); + } + + sel_printers_prev = sel_printers; + sel_type = 0; + sel_type_prev = wxNOT_FOUND; + list_type->SetSelection(sel_type); + list_profile->Clear(); + } + + if (sel_type != sel_type_prev) { + // Refresh vendor list + + // XXX: The vendor list is created with quadratic complexity here, + // but the number of vendors is going to be very small this shouldn't be a problem. + + list_vendor->Clear(); + list_vendor->append(_L("(All)"), &EMPTY); + if (sel_printers_count != 0 && sel_type != wxNOT_FOUND) { + const std::string& type = list_type->get_data(sel_type); + // find printer preset + for (int i = 0; i < sel_printers_count; i++) { + const std::string& printer_name = list_printer->get_data(sel_printers[i]); + const Preset* printer = nullptr; + for (const Preset* it : materials->printers) { + if (it->name == printer_name) { + printer = it; + break; + } + } + materials->filter_presets(printer, type, EMPTY, [this](const Preset* p) { + const std::string& vendor = this->materials->get_vendor(p); + if (list_vendor->find(vendor) == wxNOT_FOUND) { + list_vendor->append(vendor, &vendor); + } + }); + } + sort_list_data(list_vendor, true, false); + } + + sel_type_prev = sel_type; + sel_vendor = 0; + sel_vendor_prev = wxNOT_FOUND; + list_vendor->SetSelection(sel_vendor); + list_profile->Clear(); + } + + if (sel_vendor != sel_vendor_prev) { + // Refresh material list + + list_profile->Clear(); + clear_compatible_printers_label(); + if (sel_printers_count != 0 && sel_type != wxNOT_FOUND && sel_vendor != wxNOT_FOUND) { + const std::string& type = list_type->get_data(sel_type); + const std::string& vendor = list_vendor->get_data(sel_vendor); + // finst printer preset + std::vector to_list; + for (int i = 0; i < sel_printers_count; i++) { + const std::string& printer_name = list_printer->get_data(sel_printers[i]); + const Preset* printer = nullptr; + for (const Preset* it : materials->printers) { + if (it->name == printer_name) { + printer = it; + break; + } + } + + materials->filter_presets(printer, type, vendor, [this, &to_list](const Preset* p) { + const std::string& section = materials->appconfig_section(); + bool checked = wizard_p()->appconfig_new.has(section, p->name); + bool was_checked = false; + + int cur_i = list_profile->find(p->alias); + if (cur_i == wxNOT_FOUND) { + cur_i = list_profile->append(p->alias + (materials->get_omnipresent(p) ? "" : " *"), &p->alias); + to_list.emplace_back(p->alias, materials->get_omnipresent(p), checked); + } + else { + was_checked = list_profile->IsChecked(cur_i); + to_list[cur_i].checked = checked || was_checked; + } + list_profile->Check(cur_i, checked || was_checked); + + /* Update preset selection in config. + * If one preset from aliases bundle is selected, + * than mark all presets with this aliases as selected + * */ + if (checked && !was_checked) + wizard_p()->update_presets_in_config(section, p->alias, true); + else if (!checked && was_checked) + wizard_p()->appconfig_new.set(section, p->name, "1"); + }); + } + sort_list_data(list_profile, to_list); + } + + sel_vendor_prev = sel_vendor; + } + wxGetApp().UpdateDarkUI(list_profile); +} + +void PageMaterials::sort_list_data(StringList* list, bool add_All_item, bool material_type_ordering) +{ +// get data from list +// sort data +// first should be +// then prusa profiles +// then the rest +// in alphabetical order + + std::vector> prusa_profiles; + std::vector> other_profiles; + for (int i = 0 ; i < list->size(); ++i) { + const std::string& data = list->get_data(i); + if (data == EMPTY) // do not sort item + continue; + if (!material_type_ordering && data.find("Prusa") != std::string::npos) + prusa_profiles.push_back(data); + else + other_profiles.push_back(data); + } + if(material_type_ordering) { + + const ConfigOptionDef* def = print_config_def.get("filament_type"); + std::vectorenum_values = def->enum_values; + size_t end_of_sorted = 0; + for (size_t vals = 0; vals < enum_values.size(); vals++) { + for (size_t profs = end_of_sorted; profs < other_profiles.size(); profs++) + { + // find instead compare because PET vs PETG + if (other_profiles[profs].get().find(enum_values[vals]) != std::string::npos) { + //swap + if(profs != end_of_sorted) { + std::reference_wrapper aux = other_profiles[end_of_sorted]; + other_profiles[end_of_sorted] = other_profiles[profs]; + other_profiles[profs] = aux; + } + end_of_sorted++; + break; + } + } + } + } else { + std::sort(prusa_profiles.begin(), prusa_profiles.end(), [](std::reference_wrapper a, std::reference_wrapper b) { + return a.get() < b.get(); + }); + std::sort(other_profiles.begin(), other_profiles.end(), [](std::reference_wrapper a, std::reference_wrapper b) { + return a.get() < b.get(); + }); + } + + list->Clear(); + if (add_All_item) + list->append(_L("(All)"), &EMPTY); + for (const auto& item : prusa_profiles) + list->append(item, &const_cast(item.get())); + for (const auto& item : other_profiles) + list->append(item, &const_cast(item.get())); +} + +void PageMaterials::sort_list_data(PresetList* list, const std::vector& data) +{ + // sort data + // then prusa profiles + // then the rest + // in alphabetical order + std::vector prusa_profiles; + std::vector other_profiles; + //for (int i = 0; i < data.size(); ++i) { + for (const auto& item : data) { + const std::string& name = item.name; + if (name.find("Prusa") != std::string::npos) + prusa_profiles.emplace_back(item); + else + other_profiles.emplace_back(item); + } + std::sort(prusa_profiles.begin(), prusa_profiles.end(), [](ProfilePrintData a, ProfilePrintData b) { + return a.name.get() < b.name.get(); + }); + std::sort(other_profiles.begin(), other_profiles.end(), [](ProfilePrintData a, ProfilePrintData b) { + return a.name.get() < b.name.get(); + }); + list->Clear(); + for (size_t i = 0; i < prusa_profiles.size(); ++i) { + list->append(std::string(prusa_profiles[i].name) + (prusa_profiles[i].omnipresent ? "" : " *"), &const_cast(prusa_profiles[i].name.get())); + list->Check(i, prusa_profiles[i].checked); + } + for (size_t i = 0; i < other_profiles.size(); ++i) { + list->append(std::string(other_profiles[i].name) + (other_profiles[i].omnipresent ? "" : " *"), &const_cast(other_profiles[i].name.get())); + list->Check(i + prusa_profiles.size(), other_profiles[i].checked); + } +} + +void PageMaterials::select_material(int i) +{ + const bool checked = list_profile->IsChecked(i); + + const std::string& alias_key = list_profile->get_data(i); + wizard_p()->update_presets_in_config(materials->appconfig_section(), alias_key, checked); +} + +void PageMaterials::select_all(bool select) +{ + wxWindowUpdateLocker freeze_guard(this); + (void)freeze_guard; + + for (unsigned i = 0; i < list_profile->GetCount(); i++) { + const bool current = list_profile->IsChecked(i); + if (current != select) { + list_profile->Check(i, select); + select_material(i); + } + } +} + +void PageMaterials::clear() +{ + list_printer->Clear(); + list_type->Clear(); + list_vendor->Clear(); + list_profile->Clear(); + sel_printers_prev.Clear(); + sel_type_prev = wxNOT_FOUND; + sel_vendor_prev = wxNOT_FOUND; + presets_loaded = false; +} + +void PageMaterials::on_activate() +{ + if (! presets_loaded) { + wizard_p()->update_materials(materials->technology); + reload_presets(); + } + first_paint = true; +} + + +const char *PageCustom::default_profile_name = "My Settings"; + +PageCustom::PageCustom(ConfigWizard *parent) + : ConfigWizardPage(parent, _L("Custom Printer Setup"), _L("Custom Printer")) +{ + cb_custom = new wxCheckBox(this, wxID_ANY, _L("Define a custom printer profile")); + tc_profile_name = new wxTextCtrl(this, wxID_ANY, default_profile_name); + auto *label = new wxStaticText(this, wxID_ANY, _L("Custom profile name:")); + + wxGetApp().UpdateDarkUI(tc_profile_name); + + tc_profile_name->Enable(false); + tc_profile_name->Bind(wxEVT_KILL_FOCUS, [this](wxFocusEvent &evt) { + if (tc_profile_name->GetValue().IsEmpty()) { + if (profile_name_prev.IsEmpty()) { tc_profile_name->SetValue(default_profile_name); } + else { tc_profile_name->SetValue(profile_name_prev); } + } else { + profile_name_prev = tc_profile_name->GetValue(); + } + evt.Skip(); + }); + + cb_custom->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent &event) { + tc_profile_name->Enable(custom_wanted()); + wizard_p()->on_custom_setup(custom_wanted()); + + }); + + append(cb_custom); + append(label); + append(tc_profile_name); +} + +PageUpdate::PageUpdate(ConfigWizard *parent) + : ConfigWizardPage(parent, _L("Automatic updates"), _L("Updates")) + , version_check(true) + , preset_update(true) +{ + const AppConfig *app_config = wxGetApp().app_config; + auto boldfont = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT); + boldfont.SetWeight(wxFONTWEIGHT_BOLD); + + auto *box_slic3r = new wxCheckBox(this, wxID_ANY, _L("Check for application updates")); + box_slic3r->SetValue(app_config->get("notify_release") != "none"); + append(box_slic3r); + append_text(wxString::Format(_L( + "If enabled, %s checks for new application versions online. When a new version becomes available, " + "a notification is displayed at the next application startup (never during program usage). " + "This is only a notification mechanisms, no automatic installation is done."), SLIC3R_APP_NAME)); + + append_spacer(VERTICAL_SPACING); + + auto *box_presets = new wxCheckBox(this, wxID_ANY, _L("Update built-in Presets automatically")); + box_presets->SetValue(app_config->get("preset_update") == "1"); + append(box_presets); + append_text(wxString::Format(_L( + "If enabled, %s downloads updates of built-in system presets in the background." + "These updates are downloaded into a separate temporary location." + "When a new preset version becomes available it is offered at application startup."), SLIC3R_APP_NAME)); + const auto text_bold = _L("Updates are never applied without user's consent and never overwrite user's customized settings."); + auto *label_bold = new wxStaticText(this, wxID_ANY, text_bold); + label_bold->SetFont(boldfont); + label_bold->Wrap(WRAP_WIDTH); + append(label_bold); + append_text(_L("Additionally a backup snapshot of the whole configuration is created before an update is applied.")); + + box_slic3r->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent &event) { this->version_check = event.IsChecked(); }); + box_presets->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent &event) { this->preset_update = event.IsChecked(); }); +} + +PageReloadFromDisk::PageReloadFromDisk(ConfigWizard* parent) + : ConfigWizardPage(parent, _L("Reload from disk"), _L("Reload from disk")) + , full_pathnames(false) +{ + auto* box_pathnames = new wxCheckBox(this, wxID_ANY, _L("Export full pathnames of models and parts sources into 3mf and amf files")); + box_pathnames->SetValue(wxGetApp().app_config->get("export_sources_full_pathnames") == "1"); + append(box_pathnames); + append_text(_L( + "If enabled, allows the Reload from disk command to automatically find and load the files when invoked.\n" + "If not enabled, the Reload from disk command will ask to select each file using an open file dialog." + )); + + box_pathnames->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent& event) { this->full_pathnames = event.IsChecked(); }); +} + +#ifdef _WIN32 +PageFilesAssociation::PageFilesAssociation(ConfigWizard* parent) + : ConfigWizardPage(parent, _L("Files association"), _L("Files association")) +{ + cb_3mf = new wxCheckBox(this, wxID_ANY, _L("Associate .3mf files to PrusaSlicer")); + cb_stl = new wxCheckBox(this, wxID_ANY, _L("Associate .stl files to PrusaSlicer")); +// cb_gcode = new wxCheckBox(this, wxID_ANY, _L("Associate .gcode files to PrusaSlicer G-code Viewer")); + + append(cb_3mf); + append(cb_stl); +// append(cb_gcode); +} +#endif // _WIN32 + +PageMode::PageMode(ConfigWizard *parent) + : ConfigWizardPage(parent, _L("View mode"), _L("View mode")) +{ + append_text(_L("PrusaSlicer's user interfaces comes in three variants:\nSimple, Advanced, and Expert.\n" + "The Simple mode shows only the most frequently used settings relevant for regular 3D printing. " + "The other two offer progressively more sophisticated fine-tuning, " + "they are suitable for advanced and expert users, respectively.")); + + radio_simple = new wxRadioButton(this, wxID_ANY, _L("Simple mode")); + radio_advanced = new wxRadioButton(this, wxID_ANY, _L("Advanced mode")); + radio_expert = new wxRadioButton(this, wxID_ANY, _L("Expert mode")); + + std::string mode { "simple" }; + wxGetApp().app_config->get("", "view_mode", mode); + + if (mode == "advanced") { radio_advanced->SetValue(true); } + else if (mode == "expert") { radio_expert->SetValue(true); } + else { radio_simple->SetValue(true); } + + append(radio_simple); + append(radio_advanced); + append(radio_expert); + + append_text("\n" + _L("The size of the object can be specified in inches")); + check_inch = new wxCheckBox(this, wxID_ANY, _L("Use inches")); + check_inch->SetValue(wxGetApp().app_config->get("use_inches") == "1"); + append(check_inch); + + on_activate(); +} + +void PageMode::serialize_mode(AppConfig *app_config) const +{ + std::string mode = ""; + + if (radio_simple->GetValue()) { mode = "simple"; } + if (radio_advanced->GetValue()) { mode = "advanced"; } + if (radio_expert->GetValue()) { mode = "expert"; } + + app_config->set("view_mode", mode); + app_config->set("use_inches", check_inch->GetValue() ? "1" : "0"); +} + +PageVendors::PageVendors(ConfigWizard *parent) + : ConfigWizardPage(parent, _L("Other Vendors"), _L("Other Vendors")) +{ + const AppConfig &appconfig = this->wizard_p()->appconfig_new; + + append_text(wxString::Format(_L("Pick another vendor supported by %s"), SLIC3R_APP_NAME) + ":"); + + auto boldfont = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT); + boldfont.SetWeight(wxFONTWEIGHT_BOLD); + + for (const auto &pair : wizard_p()->bundles) { + const VendorProfile *vendor = pair.second.vendor_profile; + if (vendor->id == PresetBundle::PRUSA_BUNDLE) { continue; } + + auto *cbox = new wxCheckBox(this, wxID_ANY, vendor->name); + cbox->Bind(wxEVT_CHECKBOX, [=](wxCommandEvent &event) { + wizard_p()->on_3rdparty_install(vendor, cbox->IsChecked()); + }); + + const auto &vendors = appconfig.vendors(); + const bool enabled = vendors.find(pair.first) != vendors.end(); + if (enabled) { + cbox->SetValue(true); + + auto pages = wizard_p()->pages_3rdparty.find(vendor->id); + wxCHECK_RET(pages != wizard_p()->pages_3rdparty.end(), "Internal error: 3rd party vendor printers page not created"); + + for (PagePrinters* page : { pages->second.first, pages->second.second }) + if (page) page->install = true; + } + + append(cbox); + } +} + +PageFirmware::PageFirmware(ConfigWizard *parent) + : ConfigWizardPage(parent, _L("Firmware Type"), _L("Firmware"), 1) + , gcode_opt(*print_config_def.get("gcode_flavor")) + , gcode_picker(nullptr) +{ + append_text(_L("Choose the type of firmware used by your printer.")); + append_text(_(gcode_opt.tooltip)); + + wxArrayString choices; + choices.Alloc(gcode_opt.enum_labels.size()); + for (const auto &label : gcode_opt.enum_labels) { + choices.Add(label); + } + + gcode_picker = new wxChoice(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, choices); + wxGetApp().UpdateDarkUI(gcode_picker); + const auto &enum_values = gcode_opt.enum_values; + auto needle = enum_values.cend(); + if (gcode_opt.default_value) { + needle = std::find(enum_values.cbegin(), enum_values.cend(), gcode_opt.default_value->serialize()); + } + if (needle != enum_values.cend()) { + gcode_picker->SetSelection(needle - enum_values.cbegin()); + } else { + gcode_picker->SetSelection(0); + } + + append(gcode_picker); +} + +void PageFirmware::apply_custom_config(DynamicPrintConfig &config) +{ + auto sel = gcode_picker->GetSelection(); + if (sel >= 0 && (size_t)sel < gcode_opt.enum_labels.size()) { + auto *opt = new ConfigOptionEnum(static_cast(sel)); + config.set_key_value("gcode_flavor", opt); + } +} + +PageBedShape::PageBedShape(ConfigWizard *parent) + : ConfigWizardPage(parent, _L("Bed Shape and Size"), _L("Bed Shape"), 1) + , shape_panel(new BedShapePanel(this)) +{ + append_text(_L("Set the shape of your printer's bed.")); + + shape_panel->build_panel(*wizard_p()->custom_config->option("bed_shape"), + *wizard_p()->custom_config->option("bed_custom_texture"), + *wizard_p()->custom_config->option("bed_custom_model")); + + append(shape_panel); +} + +void PageBedShape::apply_custom_config(DynamicPrintConfig &config) +{ + const std::vector& points = shape_panel->get_shape(); + const std::string& custom_texture = shape_panel->get_custom_texture(); + const std::string& custom_model = shape_panel->get_custom_model(); + config.set_key_value("bed_shape", new ConfigOptionPoints(points)); + config.set_key_value("bed_custom_texture", new ConfigOptionString(custom_texture)); + config.set_key_value("bed_custom_model", new ConfigOptionString(custom_model)); +} + +static void focus_event(wxFocusEvent& e, wxTextCtrl* ctrl, double def_value) +{ + e.Skip(); + wxString str = ctrl->GetValue(); + + const char dec_sep = is_decimal_separator_point() ? '.' : ','; + const char dec_sep_alt = dec_sep == '.' ? ',' : '.'; + // Replace the first incorrect separator in decimal number. + bool was_replaced = str.Replace(dec_sep_alt, dec_sep, false) != 0; + + double val = 0.0; + if (!str.ToDouble(&val)) { + if (val == 0.0) + val = def_value; + ctrl->SetValue(double_to_string(val)); + show_error(nullptr, _L("Invalid numeric input.")); + ctrl->SetFocus(); + } + else if (was_replaced) + ctrl->SetValue(double_to_string(val)); +} + +class DiamTextCtrl : public wxTextCtrl +{ +public: + DiamTextCtrl(wxWindow* parent) + { +#ifdef _WIN32 + long style = wxBORDER_SIMPLE; +#else + long style = 0; +#endif + Create(parent, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(Field::def_width_thinner() * wxGetApp().em_unit(), wxDefaultCoord), style); + wxGetApp().UpdateDarkUI(this); + } + ~DiamTextCtrl() {} +}; + +PageDiameters::PageDiameters(ConfigWizard *parent) + : ConfigWizardPage(parent, _L("Filament and Nozzle Diameters"), _L("Print Diameters"), 1) + , diam_nozzle(new DiamTextCtrl(this)) + , diam_filam (new DiamTextCtrl(this)) +{ + auto *default_nozzle = print_config_def.get("nozzle_diameter")->get_default_value(); + wxString value = double_to_string(default_nozzle != nullptr && default_nozzle->size() > 0 ? default_nozzle->get_at(0) : 0.5); + diam_nozzle->SetValue(value); + + auto *default_filam = print_config_def.get("filament_diameter")->get_default_value(); + value = double_to_string(default_filam != nullptr && default_filam->size() > 0 ? default_filam->get_at(0) : 3.0); + diam_filam->SetValue(value); + + diam_nozzle->Bind(wxEVT_KILL_FOCUS, [this](wxFocusEvent& e) { focus_event(e, diam_nozzle, 0.5); }, diam_nozzle->GetId()); + diam_filam ->Bind(wxEVT_KILL_FOCUS, [this](wxFocusEvent& e) { focus_event(e, diam_filam , 3.0); }, diam_filam->GetId()); + + append_text(_L("Enter the diameter of your printer's hot end nozzle.")); + + auto *sizer_nozzle = new wxFlexGridSizer(3, 5, 5); + auto *text_nozzle = new wxStaticText(this, wxID_ANY, _L("Nozzle Diameter:")); + auto *unit_nozzle = new wxStaticText(this, wxID_ANY, _L("mm")); + sizer_nozzle->AddGrowableCol(0, 1); + sizer_nozzle->Add(text_nozzle, 0, wxALIGN_CENTRE_VERTICAL); + sizer_nozzle->Add(diam_nozzle); + sizer_nozzle->Add(unit_nozzle, 0, wxALIGN_CENTRE_VERTICAL); + append(sizer_nozzle); + + append_spacer(VERTICAL_SPACING); + + append_text(_L("Enter the diameter of your filament.")); + append_text(_L("Good precision is required, so use a caliper and do multiple measurements along the filament, then compute the average.")); + + auto *sizer_filam = new wxFlexGridSizer(3, 5, 5); + auto *text_filam = new wxStaticText(this, wxID_ANY, _L("Filament Diameter:")); + auto *unit_filam = new wxStaticText(this, wxID_ANY, _L("mm")); + sizer_filam->AddGrowableCol(0, 1); + sizer_filam->Add(text_filam, 0, wxALIGN_CENTRE_VERTICAL); + sizer_filam->Add(diam_filam); + sizer_filam->Add(unit_filam, 0, wxALIGN_CENTRE_VERTICAL); + append(sizer_filam); +} + +void PageDiameters::apply_custom_config(DynamicPrintConfig &config) +{ + double val = 0.0; + diam_nozzle->GetValue().ToDouble(&val); + auto *opt_nozzle = new ConfigOptionFloats(1, val); + config.set_key_value("nozzle_diameter", opt_nozzle); + + val = 0.0; + diam_filam->GetValue().ToDouble(&val); + auto * opt_filam = new ConfigOptionFloats(1, val); + config.set_key_value("filament_diameter", opt_filam); + + auto set_extrusion_width = [&config, opt_nozzle](const char *key, double dmr) { + char buf[64]; // locales don't matter here (sprintf/atof) + sprintf(buf, "%.2lf", dmr * opt_nozzle->values.front() / 0.4); + config.set_key_value(key, new ConfigOptionFloatOrPercent(atof(buf), false)); + }; + + set_extrusion_width("support_material_extrusion_width", 0.35); + set_extrusion_width("top_infill_extrusion_width", 0.40); + set_extrusion_width("first_layer_extrusion_width", 0.42); + + set_extrusion_width("extrusion_width", 0.45); + set_extrusion_width("perimeter_extrusion_width", 0.45); + set_extrusion_width("external_perimeter_extrusion_width", 0.45); + set_extrusion_width("infill_extrusion_width", 0.45); + set_extrusion_width("solid_infill_extrusion_width", 0.45); +} + +class SpinCtrlDouble: public wxSpinCtrlDouble +{ +public: + SpinCtrlDouble(wxWindow* parent) + { +#ifdef _WIN32 + long style = wxSP_ARROW_KEYS | wxBORDER_SIMPLE; +#else + long style = wxSP_ARROW_KEYS; +#endif + Create(parent, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, style); +#ifdef _WIN32 + wxGetApp().UpdateDarkUI(this->GetText()); +#endif + this->Refresh(); + } + ~SpinCtrlDouble() {} +}; + +PageTemperatures::PageTemperatures(ConfigWizard *parent) + : ConfigWizardPage(parent, _L("Nozzle and Bed Temperatures"), _L("Temperatures"), 1) + , spin_extr(new SpinCtrlDouble(this)) + , spin_bed (new SpinCtrlDouble(this)) +{ + spin_extr->SetIncrement(5.0); + const auto &def_extr = *print_config_def.get("temperature"); + spin_extr->SetRange(def_extr.min, def_extr.max); + auto *default_extr = def_extr.get_default_value(); + spin_extr->SetValue(default_extr != nullptr && default_extr->size() > 0 ? default_extr->get_at(0) : 200); + + spin_bed->SetIncrement(5.0); + const auto &def_bed = *print_config_def.get("bed_temperature"); + spin_bed->SetRange(def_bed.min, def_bed.max); + auto *default_bed = def_bed.get_default_value(); + spin_bed->SetValue(default_bed != nullptr && default_bed->size() > 0 ? default_bed->get_at(0) : 0); + + append_text(_L("Enter the temperature needed for extruding your filament.")); + append_text(_L("A rule of thumb is 160 to 230 °C for PLA, and 215 to 250 °C for ABS.")); + + auto *sizer_extr = new wxFlexGridSizer(3, 5, 5); + auto *text_extr = new wxStaticText(this, wxID_ANY, _L("Extrusion Temperature:")); + auto *unit_extr = new wxStaticText(this, wxID_ANY, _L("°C")); + sizer_extr->AddGrowableCol(0, 1); + sizer_extr->Add(text_extr, 0, wxALIGN_CENTRE_VERTICAL); + sizer_extr->Add(spin_extr); + sizer_extr->Add(unit_extr, 0, wxALIGN_CENTRE_VERTICAL); + append(sizer_extr); + + append_spacer(VERTICAL_SPACING); + + append_text(_L("Enter the bed temperature needed for getting your filament to stick to your heated bed.")); + append_text(_L("A rule of thumb is 60 °C for PLA and 110 °C for ABS. Leave zero if you have no heated bed.")); + + auto *sizer_bed = new wxFlexGridSizer(3, 5, 5); + auto *text_bed = new wxStaticText(this, wxID_ANY, _L("Bed Temperature:")); + auto *unit_bed = new wxStaticText(this, wxID_ANY, _L("°C")); + sizer_bed->AddGrowableCol(0, 1); + sizer_bed->Add(text_bed, 0, wxALIGN_CENTRE_VERTICAL); + sizer_bed->Add(spin_bed); + sizer_bed->Add(unit_bed, 0, wxALIGN_CENTRE_VERTICAL); + append(sizer_bed); +} + +void PageTemperatures::apply_custom_config(DynamicPrintConfig &config) +{ + auto *opt_extr = new ConfigOptionInts(1, spin_extr->GetValue()); + config.set_key_value("temperature", opt_extr); + auto *opt_extr1st = new ConfigOptionInts(1, spin_extr->GetValue()); + config.set_key_value("first_layer_temperature", opt_extr1st); + auto *opt_bed = new ConfigOptionInts(1, spin_bed->GetValue()); + config.set_key_value("bed_temperature", opt_bed); + auto *opt_bed1st = new ConfigOptionInts(1, spin_bed->GetValue()); + config.set_key_value("first_layer_bed_temperature", opt_bed1st); +} + + +// Index + +ConfigWizardIndex::ConfigWizardIndex(wxWindow *parent) + : wxPanel(parent) + , bg(ScalableBitmap(parent, "PrusaSlicer_192px_transparent.png", 192)) + , bullet_black(ScalableBitmap(parent, "bullet_black.png")) + , bullet_blue(ScalableBitmap(parent, "bullet_blue.png")) + , bullet_white(ScalableBitmap(parent, "bullet_white.png")) + , item_active(NO_ITEM) + , item_hover(NO_ITEM) + , last_page((size_t)-1) +{ +#ifndef __WXOSX__ + SetDoubleBuffered(true);// SetDoubleBuffered exists on Win and Linux/GTK, but is missing on OSX +#endif //__WXOSX__ + SetMinSize(bg.GetSize()); + + const wxSize size = GetTextExtent("m"); + em_w = size.x; + em_h = size.y; + + Bind(wxEVT_PAINT, &ConfigWizardIndex::on_paint, this); + Bind(wxEVT_SIZE, [this](wxEvent& e) { e.Skip(); Refresh(); }); + Bind(wxEVT_MOTION, &ConfigWizardIndex::on_mouse_move, this); + + Bind(wxEVT_LEAVE_WINDOW, [this](wxMouseEvent &evt) { + if (item_hover != -1) { + item_hover = -1; + Refresh(); + } + evt.Skip(); + }); + + Bind(wxEVT_LEFT_UP, [this](wxMouseEvent &evt) { + if (item_hover >= 0) { go_to(item_hover); } + }); +} + +wxDECLARE_EVENT(EVT_INDEX_PAGE, wxCommandEvent); + +void ConfigWizardIndex::add_page(ConfigWizardPage *page) +{ + last_page = items.size(); + items.emplace_back(Item { page->shortname, page->indent, page }); + Refresh(); +} + +void ConfigWizardIndex::add_label(wxString label, unsigned indent) +{ + items.emplace_back(Item { std::move(label), indent, nullptr }); + Refresh(); +} + +ConfigWizardPage* ConfigWizardIndex::active_page() const +{ + if (item_active >= items.size()) { return nullptr; } + + return items[item_active].page; +} + +void ConfigWizardIndex::go_prev() +{ + // Search for a preceiding item that is a page (not a label, ie. page != nullptr) + + if (item_active == NO_ITEM) { return; } + + for (size_t i = item_active; i > 0; i--) { + if (items[i - 1].page != nullptr) { + go_to(i - 1); + return; + } + } +} + +void ConfigWizardIndex::go_next() +{ + // Search for a next item that is a page (not a label, ie. page != nullptr) + + if (item_active == NO_ITEM) { return; } + + for (size_t i = item_active + 1; i < items.size(); i++) { + if (items[i].page != nullptr) { + go_to(i); + return; + } + } +} + +// This one actually performs the go-to op +void ConfigWizardIndex::go_to(size_t i) +{ + if (i != item_active + && i < items.size() + && items[i].page != nullptr) { + auto *new_active = items[i].page; + auto *former_active = active_page(); + if (former_active != nullptr) { + former_active->Hide(); + } + + item_active = i; + new_active->Show(); + + wxCommandEvent evt(EVT_INDEX_PAGE, GetId()); + AddPendingEvent(evt); + + Refresh(); + + new_active->on_activate(); + } +} + +void ConfigWizardIndex::go_to(const ConfigWizardPage *page) +{ + if (page == nullptr) { return; } + + for (size_t i = 0; i < items.size(); i++) { + if (items[i].page == page) { + go_to(i); + return; + } + } +} + +void ConfigWizardIndex::clear() +{ + auto *former_active = active_page(); + if (former_active != nullptr) { former_active->Hide(); } + + items.clear(); + item_active = NO_ITEM; +} + +void ConfigWizardIndex::on_paint(wxPaintEvent & evt) +{ + const auto size = GetClientSize(); + if (size.GetHeight() == 0 || size.GetWidth() == 0) { return; } + + wxPaintDC dc(this); + + const auto bullet_w = bullet_black.GetWidth(); + const auto bullet_h = bullet_black.GetHeight(); + const int yoff_icon = bullet_h < em_h ? (em_h - bullet_h) / 2 : 0; + const int yoff_text = bullet_h > em_h ? (bullet_h - em_h) / 2 : 0; + const int yinc = item_height(); + + int index_width = 0; + + unsigned y = 0; + for (size_t i = 0; i < items.size(); i++) { + const Item& item = items[i]; + unsigned x = em_w/2 + item.indent * em_w; + + if (i == item_active || (item_hover >= 0 && i == (size_t)item_hover)) { + dc.DrawBitmap(bullet_blue.get_bitmap(), x, y + yoff_icon, false); + } + else if (i < item_active) { dc.DrawBitmap(bullet_black.get_bitmap(), x, y + yoff_icon, false); } + else if (i > item_active) { dc.DrawBitmap(bullet_white.get_bitmap(), x, y + yoff_icon, false); } + + x += + bullet_w + em_w/2; + const auto text_size = dc.GetTextExtent(item.label); + dc.SetTextForeground(wxGetApp().get_label_clr_default()); + dc.DrawText(item.label, x, y + yoff_text); + + y += yinc; + index_width = std::max(index_width, (int)x + text_size.x); + } + + //draw logo + if (int y = size.y - bg.GetHeight(); y>=0) { + dc.DrawBitmap(bg.get_bitmap(), 0, y, false); + index_width = std::max(index_width, bg.GetWidth() + em_w / 2); + } + + if (GetMinSize().x < index_width) { + CallAfter([this, index_width]() { + SetMinSize(wxSize(index_width, GetMinSize().y)); + Refresh(); + }); + } +} + +void ConfigWizardIndex::on_mouse_move(wxMouseEvent &evt) +{ + const wxClientDC dc(this); + const wxPoint pos = evt.GetLogicalPosition(dc); + + const ssize_t item_hover_new = pos.y / item_height(); + + if (item_hover_new < ssize_t(items.size()) && item_hover_new != item_hover) { + item_hover = item_hover_new; + Refresh(); + } + + evt.Skip(); +} + +void ConfigWizardIndex::msw_rescale() +{ + const wxSize size = GetTextExtent("m"); + em_w = size.x; + em_h = size.y; + + SetMinSize(bg.GetSize()); + + Refresh(); +} + + +// Materials + +const std::string Materials::UNKNOWN = "(Unknown)"; + +void Materials::push(const Preset *preset) +{ + presets.emplace_back(preset); + types.insert(technology & T_FFF + ? Materials::get_filament_type(preset) + : Materials::get_material_type(preset)); +} + +void Materials::add_printer(const Preset* preset) +{ + printers.insert(preset); +} + +void Materials::clear() +{ + presets.clear(); + types.clear(); + printers.clear(); + compatibility_counter.clear(); +} + +const std::string& Materials::appconfig_section() const +{ + return (technology & T_FFF) ? AppConfig::SECTION_FILAMENTS : AppConfig::SECTION_MATERIALS; +} + +const std::string& Materials::get_type(const Preset *preset) const +{ + return (technology & T_FFF) ? get_filament_type(preset) : get_material_type(preset); +} + +const std::string& Materials::get_vendor(const Preset *preset) const +{ + return (technology & T_FFF) ? get_filament_vendor(preset) : get_material_vendor(preset); +} + +const std::string& Materials::get_filament_type(const Preset *preset) +{ + const auto *opt = preset->config.opt("filament_type"); + if (opt != nullptr && opt->values.size() > 0) { + return opt->values[0]; + } else { + return UNKNOWN; + } +} + +const std::string& Materials::get_filament_vendor(const Preset *preset) +{ + const auto *opt = preset->config.opt("filament_vendor"); + return opt != nullptr ? opt->value : UNKNOWN; +} + +const std::string& Materials::get_material_type(const Preset *preset) +{ + const auto *opt = preset->config.opt("material_type"); + if (opt != nullptr) { + return opt->value; + } else { + return UNKNOWN; + } +} + +const std::string& Materials::get_material_vendor(const Preset *preset) +{ + const auto *opt = preset->config.opt("material_vendor"); + return opt != nullptr ? opt->value : UNKNOWN; +} + +// priv + +static const std::unordered_map> legacy_preset_map {{ + { "Original Prusa i3 MK2.ini", std::make_pair("MK2S", "0.4") }, + { "Original Prusa i3 MK2 MM Single Mode.ini", std::make_pair("MK2SMM", "0.4") }, + { "Original Prusa i3 MK2 MM Single Mode 0.6 nozzle.ini", std::make_pair("MK2SMM", "0.6") }, + { "Original Prusa i3 MK2 MultiMaterial.ini", std::make_pair("MK2SMM", "0.4") }, + { "Original Prusa i3 MK2 MultiMaterial 0.6 nozzle.ini", std::make_pair("MK2SMM", "0.6") }, + { "Original Prusa i3 MK2 0.25 nozzle.ini", std::make_pair("MK2S", "0.25") }, + { "Original Prusa i3 MK2 0.6 nozzle.ini", std::make_pair("MK2S", "0.6") }, + { "Original Prusa i3 MK3.ini", std::make_pair("MK3", "0.4") }, +}}; + +void ConfigWizard::priv::load_pages() +{ + wxWindowUpdateLocker freeze_guard(q); + (void)freeze_guard; + + const ConfigWizardPage *former_active = index->active_page(); + + index->clear(); + + index->add_page(page_welcome); + + // Printers + if (!only_sla_mode) + index->add_page(page_fff); + index->add_page(page_msla); + if (!only_sla_mode) { + index->add_page(page_vendors); + for (const auto &pages : pages_3rdparty) { + for ( PagePrinters* page : { pages.second.first, pages.second.second }) + if (page && page->install) + index->add_page(page); + } + + index->add_page(page_custom); + if (page_custom->custom_wanted()) { + index->add_page(page_firmware); + index->add_page(page_bed); + index->add_page(page_diams); + index->add_page(page_temps); + } + + // Filaments & Materials + if (any_fff_selected) { index->add_page(page_filaments); } + } + if (any_sla_selected) { index->add_page(page_sla_materials); } + + // there should to be selected at least one printer + btn_finish->Enable(any_fff_selected || any_sla_selected || custom_printer_selected); + + index->add_page(page_update); + index->add_page(page_reload_from_disk); +#ifdef _WIN32 + index->add_page(page_files_association); +#endif // _WIN32 + index->add_page(page_mode); + + index->go_to(former_active); // Will restore the active item/page if possible + + q->Layout(); +// This Refresh() is needed to avoid ugly artifacts after printer selection, when no one vendor was selected from the very beginnig + q->Refresh(); +} + +void ConfigWizard::priv::init_dialog_size() +{ + // Clamp the Wizard size based on screen dimensions + + const auto idx = wxDisplay::GetFromWindow(q); + wxDisplay display(idx != wxNOT_FOUND ? idx : 0u); + + const auto disp_rect = display.GetClientArea(); + wxRect window_rect( + disp_rect.x + disp_rect.width / 20, + disp_rect.y + disp_rect.height / 20, + 9*disp_rect.width / 10, + 9*disp_rect.height / 10); + + const int width_hint = index->GetSize().GetWidth() + std::max(90 * em(), (only_sla_mode ? page_msla->get_width() : page_fff->get_width()) + 30 * em()); // XXX: magic constant, I found no better solution + if (width_hint < window_rect.width) { + window_rect.x += (window_rect.width - width_hint) / 2; + window_rect.width = width_hint; + } + + q->SetSize(window_rect); +} + +void ConfigWizard::priv::load_vendors() +{ + bundles = BundleMap::load(); + + // Load up the set of vendors / models / variants the user has had enabled up till now + AppConfig *app_config = wxGetApp().app_config; + if (! app_config->legacy_datadir()) { + appconfig_new.set_vendors(*app_config); + } else { + // In case of legacy datadir, try to guess the preference based on the printer preset files that are present + const auto printer_dir = fs::path(Slic3r::data_dir()) / "printer"; + for (auto &dir_entry : boost::filesystem::directory_iterator(printer_dir)) + if (Slic3r::is_ini_file(dir_entry)) { + auto needle = legacy_preset_map.find(dir_entry.path().filename().string()); + if (needle == legacy_preset_map.end()) { continue; } + + const auto &model = needle->second.first; + const auto &variant = needle->second.second; + appconfig_new.set_variant("PrusaResearch", model, variant, true); + } + } + + // Initialize the is_visible flag in printer Presets + for (auto &pair : bundles) { + pair.second.preset_bundle->load_installed_printers(appconfig_new); + } + + // Copy installed filaments and SLA material names from app_config to appconfig_new + // while resolving current names of profiles, which were renamed in the meantime. + for (PrinterTechnology technology : { ptFFF, ptSLA }) { + const std::string §ion_name = (technology == ptFFF) ? AppConfig::SECTION_FILAMENTS : AppConfig::SECTION_MATERIALS; + std::map section_new; + if (app_config->has_section(section_name)) { + const std::map §ion_old = app_config->get_section(section_name); + for (const auto& material_name_and_installed : section_old) + if (material_name_and_installed.second == "1") { + // Material is installed. Resolve it in bundles. + size_t num_found = 0; + const std::string &material_name = material_name_and_installed.first; + for (auto &bundle : bundles) { + const PresetCollection &materials = bundle.second.preset_bundle->materials(technology); + const Preset *preset = materials.find_preset(material_name); + if (preset == nullptr) { + // Not found. Maybe the material preset is there, bu it was was renamed? + const std::string *new_name = materials.get_preset_name_renamed(material_name); + if (new_name != nullptr) + preset = materials.find_preset(*new_name); + } + if (preset != nullptr) { + // Materal preset was found, mark it as installed. + section_new[preset->name] = "1"; + ++ num_found; + } + } + if (num_found == 0) + BOOST_LOG_TRIVIAL(error) << boost::format("Profile %1% was not found in installed vendor Preset Bundles.") % material_name; + else if (num_found > 1) + BOOST_LOG_TRIVIAL(error) << boost::format("Profile %1% was found in %2% vendor Preset Bundles.") % material_name % num_found; + } + } + appconfig_new.set_section(section_name, section_new); + }; +} + +void ConfigWizard::priv::add_page(ConfigWizardPage *page) +{ + const int proportion = (page->shortname == _L("Filaments")) || (page->shortname == _L("SLA Materials")) ? 1 : 0; + hscroll_sizer->Add(page, proportion, wxEXPAND); + all_pages.push_back(page); +} + +void ConfigWizard::priv::enable_next(bool enable) +{ + btn_next->Enable(enable); + btn_finish->Enable(enable); +} + +void ConfigWizard::priv::set_start_page(ConfigWizard::StartPage start_page) +{ + switch (start_page) { + case ConfigWizard::SP_PRINTERS: + index->go_to(page_fff); + btn_next->SetFocus(); + break; + case ConfigWizard::SP_FILAMENTS: + index->go_to(page_filaments); + btn_finish->SetFocus(); + break; + case ConfigWizard::SP_MATERIALS: + index->go_to(page_sla_materials); + btn_finish->SetFocus(); + break; + default: + index->go_to(page_welcome); + btn_next->SetFocus(); + break; + } +} + +void ConfigWizard::priv::create_3rdparty_pages() +{ + for (const auto &pair : bundles) { + const VendorProfile *vendor = pair.second.vendor_profile; + if (vendor->id == PresetBundle::PRUSA_BUNDLE) { continue; } + + bool is_fff_technology = false; + bool is_sla_technology = false; + + for (auto& model: vendor->models) + { + if (!is_fff_technology && model.technology == ptFFF) + is_fff_technology = true; + if (!is_sla_technology && model.technology == ptSLA) + is_sla_technology = true; + } + + PagePrinters* pageFFF = nullptr; + PagePrinters* pageSLA = nullptr; + + if (is_fff_technology) { + pageFFF = new PagePrinters(q, vendor->name + " " +_L("FFF Technology Printers"), vendor->name+" FFF", *vendor, 1, T_FFF); + add_page(pageFFF); + } + + if (is_sla_technology) { + pageSLA = new PagePrinters(q, vendor->name + " " + _L("SLA Technology Printers"), vendor->name+" MSLA", *vendor, 1, T_SLA); + add_page(pageSLA); + } + + pages_3rdparty.insert({vendor->id, {pageFFF, pageSLA}}); + } +} + +void ConfigWizard::priv::set_run_reason(RunReason run_reason) +{ + this->run_reason = run_reason; + for (auto &page : all_pages) { + page->set_run_reason(run_reason); + } +} + +void ConfigWizard::priv::update_materials(Technology technology) +{ + if (any_fff_selected && (technology & T_FFF)) { + filaments.clear(); + aliases_fff.clear(); + // Iterate filaments in all bundles + for (const auto &pair : bundles) { + for (const auto &filament : pair.second.preset_bundle->filaments) { + // Check if filament is already added + if (filaments.containts(&filament)) + continue; + // Iterate printers in all bundles + for (const auto &printer : pair.second.preset_bundle->printers) { + if (!printer.is_visible || printer.printer_technology() != ptFFF) + continue; + // Filter out inapplicable printers + if (is_compatible_with_printer(PresetWithVendorProfile(filament, filament.vendor), PresetWithVendorProfile(printer, printer.vendor))) { + if (!filaments.containts(&filament)) { + filaments.push(&filament); + if (!filament.alias.empty()) + aliases_fff[filament.alias].insert(filament.name); + } + filaments.add_printer(&printer); + } + } + + } + } + // count compatible printers + for (const auto& preset : filaments.presets) { + + const auto filter = [preset](const std::pair element) { + return preset->alias == element.first; + }; + if (std::find_if(filaments.compatibility_counter.begin(), filaments.compatibility_counter.end(), filter) != filaments.compatibility_counter.end()) { + continue; + } + std::vector idx_with_same_alias; + for (size_t i = 0; i < filaments.presets.size(); ++i) { + if (preset->alias == filaments.presets[i]->alias) + idx_with_same_alias.push_back(i); + } + size_t counter = 0; + for (const auto& printer : filaments.printers) { + if (!(*printer).is_visible || (*printer).printer_technology() != ptFFF) + continue; + bool compatible = false; + // Test otrher materials with same alias + for (size_t i = 0; i < idx_with_same_alias.size() && !compatible; ++i) { + const Preset& prst = *(filaments.presets[idx_with_same_alias[i]]); + const Preset& prntr = *printer; + if (is_compatible_with_printer(PresetWithVendorProfile(prst, prst.vendor), PresetWithVendorProfile(prntr, prntr.vendor))) { + compatible = true; + break; + } + } + if (compatible) + counter++; + } + filaments.compatibility_counter.emplace_back(preset->alias, counter); + } + } + + if (any_sla_selected && (technology & T_SLA)) { + sla_materials.clear(); + aliases_sla.clear(); + + // Iterate SLA materials in all bundles + for (const auto &pair : bundles) { + for (const auto &material : pair.second.preset_bundle->sla_materials) { + // Check if material is already added + if (sla_materials.containts(&material)) + continue; + // Iterate printers in all bundles + // For now, we only allow the profiles to be compatible with another profiles inside the same bundle. + for (const auto& printer : pair.second.preset_bundle->printers) { + if(!printer.is_visible || printer.printer_technology() != ptSLA) + continue; + // Filter out inapplicable printers + if (is_compatible_with_printer(PresetWithVendorProfile(material, nullptr), PresetWithVendorProfile(printer, nullptr))) { + // Check if material is already added + if(!sla_materials.containts(&material)) { + sla_materials.push(&material); + if (!material.alias.empty()) + aliases_sla[material.alias].insert(material.name); + } + sla_materials.add_printer(&printer); + } + } + } + } + // count compatible printers + for (const auto& preset : sla_materials.presets) { + + const auto filter = [preset](const std::pair element) { + return preset->alias == element.first; + }; + if (std::find_if(sla_materials.compatibility_counter.begin(), sla_materials.compatibility_counter.end(), filter) != sla_materials.compatibility_counter.end()) { + continue; + } + std::vector idx_with_same_alias; + for (size_t i = 0; i < sla_materials.presets.size(); ++i) { + if(preset->alias == sla_materials.presets[i]->alias) + idx_with_same_alias.push_back(i); + } + size_t counter = 0; + for (const auto& printer : sla_materials.printers) { + if (!(*printer).is_visible || (*printer).printer_technology() != ptSLA) + continue; + bool compatible = false; + // Test otrher materials with same alias + for (size_t i = 0; i < idx_with_same_alias.size() && !compatible; ++i) { + const Preset& prst = *(sla_materials.presets[idx_with_same_alias[i]]); + const Preset& prntr = *printer; + if (is_compatible_with_printer(PresetWithVendorProfile(prst, prst.vendor), PresetWithVendorProfile(prntr, prntr.vendor))) { + compatible = true; + break; + } + } + if (compatible) + counter++; + } + sla_materials.compatibility_counter.emplace_back(preset->alias, counter); + } + } +} + +void ConfigWizard::priv::on_custom_setup(const bool custom_wanted) +{ + custom_printer_selected = custom_wanted; + load_pages(); +} + +void ConfigWizard::priv::on_printer_pick(PagePrinters *page, const PrinterPickerEvent &evt) +{ + if (check_sla_selected() != any_sla_selected || + check_fff_selected() != any_fff_selected) { + any_fff_selected = check_fff_selected(); + any_sla_selected = check_sla_selected(); + + load_pages(); + } + + // Update the is_visible flag on relevant printer profiles + for (auto &pair : bundles) { + if (pair.first != evt.vendor_id) { continue; } + + for (auto &preset : pair.second.preset_bundle->printers) { + if (preset.config.opt_string("printer_model") == evt.model_id + && preset.config.opt_string("printer_variant") == evt.variant_name) { + preset.is_visible = evt.enable; + } + } + + // When a printer model is picked, but there is no material installed compatible with this printer model, + // install default materials for selected printer model silently. + check_and_install_missing_materials(page->technology, evt.model_id); + } + + if (page->technology & T_FFF) { + page_filaments->clear(); + } else if (page->technology & T_SLA) { + page_sla_materials->clear(); + } +} + +void ConfigWizard::priv::select_default_materials_for_printer_model(const VendorProfile::PrinterModel &printer_model, Technology technology) +{ + PageMaterials* page_materials = technology & T_FFF ? page_filaments : page_sla_materials; + for (const std::string& material : printer_model.default_materials) + appconfig_new.set(page_materials->materials->appconfig_section(), material, "1"); +} + +void ConfigWizard::priv::select_default_materials_for_printer_models(Technology technology, const std::set &printer_models) +{ + PageMaterials *page_materials = technology & T_FFF ? page_filaments : page_sla_materials; + const std::string &appconfig_section = page_materials->materials->appconfig_section(); + + // Following block was unnecessary. Its enough to iterate printer_models once. Not for every vendor printer page. + // Filament is selected on same page for all printers of same technology. + /* + auto select_default_materials_for_printer_page = [this, appconfig_section, printer_models, technology](PagePrinters *page_printers, Technology technology) + { + const std::string vendor_id = page_printers->get_vendor_id(); + for (auto& pair : bundles) + if (pair.first == vendor_id) + for (const VendorProfile::PrinterModel *printer_model : printer_models) + for (const std::string &material : printer_model->default_materials) + appconfig_new.set(appconfig_section, material, "1"); + }; + + PagePrinters* page_printers = technology & T_FFF ? page_fff : page_msla; + select_default_materials_for_printer_page(page_printers, technology); + + for (const auto& printer : pages_3rdparty) + { + page_printers = technology & T_FFF ? printer.second.first : printer.second.second; + if (page_printers) + select_default_materials_for_printer_page(page_printers, technology); + } + */ + + // Iterate printer_models and select default materials. If none available -> msg to user. + std::vector models_without_default; + for (const VendorProfile::PrinterModel* printer_model : printer_models) { + if (printer_model->default_materials.empty()) { + models_without_default.emplace_back(printer_model); + } else { + for (const std::string& material : printer_model->default_materials) + appconfig_new.set(appconfig_section, material, "1"); + } + } + + if (!models_without_default.empty()) { + std::string printer_names = "\n\n"; + for (const VendorProfile::PrinterModel* printer_model : models_without_default) { + printer_names += printer_model->name + "\n"; + } + printer_names += "\n\n"; + std::string message = (technology & T_FFF ? + GUI::format(_L("Following printer profiles has no default filament: %1%Please select one manually."), printer_names) : + GUI::format(_L("Following printer profiles has no default material: %1%Please select one manually."), printer_names)); + MessageDialog msg(q, message, _L("Notice"), wxOK); + msg.ShowModal(); + } + + update_materials(technology); + ((technology & T_FFF) ? page_filaments : page_sla_materials)->reload_presets(); +} + +void ConfigWizard::priv::on_3rdparty_install(const VendorProfile *vendor, bool install) +{ + auto it = pages_3rdparty.find(vendor->id); + wxCHECK_RET(it != pages_3rdparty.end(), "Internal error: GUI page not found for 3rd party vendor profile"); + + for (PagePrinters* page : { it->second.first, it->second.second }) + if (page) { + if (page->install && !install) + page->select_all(false); + page->install = install; + // if some 3rd vendor is selected, select first printer for them + if (install) + page->printer_pickers[0]->select_one(0, true); + page->Layout(); + } + + load_pages(); +} + +bool ConfigWizard::priv::on_bnt_finish() +{ + wxBusyCursor wait; + /* When Filaments or Sla Materials pages are activated, + * materials for this pages are automaticaly updated and presets are reloaded. + * + * But, if _Finish_ button was clicked without activation of those pages + * (for example, just some printers were added/deleted), + * than last changes wouldn't be updated for filaments/materials. + * SO, do that before close of Wizard + */ + update_materials(T_ANY); + if (any_fff_selected) + page_filaments->reload_presets(); + if (any_sla_selected) + page_sla_materials->reload_presets(); + + // theres no need to check that filament is selected if we have only custom printer + if (custom_printer_selected && !any_fff_selected && !any_sla_selected) return true; + // check, that there is selected at least one filament/material + return check_and_install_missing_materials(T_ANY); +} + +// This allmighty method verifies, whether there is at least a single compatible filament or SLA material installed +// for each Printer preset of each Printer Model installed. +// +// In case only_for_model_id is set, then the test is done for that particular printer model only, and the default materials are installed silently. +// Otherwise the user is quieried whether to install the missing default materials or not. +// +// Return true if the tested Printer Models already had materials installed. +// Return false if there were some Printer Models with missing materials, independent from whether the defaults were installed for these +// respective Printer Models or not. +bool ConfigWizard::priv::check_and_install_missing_materials(Technology technology, const std::string &only_for_model_id) +{ + // Walk over all installed Printer presets and verify whether there is a filament or SLA material profile installed at the same PresetBundle, + // which is compatible with it. + const auto printer_models_missing_materials = [this, only_for_model_id](PrinterTechnology technology, const std::string §ion) + { + const std::map &appconfig_presets = appconfig_new.has_section(section) ? appconfig_new.get_section(section) : std::map(); + std::set printer_models_without_material; + for (const auto &pair : bundles) { + const PresetCollection &materials = pair.second.preset_bundle->materials(technology); + for (const auto &printer : pair.second.preset_bundle->printers) { + if (printer.is_visible && printer.printer_technology() == technology) { + const VendorProfile::PrinterModel *printer_model = PresetUtils::system_printer_model(printer); + assert(printer_model != nullptr); + if ((only_for_model_id.empty() || only_for_model_id == printer_model->id) && + printer_models_without_material.find(printer_model) == printer_models_without_material.end()) { + bool has_material = false; + for (const auto& preset : appconfig_presets) { + if (preset.second == "1") { + const Preset *material = materials.find_preset(preset.first, false); + if (material != nullptr && is_compatible_with_printer(PresetWithVendorProfile(*material, nullptr), PresetWithVendorProfile(printer, nullptr))) { + has_material = true; + break; + } + } + } + if (! has_material) + printer_models_without_material.insert(printer_model); + } + } + } + } + assert(printer_models_without_material.empty() || only_for_model_id.empty() || only_for_model_id == (*printer_models_without_material.begin())->id); + return printer_models_without_material; + }; + + const auto ask_and_select_default_materials = [this](const wxString &message, const std::set &printer_models, Technology technology) + { + //wxMessageDialog msg(q, message, _L("Notice"), wxYES_NO); + MessageDialog msg(q, message, _L("Notice"), wxYES_NO); + if (msg.ShowModal() == wxID_YES) + select_default_materials_for_printer_models(technology, printer_models); + }; + + const auto printer_model_list = [](const std::set &printer_models) -> wxString { + wxString out; + for (const VendorProfile::PrinterModel *printer_model : printer_models) { + wxString name = from_u8(printer_model->name); + out += "\t\t"; + out += name; + out += "\n"; + } + return out; + }; + + if (any_fff_selected && (technology & T_FFF)) { + std::set printer_models_without_material = printer_models_missing_materials(ptFFF, AppConfig::SECTION_FILAMENTS); + if (! printer_models_without_material.empty()) { + if (only_for_model_id.empty()) + ask_and_select_default_materials( + _L("The following FFF printer models have no filament selected:") + + "\n\n\t" + + printer_model_list(printer_models_without_material) + + "\n\n\t" + + _L("Do you want to select default filaments for these FFF printer models?"), + printer_models_without_material, + T_FFF); + else + select_default_materials_for_printer_model(**printer_models_without_material.begin(), T_FFF); + return false; + } + } + + if (any_sla_selected && (technology & T_SLA)) { + std::set printer_models_without_material = printer_models_missing_materials(ptSLA, AppConfig::SECTION_MATERIALS); + if (! printer_models_without_material.empty()) { + if (only_for_model_id.empty()) + ask_and_select_default_materials( + _L("The following SLA printer models have no materials selected:") + + "\n\n\t" + + printer_model_list(printer_models_without_material) + + "\n\n\t" + + _L("Do you want to select default SLA materials for these printer models?"), + printer_models_without_material, + T_SLA); + else + select_default_materials_for_printer_model(**printer_models_without_material.begin(), T_SLA); + return false; + } + } + + return true; +} + +static std::set get_new_added_presets(const std::map& old_data, const std::map& new_data) +{ + auto get_aliases = [](const std::map& data) { + std::set old_aliases; + for (auto item : data) { + const std::string& name = item.first; + size_t pos = name.find("@"); + old_aliases.emplace(pos == std::string::npos ? name : name.substr(0, pos-1)); + } + return old_aliases; + }; + + std::set old_aliases = get_aliases(old_data); + std::set new_aliases = get_aliases(new_data); + std::set diff; + std::set_difference(new_aliases.begin(), new_aliases.end(), old_aliases.begin(), old_aliases.end(), std::inserter(diff, diff.begin())); + + return diff; +} + +static std::string get_first_added_preset(const std::map& old_data, const std::map& new_data) +{ + std::set diff = get_new_added_presets(old_data, new_data); + if (diff.empty()) + return std::string(); + return *diff.begin(); +} + +bool ConfigWizard::priv::apply_config(AppConfig *app_config, PresetBundle *preset_bundle, const PresetUpdater *updater, bool& apply_keeped_changes) +{ + wxString header, caption = _L("Configuration is edited in ConfigWizard"); + const auto enabled_vendors = appconfig_new.vendors(); + const auto enabled_vendors_old = app_config->vendors(); + + bool suppress_sla_printer = model_has_multi_part_objects(wxGetApp().model()); + PrinterTechnology preferred_pt = ptAny; + auto get_preferred_printer_technology = [enabled_vendors, enabled_vendors_old, suppress_sla_printer](const std::string& bundle_name, const Bundle& bundle) { + const auto config = enabled_vendors.find(bundle_name); + PrinterTechnology pt = ptAny; + if (config != enabled_vendors.end()) { + for (const auto& model : bundle.vendor_profile->models) { + if (const auto model_it = config->second.find(model.id); + model_it != config->second.end() && model_it->second.size() > 0) { + pt = model.technology; + const auto config_old = enabled_vendors_old.find(bundle_name); + if (config_old == enabled_vendors_old.end() || config_old->second.find(model.id) == config_old->second.end()) { + // if preferred printer model has SLA printer technology it's important to check the model for multi-part state + if (pt == ptSLA && suppress_sla_printer) + continue; + return pt; + } + + if (const auto model_it_old = config_old->second.find(model.id); + model_it_old == config_old->second.end() || model_it_old->second != model_it->second) { + // if preferred printer model has SLA printer technology it's important to check the model for multi-part state + if (pt == ptSLA && suppress_sla_printer) + continue; + return pt; + } + } + } + } + return pt; + }; + // Prusa printers are considered first, then 3rd party. + if (preferred_pt = get_preferred_printer_technology("PrusaResearch", bundles.prusa_bundle()); + preferred_pt == ptAny || (preferred_pt == ptSLA && suppress_sla_printer)) { + for (const auto& bundle : bundles) { + if (bundle.second.is_prusa_bundle) { continue; } + if (PrinterTechnology pt = get_preferred_printer_technology(bundle.first, bundle.second); pt == ptAny) + continue; + else if (preferred_pt == ptAny) + preferred_pt = pt; + if(!(preferred_pt == ptAny || (preferred_pt == ptSLA && suppress_sla_printer))) + break; + } + } + + if (preferred_pt == ptSLA && !wxGetApp().may_switch_to_SLA_preset(caption)) + return false; + + bool check_unsaved_preset_changes = page_welcome->reset_user_profile(); + if (check_unsaved_preset_changes) + header = _L("All user presets will be deleted."); + int act_btns = UnsavedChangesDialog::ActionButtons::KEEP; + if (!check_unsaved_preset_changes) + act_btns |= UnsavedChangesDialog::ActionButtons::SAVE; + + // Install bundles from resources if needed: + std::vector install_bundles; + for (const auto &pair : bundles) { + if (! pair.second.is_in_resources) { continue; } + + if (pair.second.is_prusa_bundle) { + // Always install Prusa bundle, because it has a lot of filaments/materials + // likely to be referenced by other profiles. + install_bundles.emplace_back(pair.first); + continue; + } + + const auto vendor = enabled_vendors.find(pair.first); + if (vendor == enabled_vendors.end()) { continue; } + + size_t size_sum = 0; + for (const auto &model : vendor->second) { size_sum += model.second.size(); } + + if (size_sum > 0) { + // This vendor needs to be installed + install_bundles.emplace_back(pair.first); + } + } + if (!check_unsaved_preset_changes) + if ((check_unsaved_preset_changes = install_bundles.size() > 0)) + header = _L_PLURAL("A new vendor was installed and one of its printers will be activated", "New vendors were installed and one of theirs printers will be activated", install_bundles.size()); + +#ifdef __linux__ + // Desktop integration on Linux + if (page_welcome->integrate_desktop()) + DesktopIntegrationDialog::perform_desktop_integration(); +#endif + + // Decide whether to create snapshot based on run_reason and the reset profile checkbox + bool snapshot = true; + Snapshot::Reason snapshot_reason = Snapshot::SNAPSHOT_UPGRADE; + switch (run_reason) { + case ConfigWizard::RR_DATA_EMPTY: + snapshot = false; + break; + case ConfigWizard::RR_DATA_LEGACY: + snapshot = true; + break; + case ConfigWizard::RR_DATA_INCOMPAT: + // In this case snapshot has already been taken by + // PresetUpdater with the appropriate reason + snapshot = false; + break; + case ConfigWizard::RR_USER: + snapshot = page_welcome->reset_user_profile(); + snapshot_reason = Snapshot::SNAPSHOT_USER; + break; + } + + if (snapshot && ! take_config_snapshot_cancel_on_error(*app_config, snapshot_reason, "", _u8L("Do you want to continue changing the configuration?"))) + return false; + + if (check_unsaved_preset_changes && + !wxGetApp().check_and_keep_current_preset_changes(caption, header, act_btns, &apply_keeped_changes)) + return false; + + if (install_bundles.size() > 0) { + // Install bundles from resources. + // Don't create snapshot - we've already done that above if applicable. + if (! updater->install_bundles_rsrc(std::move(install_bundles), false)) + return false; + } else { + BOOST_LOG_TRIVIAL(info) << "No bundles need to be installed from resources"; + } + + if (page_welcome->reset_user_profile()) { + BOOST_LOG_TRIVIAL(info) << "Resetting user profiles..."; + preset_bundle->reset(true); + } + + std::string preferred_model; + std::string preferred_variant; + auto get_preferred_printer_model = [enabled_vendors, enabled_vendors_old, preferred_pt](const std::string& bundle_name, const Bundle& bundle, std::string& variant) { + const auto config = enabled_vendors.find(bundle_name); + if (config == enabled_vendors.end()) + return std::string(); + for (const auto& model : bundle.vendor_profile->models) { + if (const auto model_it = config->second.find(model.id); + model_it != config->second.end() && model_it->second.size() > 0 && + preferred_pt == model.technology) { + variant = *model_it->second.begin(); + const auto config_old = enabled_vendors_old.find(bundle_name); + if (config_old == enabled_vendors_old.end()) + return model.id; + const auto model_it_old = config_old->second.find(model.id); + if (model_it_old == config_old->second.end()) + return model.id; + else if (model_it_old->second != model_it->second) { + for (const auto& var : model_it->second) + if (model_it_old->second.find(var) == model_it_old->second.end()) { + variant = var; + return model.id; + } + } + } + } + if (!variant.empty()) + variant.clear(); + return std::string(); + }; + // Prusa printers are considered first, then 3rd party. + if (preferred_model = get_preferred_printer_model("PrusaResearch", bundles.prusa_bundle(), preferred_variant); + preferred_model.empty()) { + for (const auto& bundle : bundles) { + if (bundle.second.is_prusa_bundle) { continue; } + if (preferred_model = get_preferred_printer_model(bundle.first, bundle.second, preferred_variant); + !preferred_model.empty()) + break; + } + } + + // if unsaved changes was not cheched till this moment + if (!check_unsaved_preset_changes) { + if ((check_unsaved_preset_changes = !preferred_model.empty())) { + header = _L("A new Printer was installed and it will be activated."); + if (!wxGetApp().check_and_keep_current_preset_changes(caption, header, act_btns, &apply_keeped_changes)) + return false; + } + else if ((check_unsaved_preset_changes = enabled_vendors_old != enabled_vendors)) { + header = _L("Some Printers were uninstalled."); + if (!wxGetApp().check_and_keep_current_preset_changes(caption, header, act_btns, &apply_keeped_changes)) + return false; + } + } + + std::string first_added_filament, first_added_sla_material; + auto get_first_added_material_preset = [this, app_config](const std::string& section_name, std::string& first_added_preset) { + if (appconfig_new.has_section(section_name)) { + // get first of new added preset names + const std::map& old_presets = app_config->has_section(section_name) ? app_config->get_section(section_name) : std::map(); + first_added_preset = get_first_added_preset(old_presets, appconfig_new.get_section(section_name)); + } + }; + get_first_added_material_preset(AppConfig::SECTION_FILAMENTS, first_added_filament); + get_first_added_material_preset(AppConfig::SECTION_MATERIALS, first_added_sla_material); + + // if unsaved changes was not cheched till this moment + if (!check_unsaved_preset_changes) { + if ((check_unsaved_preset_changes = !first_added_filament.empty() || !first_added_sla_material.empty())) { + header = !first_added_filament.empty() ? + _L("A new filament was installed and it will be activated.") : + _L("A new SLA material was installed and it will be activated."); + if (!wxGetApp().check_and_keep_current_preset_changes(caption, header, act_btns, &apply_keeped_changes)) + return false; + } + else { + auto changed = [app_config, &appconfig_new = std::as_const(this->appconfig_new)](const std::string& section_name) { + return (app_config->has_section(section_name) ? app_config->get_section(section_name) : std::map()) != appconfig_new.get_section(section_name); + }; + bool is_filaments_changed = changed(AppConfig::SECTION_FILAMENTS); + bool is_sla_materials_changed = changed(AppConfig::SECTION_MATERIALS); + if ((check_unsaved_preset_changes = is_filaments_changed || is_sla_materials_changed)) { + header = is_filaments_changed ? _L("Some filaments were uninstalled.") : _L("Some SLA materials were uninstalled."); + if (!wxGetApp().check_and_keep_current_preset_changes(caption, header, act_btns, &apply_keeped_changes)) + return false; + } + } + } + + // apply materials in app_config + for (const std::string& section_name : {AppConfig::SECTION_FILAMENTS, AppConfig::SECTION_MATERIALS}) + app_config->set_section(section_name, appconfig_new.get_section(section_name)); + + app_config->set_vendors(appconfig_new); + + app_config->set("notify_release", page_update->version_check ? "all" : "none"); + app_config->set("preset_update", page_update->preset_update ? "1" : "0"); + app_config->set("export_sources_full_pathnames", page_reload_from_disk->full_pathnames ? "1" : "0"); + +#ifdef _WIN32 + app_config->set("associate_3mf", page_files_association->associate_3mf() ? "1" : "0"); + app_config->set("associate_stl", page_files_association->associate_stl() ? "1" : "0"); +// app_config->set("associate_gcode", page_files_association->associate_gcode() ? "1" : "0"); + + if (wxGetApp().is_editor()) { + if (page_files_association->associate_3mf()) + wxGetApp().associate_3mf_files(); + if (page_files_association->associate_stl()) + wxGetApp().associate_stl_files(); + } +// else { +// if (page_files_association->associate_gcode()) +// wxGetApp().associate_gcode_files(); +// } +#endif // _WIN32 + + page_mode->serialize_mode(app_config); + + if (check_unsaved_preset_changes) + preset_bundle->load_presets(*app_config, ForwardCompatibilitySubstitutionRule::EnableSilentDisableSystem, + {preferred_model, preferred_variant, first_added_filament, first_added_sla_material}); + + if (!only_sla_mode && page_custom->custom_wanted()) { + // if unsaved changes was not cheched till this moment + if (!check_unsaved_preset_changes && + !wxGetApp().check_and_keep_current_preset_changes(caption, _L("Custom printer was installed and it will be activated."), act_btns, &apply_keeped_changes)) + return false; + + page_firmware->apply_custom_config(*custom_config); + page_bed->apply_custom_config(*custom_config); + page_diams->apply_custom_config(*custom_config); + page_temps->apply_custom_config(*custom_config); + +#if ENABLE_COPY_CUSTOM_BED_MODEL_AND_TEXTURE + copy_bed_model_and_texture_if_needed(*custom_config); +#endif // ENABLE_COPY_CUSTOM_BED_MODEL_AND_TEXTURE + + const std::string profile_name = page_custom->profile_name(); + preset_bundle->load_config_from_wizard(profile_name, *custom_config); + } + + // Update the selections from the compatibilty. + preset_bundle->export_selections(*app_config); + + return true; +} +void ConfigWizard::priv::update_presets_in_config(const std::string& section, const std::string& alias_key, bool add) +{ + const PresetAliases& aliases = section == AppConfig::SECTION_FILAMENTS ? aliases_fff : aliases_sla; + + auto update = [this, add](const std::string& s, const std::string& key) { + assert(! s.empty()); + if (add) + appconfig_new.set(s, key, "1"); + else + appconfig_new.erase(s, key); + }; + + // add or delete presets had a same alias + auto it = aliases.find(alias_key); + if (it != aliases.end()) + for (const std::string& name : it->second) + update(section, name); +} + +bool ConfigWizard::priv::check_fff_selected() +{ + bool ret = page_fff->any_selected(); + for (const auto& printer: pages_3rdparty) + if (printer.second.first) // FFF page + ret |= printer.second.first->any_selected(); + return ret; +} + +bool ConfigWizard::priv::check_sla_selected() +{ + bool ret = page_msla->any_selected(); + for (const auto& printer: pages_3rdparty) + if (printer.second.second) // SLA page + ret |= printer.second.second->any_selected(); + return ret; +} + + +// Public + +ConfigWizard::ConfigWizard(wxWindow *parent) + : DPIDialog(parent, wxID_ANY, wxString(SLIC3R_APP_NAME) + " - " + _(name()), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) + , p(new priv(this)) +{ + this->SetFont(wxGetApp().normal_font()); + + p->load_vendors(); + p->custom_config.reset(DynamicPrintConfig::new_from_defaults_keys({ + "gcode_flavor", "bed_shape", "bed_custom_texture", "bed_custom_model", "nozzle_diameter", "filament_diameter", "temperature", "bed_temperature", + })); + + p->index = new ConfigWizardIndex(this); + + auto *vsizer = new wxBoxSizer(wxVERTICAL); + auto *topsizer = new wxBoxSizer(wxHORIZONTAL); + auto* hline = new StaticLine(this); + p->btnsizer = new wxBoxSizer(wxHORIZONTAL); + + // Initially we _do not_ SetScrollRate in order to figure out the overall width of the Wizard without scrolling. + // Later, we compare that to the size of the current screen and set minimum width based on that (see below). + p->hscroll = new wxScrolledWindow(this); + p->hscroll_sizer = new wxBoxSizer(wxHORIZONTAL); + p->hscroll->SetSizer(p->hscroll_sizer); + + topsizer->Add(p->index, 0, wxEXPAND); + topsizer->AddSpacer(INDEX_MARGIN); + topsizer->Add(p->hscroll, 1, wxEXPAND); + + p->btn_sel_all = new wxButton(this, wxID_ANY, _L("Select all standard printers")); + p->btnsizer->Add(p->btn_sel_all); + + p->btn_prev = new wxButton(this, wxID_ANY, _L("< &Back")); + p->btn_next = new wxButton(this, wxID_ANY, _L("&Next >")); + p->btn_finish = new wxButton(this, wxID_APPLY, _L("&Finish")); + p->btn_cancel = new wxButton(this, wxID_CANCEL, _L("Cancel")); // Note: The label needs to be present, otherwise we get accelerator bugs on Mac + p->btnsizer->AddStretchSpacer(); + p->btnsizer->Add(p->btn_prev, 0, wxLEFT, BTN_SPACING); + p->btnsizer->Add(p->btn_next, 0, wxLEFT, BTN_SPACING); + p->btnsizer->Add(p->btn_finish, 0, wxLEFT, BTN_SPACING); + p->btnsizer->Add(p->btn_cancel, 0, wxLEFT, BTN_SPACING); + + wxGetApp().UpdateDarkUI(p->btn_sel_all); + wxGetApp().UpdateDarkUI(p->btn_prev); + wxGetApp().UpdateDarkUI(p->btn_next); + wxGetApp().UpdateDarkUI(p->btn_finish); + wxGetApp().UpdateDarkUI(p->btn_cancel); + + const auto prusa_it = p->bundles.find("PrusaResearch"); + wxCHECK_RET(prusa_it != p->bundles.cend(), "Vendor PrusaResearch not found"); + const VendorProfile *vendor_prusa = prusa_it->second.vendor_profile; + + p->add_page(p->page_welcome = new PageWelcome(this)); + + + p->page_fff = new PagePrinters(this, _L("Prusa FFF Technology Printers"), "Prusa FFF", *vendor_prusa, 0, T_FFF); + p->only_sla_mode = !p->page_fff->has_printers; + if (!p->only_sla_mode) { + p->add_page(p->page_fff); + p->page_fff->is_primary_printer_page = true; + } + + + p->page_msla = new PagePrinters(this, _L("Prusa MSLA Technology Printers"), "Prusa MSLA", *vendor_prusa, 0, T_SLA); + p->add_page(p->page_msla); + if (p->only_sla_mode) { + p->page_msla->is_primary_printer_page = true; + } + + if (!p->only_sla_mode) { + // Pages for 3rd party vendors + p->create_3rdparty_pages(); // Needs to be done _before_ creating PageVendors + p->add_page(p->page_vendors = new PageVendors(this)); + p->add_page(p->page_custom = new PageCustom(this)); + p->custom_printer_selected = p->page_custom->custom_wanted(); + } + + p->any_sla_selected = p->check_sla_selected(); + p->any_fff_selected = ! p->only_sla_mode && p->check_fff_selected(); + + p->update_materials(T_ANY); + if (!p->only_sla_mode) + p->add_page(p->page_filaments = new PageMaterials(this, &p->filaments, + _L("Filament Profiles Selection"), _L("Filaments"), _L("Type:") )); + + p->add_page(p->page_sla_materials = new PageMaterials(this, &p->sla_materials, + _L("SLA Material Profiles Selection") + " ", _L("SLA Materials"), _L("Type:") )); + + + p->add_page(p->page_update = new PageUpdate(this)); + p->add_page(p->page_reload_from_disk = new PageReloadFromDisk(this)); +#ifdef _WIN32 + p->add_page(p->page_files_association = new PageFilesAssociation(this)); +#endif // _WIN32 + p->add_page(p->page_mode = new PageMode(this)); + p->add_page(p->page_firmware = new PageFirmware(this)); + p->add_page(p->page_bed = new PageBedShape(this)); + p->add_page(p->page_diams = new PageDiameters(this)); + p->add_page(p->page_temps = new PageTemperatures(this)); + + p->load_pages(); + p->index->go_to(size_t{0}); + + vsizer->Add(topsizer, 1, wxEXPAND | wxALL, DIALOG_MARGIN); + vsizer->Add(hline, 0, wxEXPAND | wxLEFT | wxRIGHT, VERTICAL_SPACING); + vsizer->Add(p->btnsizer, 0, wxEXPAND | wxALL, DIALOG_MARGIN); + + SetSizer(vsizer); + SetSizerAndFit(vsizer); + + // We can now enable scrolling on hscroll + p->hscroll->SetScrollRate(30, 30); + + on_window_geometry(this, [this]() { + p->init_dialog_size(); + }); + + p->btn_prev->Bind(wxEVT_BUTTON, [this](const wxCommandEvent &) { this->p->index->go_prev(); }); + + p->btn_next->Bind(wxEVT_BUTTON, [this](const wxCommandEvent &) + { + // check, that there is selected at least one filament/material + ConfigWizardPage* active_page = this->p->index->active_page(); + if (// Leaving the filaments or SLA materials page and + (active_page == p->page_filaments || active_page == p->page_sla_materials) && + // some Printer models had no filament or SLA material selected. + ! p->check_and_install_missing_materials(dynamic_cast(active_page)->materials->technology)) + // In that case don't leave the page and the function above queried the user whether to install default materials. + return; + this->p->index->go_next(); + }); + + p->btn_finish->Bind(wxEVT_BUTTON, [this](const wxCommandEvent &) + { + if (p->on_bnt_finish()) + this->EndModal(wxID_OK); + }); + + p->btn_sel_all->Bind(wxEVT_BUTTON, [this](const wxCommandEvent &) { + p->any_sla_selected = true; + p->load_pages(); + p->page_fff->select_all(true, false); + p->page_msla->select_all(true, false); + p->index->go_to(p->page_mode); + }); + + p->index->Bind(EVT_INDEX_PAGE, [this](const wxCommandEvent &) { + const bool is_last = p->index->active_is_last(); + p->btn_next->Show(! is_last); + if (is_last) + p->btn_finish->SetFocus(); + + Layout(); + }); + + if (wxLinux_gtk3) + this->Bind(wxEVT_SHOW, [this, vsizer](const wxShowEvent& e) { + ConfigWizardPage* active_page = p->index->active_page(); + if (!active_page) + return; + for (auto page : p->all_pages) + if (page != active_page) + page->Hide(); + // update best size for the dialog after hiding of the non-active pages + vsizer->SetSizeHints(this); + // set initial dialog size + p->init_dialog_size(); + }); +} + +ConfigWizard::~ConfigWizard() {} + +bool ConfigWizard::run(RunReason reason, StartPage start_page) +{ + BOOST_LOG_TRIVIAL(info) << boost::format("Running ConfigWizard, reason: %1%, start_page: %2%") % reason % start_page; + + GUI_App &app = wxGetApp(); + + p->set_run_reason(reason); + p->set_start_page(start_page); + + if (ShowModal() == wxID_OK) { + bool apply_keeped_changes = false; + if (! p->apply_config(app.app_config, app.preset_bundle, app.preset_updater, apply_keeped_changes)) + return false; + + if (apply_keeped_changes) + app.apply_keeped_preset_modifications(); + + app.app_config->set_legacy_datadir(false); + app.update_mode(); + app.obj_manipul()->update_ui_from_settings(); + BOOST_LOG_TRIVIAL(info) << "ConfigWizard applied"; + return true; + } else { + BOOST_LOG_TRIVIAL(info) << "ConfigWizard cancelled"; + return false; + } +} + +const wxString& ConfigWizard::name(const bool from_menu/* = false*/) +{ + // A different naming convention is used for the Wizard on Windows & GTK vs. OSX. + // Note: Don't call _() macro here. + // This function just return the current name according to the OS. + // Translation is implemented inside GUI_App::add_config_menu() +#if __APPLE__ + static const wxString config_wizard_name = L("Configuration Assistant"); + static const wxString config_wizard_name_menu = L("Configuration &Assistant"); +#else + static const wxString config_wizard_name = L("Configuration Wizard"); + static const wxString config_wizard_name_menu = L("Configuration &Wizard"); +#endif + return from_menu ? config_wizard_name_menu : config_wizard_name; +} + +void ConfigWizard::on_dpi_changed(const wxRect &suggested_rect) +{ + p->index->msw_rescale(); + + const int em = em_unit(); + + msw_buttons_rescale(this, em, { wxID_APPLY, + wxID_CANCEL, + p->btn_sel_all->GetId(), + p->btn_next->GetId(), + p->btn_prev->GetId() }); + + for (auto printer_picker: p->page_fff->printer_pickers) + msw_buttons_rescale(this, em, printer_picker->get_button_indexes()); + + p->init_dialog_size(); + + Refresh(); +} + +void ConfigWizard::on_sys_color_changed() +{ + wxGetApp().UpdateDlgDarkUI(this); + Refresh(); +} + +} +} diff --git a/src/slic3r/GUI/ConfigWizard_private.hpp b/src/slic3r/GUI/ConfigWizard_private.hpp index 4de8381ff..aa074f925 100644 --- a/src/slic3r/GUI/ConfigWizard_private.hpp +++ b/src/slic3r/GUI/ConfigWizard_private.hpp @@ -519,7 +519,7 @@ private: ssize_t item_hover; size_t last_page; - int item_height() const { return std::max(bullet_black.bmp().GetSize().GetHeight(), em_w) + em_w; } + int item_height() const { return std::max(bullet_black.GetHeight(), em_w) + em_w; } void on_paint(wxPaintEvent &evt); void on_mouse_move(wxMouseEvent &evt); diff --git a/src/slic3r/GUI/CoordAxes.cpp b/src/slic3r/GUI/CoordAxes.cpp new file mode 100644 index 000000000..edd1d4f03 --- /dev/null +++ b/src/slic3r/GUI/CoordAxes.cpp @@ -0,0 +1,104 @@ +#include "libslic3r/libslic3r.h" + +#include "CoordAxes.hpp" +#include "GUI_App.hpp" +#include "3DScene.hpp" +#if ENABLE_GL_SHADERS_ATTRIBUTES +#include "Plater.hpp" +#include "Camera.hpp" +#endif // ENABLE_GL_SHADERS_ATTRIBUTES + +#include + +#if ENABLE_WORLD_COORDINATE + +namespace Slic3r { +namespace GUI { + +const float CoordAxes::DefaultStemRadius = 0.5f; +const float CoordAxes::DefaultStemLength = 25.0f; +const float CoordAxes::DefaultTipRadius = 2.5f * CoordAxes::DefaultStemRadius; +const float CoordAxes::DefaultTipLength = 5.0f; + +#if ENABLE_GL_SHADERS_ATTRIBUTES +void CoordAxes::render(const Transform3d& trafo, float emission_factor) +#else +void CoordAxes::render(float emission_factor) +#endif // ENABLE_GL_SHADERS_ATTRIBUTES +{ +#if ENABLE_GL_SHADERS_ATTRIBUTES + auto render_axis = [this](GLShaderProgram& shader, const Transform3d& transform) { + const Camera& camera = wxGetApp().plater()->get_camera(); + const Transform3d matrix = camera.get_view_matrix() * transform; + shader.set_uniform("view_model_matrix", matrix); + shader.set_uniform("projection_matrix", camera.get_projection_matrix()); + shader.set_uniform("normal_matrix", (Matrix3d)matrix.matrix().block(0, 0, 3, 3).inverse().transpose()); + m_arrow.render(); +#else + auto render_axis = [this](const Transform3f& transform) { + glsafe(::glPushMatrix()); + glsafe(::glMultMatrixf(transform.data())); + m_arrow.render(); + glsafe(::glPopMatrix()); +#endif // ENABLE_GL_SHADERS_ATTRIBUTES + }; + + if (!m_arrow.is_initialized()) + m_arrow.init_from(stilized_arrow(16, m_tip_radius, m_tip_length, m_stem_radius, m_stem_length)); + + GLShaderProgram* curr_shader = wxGetApp().get_current_shader(); + GLShaderProgram* shader = wxGetApp().get_shader("gouraud_light"); + if (shader == nullptr) + return; + + if (curr_shader != nullptr) + curr_shader->stop_using(); + + shader->start_using(); + shader->set_uniform("emission_factor", emission_factor); + + // x axis +#if ENABLE_LEGACY_OPENGL_REMOVAL + m_arrow.set_color(ColorRGBA::X()); +#else + m_arrow.set_color(-1, ColorRGBA::X()); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL +#if ENABLE_GL_SHADERS_ATTRIBUTES + render_axis(*shader, trafo * Geometry::assemble_transform(m_origin, { 0.0, 0.5 * M_PI, 0.0 })); +#else + render_axis(Geometry::assemble_transform(m_origin, { 0.0, 0.5 * M_PI, 0.0 }).cast()); +#endif // ENABLE_GL_SHADERS_ATTRIBUTES + + // y axis +#if ENABLE_LEGACY_OPENGL_REMOVAL + m_arrow.set_color(ColorRGBA::Y()); +#else + m_arrow.set_color(-1, ColorRGBA::Y()); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL +#if ENABLE_GL_SHADERS_ATTRIBUTES + render_axis(*shader, trafo * Geometry::assemble_transform(m_origin, { -0.5 * M_PI, 0.0, 0.0 })); +#else + render_axis(Geometry::assemble_transform(m_origin, { -0.5 * M_PI, 0.0, 0.0 }).cast()); +#endif // ENABLE_GL_SHADERS_ATTRIBUTES + + // z axis +#if ENABLE_LEGACY_OPENGL_REMOVAL + m_arrow.set_color(ColorRGBA::Z()); +#else + m_arrow.set_color(-1, ColorRGBA::Z()); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL +#if ENABLE_GL_SHADERS_ATTRIBUTES + render_axis(*shader, trafo * Geometry::assemble_transform(m_origin)); +#else + render_axis(Geometry::assemble_transform(m_origin).cast()); +#endif // ENABLE_GL_SHADERS_ATTRIBUTES + + shader->stop_using(); + if (curr_shader != nullptr) + curr_shader->start_using(); +} + +} // GUI +} // Slic3r + +#endif // ENABLE_WORLD_COORDINATE diff --git a/src/slic3r/GUI/CoordAxes.hpp b/src/slic3r/GUI/CoordAxes.hpp new file mode 100644 index 000000000..9e49d1391 --- /dev/null +++ b/src/slic3r/GUI/CoordAxes.hpp @@ -0,0 +1,64 @@ +#ifndef slic3r_CoordAxes_hpp_ +#define slic3r_CoordAxes_hpp_ + +#if ENABLE_WORLD_COORDINATE +#include "GLModel.hpp" + +namespace Slic3r { +namespace GUI { + +class CoordAxes +{ +public: + static const float DefaultStemRadius; + static const float DefaultStemLength; + static const float DefaultTipRadius; + static const float DefaultTipLength; + +private: + Vec3d m_origin{ Vec3d::Zero() }; + float m_stem_radius{ DefaultStemRadius }; + float m_stem_length{ DefaultStemLength }; + float m_tip_radius{ DefaultTipRadius }; + float m_tip_length{ DefaultTipLength }; + GLModel m_arrow; + +public: + const Vec3d& get_origin() const { return m_origin; } + void set_origin(const Vec3d& origin) { m_origin = origin; } + void set_stem_radius(float radius) { + m_stem_radius = radius; + m_arrow.reset(); + } + void set_stem_length(float length) { + m_stem_length = length; + m_arrow.reset(); + } + void set_tip_radius(float radius) { + m_tip_radius = radius; + m_arrow.reset(); + } + void set_tip_length(float length) { + m_tip_length = length; + m_arrow.reset(); + } + + float get_stem_radius() const { return m_stem_radius; } + float get_stem_length() const { return m_stem_length; } + float get_tip_radius() const { return m_tip_radius; } + float get_tip_length() const { return m_tip_length; } + float get_total_length() const { return m_stem_length + m_tip_length; } + +#if ENABLE_GL_SHADERS_ATTRIBUTES + void render(const Transform3d& trafo, float emission_factor = 0.0f); +#else + void render(float emission_factor = 0.0f); +#endif // ENABLE_GL_SHADERS_ATTRIBUTES +}; + +} // GUI +} // Slic3r + +#endif // ENABLE_WORLD_COORDINATE + +#endif // slic3r_CoordAxes_hpp_ diff --git a/src/slic3r/GUI/DoubleSlider.cpp b/src/slic3r/GUI/DoubleSlider.cpp index dda50ec05..717af39ba 100644 --- a/src/slic3r/GUI/DoubleSlider.cpp +++ b/src/slic3r/GUI/DoubleSlider.cpp @@ -86,24 +86,24 @@ Control::Control( wxWindow *parent, m_bmp_thumb_higher = (style == wxSL_HORIZONTAL ? ScalableBitmap(this, "thumb_right") : ScalableBitmap(this, "thumb_up")); m_bmp_thumb_lower = (style == wxSL_HORIZONTAL ? ScalableBitmap(this, "thumb_left") : ScalableBitmap(this, "thumb_down")); - m_thumb_size = m_bmp_thumb_lower.GetBmpSize(); + m_thumb_size = m_bmp_thumb_lower.GetSize(); m_bmp_add_tick_on = ScalableBitmap(this, "colorchange_add"); m_bmp_add_tick_off = ScalableBitmap(this, "colorchange_add_f"); m_bmp_del_tick_on = ScalableBitmap(this, "colorchange_del"); m_bmp_del_tick_off = ScalableBitmap(this, "colorchange_del_f"); - m_tick_icon_dim = m_bmp_add_tick_on.GetBmpWidth(); + m_tick_icon_dim = m_bmp_add_tick_on.GetWidth(); m_bmp_one_layer_lock_on = ScalableBitmap(this, "lock_closed"); m_bmp_one_layer_lock_off = ScalableBitmap(this, "lock_closed_f"); m_bmp_one_layer_unlock_on = ScalableBitmap(this, "lock_open"); m_bmp_one_layer_unlock_off = ScalableBitmap(this, "lock_open_f"); - m_lock_icon_dim = m_bmp_one_layer_lock_on.GetBmpWidth(); + m_lock_icon_dim = m_bmp_one_layer_lock_on.GetWidth(); m_bmp_revert = ScalableBitmap(this, "undo"); - m_revert_icon_dim = m_bmp_revert.GetBmpWidth(); + m_revert_icon_dim = m_bmp_revert.GetWidth(); m_bmp_cog = ScalableBitmap(this, "cog"); - m_cog_icon_dim = m_bmp_cog.GetBmpWidth(); + m_cog_icon_dim = m_bmp_cog.GetWidth(); m_selection = ssUndef; m_ticks.set_pause_print_msg(_utf8(L("Place bearings in slots and resume printing"))); @@ -155,26 +155,11 @@ void Control::msw_rescale() { m_font = GUI::wxGetApp().normal_font(); - m_bmp_thumb_higher.msw_rescale(); - m_bmp_thumb_lower .msw_rescale(); - m_thumb_size = m_bmp_thumb_lower.bmp().GetSize(); - - m_bmp_add_tick_on .msw_rescale(); - m_bmp_add_tick_off.msw_rescale(); - m_bmp_del_tick_on .msw_rescale(); - m_bmp_del_tick_off.msw_rescale(); - m_tick_icon_dim = m_bmp_add_tick_on.bmp().GetSize().x; - - m_bmp_one_layer_lock_on .msw_rescale(); - m_bmp_one_layer_lock_off .msw_rescale(); - m_bmp_one_layer_unlock_on .msw_rescale(); - m_bmp_one_layer_unlock_off.msw_rescale(); - m_lock_icon_dim = m_bmp_one_layer_lock_on.bmp().GetSize().x; - - m_bmp_revert.msw_rescale(); - m_revert_icon_dim = m_bmp_revert.bmp().GetSize().x; - m_bmp_cog.msw_rescale(); - m_cog_icon_dim = m_bmp_cog.bmp().GetSize().x; + m_thumb_size = m_bmp_thumb_lower.GetSize(); + m_tick_icon_dim = m_bmp_add_tick_on.GetWidth(); + m_lock_icon_dim = m_bmp_one_layer_lock_on.GetWidth(); + m_revert_icon_dim = m_bmp_revert.GetWidth(); + m_cog_icon_dim = m_bmp_cog.GetWidth(); SLIDER_MARGIN = 4 + GUI::wxGetApp().em_unit(); @@ -189,22 +174,18 @@ void Control::sys_color_changed() { GUI::wxGetApp().UpdateDarkUI(GetParent()); - m_bmp_add_tick_on .msw_rescale(); - m_bmp_add_tick_off.msw_rescale(); - m_bmp_del_tick_on .msw_rescale(); - m_bmp_del_tick_off.msw_rescale(); - m_tick_icon_dim = m_bmp_add_tick_on.GetBmpWidth(); + m_bmp_add_tick_on .sys_color_changed(); + m_bmp_add_tick_off.sys_color_changed(); + m_bmp_del_tick_on .sys_color_changed(); + m_bmp_del_tick_off.sys_color_changed(); - m_bmp_one_layer_lock_on .msw_rescale(); - m_bmp_one_layer_lock_off .msw_rescale(); - m_bmp_one_layer_unlock_on .msw_rescale(); - m_bmp_one_layer_unlock_off.msw_rescale(); - m_lock_icon_dim = m_bmp_one_layer_lock_on.GetBmpWidth(); + m_bmp_one_layer_lock_on .sys_color_changed(); + m_bmp_one_layer_lock_off .sys_color_changed(); + m_bmp_one_layer_unlock_on .sys_color_changed(); + m_bmp_one_layer_unlock_off.sys_color_changed(); - m_bmp_revert.msw_rescale(); - m_revert_icon_dim = m_bmp_revert.GetBmpWidth(); - m_bmp_cog.msw_rescale(); - m_cog_icon_dim = m_bmp_cog.GetBmpWidth(); + m_bmp_revert.sys_color_changed(); + m_bmp_cog .sys_color_changed(); } int Control::GetActiveValue() const @@ -604,9 +585,12 @@ void Control::draw_action_icon(wxDC& dc, const wxPoint pt_beg, const wxPoint pt_ return; } - wxBitmap* icon = m_focus == fiActionIcon ? &m_bmp_add_tick_off.bmp() : &m_bmp_add_tick_on.bmp(); + //wxBitmap* icon = m_focus == fiActionIcon ? &m_bmp_add_tick_off.bmp() : &m_bmp_add_tick_on.bmp(); + //if (m_ticks.ticks.find(TickCode{tick}) != m_ticks.ticks.end()) + // icon = m_focus == fiActionIcon ? &m_bmp_del_tick_off.bmp() : &m_bmp_del_tick_on.bmp(); + ScalableBitmap* icon = m_focus == fiActionIcon ? &m_bmp_add_tick_off : &m_bmp_add_tick_on; if (m_ticks.ticks.find(TickCode{tick}) != m_ticks.ticks.end()) - icon = m_focus == fiActionIcon ? &m_bmp_del_tick_off.bmp() : &m_bmp_del_tick_on.bmp(); + icon = m_focus == fiActionIcon ? &m_bmp_del_tick_off : &m_bmp_del_tick_on; wxCoord x_draw, y_draw; is_horizontal() ? x_draw = pt_beg.x - 0.5*m_tick_icon_dim : y_draw = pt_beg.y - 0.5*m_tick_icon_dim; @@ -615,10 +599,12 @@ void Control::draw_action_icon(wxDC& dc, const wxPoint pt_beg, const wxPoint pt_ else is_horizontal() ? y_draw = pt_beg.y - m_tick_icon_dim-2 : x_draw = pt_end.x + 3; - if (m_draw_mode == dmSequentialFffPrint) - dc.DrawBitmap(create_scaled_bitmap("colorchange_add", nullptr, 16, true), x_draw, y_draw); + if (m_draw_mode == dmSequentialFffPrint) { + wxBitmap disabled_add = get_bmp_bundle("colorchange_add")->GetBitmapFor(this).ConvertToDisabled(); + dc.DrawBitmap(disabled_add, x_draw, y_draw); + } else - dc.DrawBitmap(*icon, x_draw, y_draw); + dc.DrawBitmap((*icon).get_bitmap(), x_draw, y_draw); //update rect of the tick action icon m_rect_tick_action = wxRect(x_draw, y_draw, m_tick_icon_dim, m_tick_icon_dim); @@ -851,7 +837,7 @@ void Control::draw_thumb_item(wxDC& dc, const wxPoint& pos, const SelectedSlider { wxCoord x_draw = pos.x - int(0.5 * m_thumb_size.x); wxCoord y_draw = pos.y - int(0.5 * m_thumb_size.y); - dc.DrawBitmap(selection == ssLower ? m_bmp_thumb_lower.bmp() : m_bmp_thumb_higher.bmp(), x_draw, y_draw); + dc.DrawBitmap(selection == ssLower ? m_bmp_thumb_lower.get_bitmap() : m_bmp_thumb_higher.get_bitmap(), x_draw, y_draw); // Update thumb rect update_thumb_rect(x_draw, y_draw, selection); @@ -945,12 +931,12 @@ void Control::draw_ticks(wxDC& dc) // Draw icon for "Pause print", "Custom Gcode" or conflict tick if (!icon_name.empty()) { - wxBitmap icon = create_scaled_bitmap(icon_name); + wxBitmapBundle* icon = get_bmp_bundle(icon_name); wxCoord x_draw, y_draw; is_horizontal() ? x_draw = pos - 0.5 * m_tick_icon_dim : y_draw = pos - 0.5 * m_tick_icon_dim; is_horizontal() ? y_draw = mid + 22 : x_draw = mid + m_thumb_size.x + 3; - dc.DrawBitmap(icon, x_draw, y_draw); + dc.DrawBitmap(icon->GetBitmapFor(this), x_draw, y_draw); } } } @@ -1262,9 +1248,12 @@ void Control::draw_one_layer_icon(wxDC& dc) if (m_draw_mode == dmSequentialGCodeView) return; - const wxBitmap& icon = m_is_one_layer ? - m_focus == fiOneLayerIcon ? m_bmp_one_layer_lock_off.bmp() : m_bmp_one_layer_lock_on.bmp() : - m_focus == fiOneLayerIcon ? m_bmp_one_layer_unlock_off.bmp() : m_bmp_one_layer_unlock_on.bmp(); + //const wxBitmap& icon = m_is_one_layer ? + // m_focus == fiOneLayerIcon ? m_bmp_one_layer_lock_off.bmp() : m_bmp_one_layer_lock_on.bmp() : + // m_focus == fiOneLayerIcon ? m_bmp_one_layer_unlock_off.bmp() : m_bmp_one_layer_unlock_on.bmp(); + const ScalableBitmap& icon = m_is_one_layer ? + m_focus == fiOneLayerIcon ? m_bmp_one_layer_lock_off : m_bmp_one_layer_lock_on : + m_focus == fiOneLayerIcon ? m_bmp_one_layer_unlock_off : m_bmp_one_layer_unlock_on; int width, height; get_size(&width, &height); @@ -1273,7 +1262,7 @@ void Control::draw_one_layer_icon(wxDC& dc) is_horizontal() ? x_draw = width-2 : x_draw = 0.5*width - 0.5*m_lock_icon_dim; is_horizontal() ? y_draw = 0.5*height - 0.5*m_lock_icon_dim : y_draw = height-2; - dc.DrawBitmap(icon, x_draw, y_draw); + dc.DrawBitmap(icon.bmp().GetBitmapFor(this), x_draw, y_draw); //update rect of the lock/unlock icon m_rect_one_layer_icon = wxRect(x_draw, y_draw, m_lock_icon_dim, m_lock_icon_dim); @@ -1291,7 +1280,7 @@ void Control::draw_revert_icon(wxDC& dc) is_horizontal() ? x_draw = width-2 : x_draw = 0.25*SLIDER_MARGIN; is_horizontal() ? y_draw = 0.25*SLIDER_MARGIN: y_draw = height-2; - dc.DrawBitmap(m_bmp_revert.bmp(), x_draw, y_draw); + dc.DrawBitmap(m_bmp_revert.get_bitmap(), x_draw, y_draw); //update rect of the lock/unlock icon m_rect_revert_icon = wxRect(x_draw, y_draw, m_revert_icon_dim, m_revert_icon_dim); @@ -1315,7 +1304,7 @@ void Control::draw_cog_icon(wxDC& dc) is_horizontal() ? y_draw = height - m_cog_icon_dim - 2 : y_draw = height - 2; } - dc.DrawBitmap(m_bmp_cog.bmp(), x_draw, y_draw); + dc.DrawBitmap(m_bmp_cog.get_bitmap(), x_draw, y_draw); //update rect of the lock/unlock icon m_rect_cog_icon = wxRect(x_draw, y_draw, m_cog_icon_dim, m_cog_icon_dim); @@ -1673,7 +1662,7 @@ void Control::append_change_extruder_menu_item(wxMenu* menu, bool switch_current if (extruders_cnt > 1) { std::array active_extruders = get_active_extruders_for_tick(m_selection == ssLower ? m_lower_value : m_higher_value); - std::vector icons = get_extruder_color_icons(true); + std::vector icons = get_extruder_color_icons(true); wxMenu* change_extruder_menu = new wxMenu(); @@ -1684,7 +1673,7 @@ void Control::append_change_extruder_menu_item(wxMenu* menu, bool switch_current if (m_mode == MultiAsSingle) append_menu_item(change_extruder_menu, wxID_ANY, item_name, "", - [this, i](wxCommandEvent&) { add_code_as_tick(ToolChange, i); }, *icons[i-1], menu, + [this, i](wxCommandEvent&) { add_code_as_tick(ToolChange, i); }, icons[i-1], menu, [is_active_extruder]() { return !is_active_extruder; }, GUI::wxGetApp().plater()); } @@ -1722,7 +1711,7 @@ void Control::append_add_color_change_menu_item(wxMenu* menu, bool switch_curren format_wxstr(_L("Switch code to Color change (%1%) for:"), gcode(ColorChange)) : format_wxstr(_L("Add color change (%1%) for:"), gcode(ColorChange)); wxMenuItem* add_color_change_menu_item = menu->AppendSubMenu(add_color_change_menu, menu_name, ""); - add_color_change_menu_item->SetBitmap(create_menu_bitmap("colorchange_add_m")); + add_color_change_menu_item->SetBitmap(*get_bmp_bundle("colorchange_add_m")); } } diff --git a/src/slic3r/GUI/ExtraRenderers.cpp b/src/slic3r/GUI/ExtraRenderers.cpp index d72e1dd32..0368a2fe2 100644 --- a/src/slic3r/GUI/ExtraRenderers.cpp +++ b/src/slic3r/GUI/ExtraRenderers.cpp @@ -33,6 +33,15 @@ wxIMPLEMENT_DYNAMIC_CLASS(DataViewBitmapText, wxObject) IMPLEMENT_VARIANT_OBJECT(DataViewBitmapText) +static wxSize get_size(const wxBitmap& icon) +{ +#ifdef __WIN32__ + return icon.GetSize(); +#else + return icon.GetScaledSize(); +#endif +} + // --------------------------------------------------------- // BitmapTextRenderer // --------------------------------------------------------- @@ -124,11 +133,7 @@ bool BitmapTextRenderer::Render(wxRect rect, wxDC *dc, int state) const wxBitmap& icon = m_value.GetBitmap(); if (icon.IsOk()) { -#ifdef __APPLE__ - wxSize icon_sz = icon.GetScaledSize(); -#else - wxSize icon_sz = icon.GetSize(); -#endif + wxSize icon_sz = get_size(icon); dc->DrawBitmap(icon, rect.x, rect.y + (rect.height - icon_sz.y) / 2); xoffset = icon_sz.x + 4; } @@ -264,11 +269,13 @@ bool BitmapChoiceRenderer::Render(wxRect rect, wxDC* dc, int state) const wxBitmap& icon = m_value.GetBitmap(); if (icon.IsOk()) { - dc->DrawBitmap(icon, rect.x, rect.y + (rect.height - icon.GetHeight()) / 2); - xoffset = icon.GetWidth() + 4; + wxSize icon_sz = get_size(icon); + + dc->DrawBitmap(icon, rect.x, rect.y + (rect.height - icon_sz.GetHeight()) / 2); + xoffset = icon_sz.GetWidth() + 4; if (rect.height==0) - rect.height= icon.GetHeight(); + rect.height= icon_sz.GetHeight(); } #ifdef _WIN32 @@ -297,7 +304,7 @@ wxWindow* BitmapChoiceRenderer::CreateEditorCtrl(wxWindow* parent, wxRect labelR if (can_create_editor_ctrl && !can_create_editor_ctrl()) return nullptr; - std::vector icons = get_extruder_color_icons(); + std::vector icons = get_extruder_color_icons(); if (icons.empty()) return nullptr; diff --git a/src/slic3r/GUI/ExtruderSequenceDialog.cpp b/src/slic3r/GUI/ExtruderSequenceDialog.cpp index 42313636e..e1c6a7ce0 100644 --- a/src/slic3r/GUI/ExtruderSequenceDialog.cpp +++ b/src/slic3r/GUI/ExtruderSequenceDialog.cpp @@ -264,9 +264,6 @@ void ExtruderSequenceDialog::on_dpi_changed(const wxRect& suggested_rect) { SetFont(wxGetApp().normal_font()); - m_bmp_add.msw_rescale(); - m_bmp_del.msw_rescale(); - const int em = em_unit(); m_intervals_grid_sizer->SetHGap(em); diff --git a/src/slic3r/GUI/Field.hpp b/src/slic3r/GUI/Field.hpp index a9812abf2..95caa8ed3 100644 --- a/src/slic3r/GUI/Field.hpp +++ b/src/slic3r/GUI/Field.hpp @@ -106,14 +106,14 @@ public: bool set_undo_to_sys_tooltip(const wxString* tip) { return m_undo_ui.set_undo_to_sys_tooltip(tip); } // ui items used for revert line value - bool has_undo_ui() const { return m_undo_ui.undo_bitmap != nullptr; } - const wxBitmap& undo_bitmap() const { return m_undo_ui.undo_bitmap->bmp(); } - const wxString* undo_tooltip() const { return m_undo_ui.undo_tooltip; } - const wxBitmap& undo_to_sys_bitmap() const { return m_undo_ui.undo_to_sys_bitmap->bmp(); } - const wxString* undo_to_sys_tooltip() const { return m_undo_ui.undo_to_sys_tooltip; } - const wxColour* label_color() const { return m_undo_ui.label_color; } - const bool blink() const { return m_undo_ui.blink; } - bool* get_blink_ptr() { return &m_undo_ui.blink; } + bool has_undo_ui() const { return m_undo_ui.undo_bitmap != nullptr; } + const wxBitmapBundle& undo_bitmap() const { return m_undo_ui.undo_bitmap->bmp(); } + const wxString* undo_tooltip() const { return m_undo_ui.undo_tooltip; } + const wxBitmapBundle& undo_to_sys_bitmap() const { return m_undo_ui.undo_to_sys_bitmap->bmp(); } + const wxString* undo_to_sys_tooltip() const { return m_undo_ui.undo_to_sys_tooltip; } + const wxColour* label_color() const { return m_undo_ui.label_color; } + const bool blink() const { return m_undo_ui.blink; } + bool* get_blink_ptr() { return &m_undo_ui.blink; } }; diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index 652410589..1e28d1287 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -386,14 +386,14 @@ void GCodeViewer::SequentialView::Marker::render() ImGui::PopStyleVar(); } -void GCodeViewer::SequentialView::GCodeWindow::load_gcode(const std::string& filename, std::vector &&lines_ends) +void GCodeViewer::SequentialView::GCodeWindow::load_gcode(const std::string& filename, const std::vector& lines_ends) { assert(! m_file.is_open()); if (m_file.is_open()) return; m_filename = filename; - m_lines_ends = std::move(lines_ends); + m_lines_ends = lines_ends; m_selected_line_id = 0; m_last_lines_size = 0; @@ -771,9 +771,7 @@ void GCodeViewer::load(const GCodeProcessorResult& gcode_result, const Print& pr // release gpu memory, if used reset(); - m_sequential_view.gcode_window.load_gcode(gcode_result.filename, - // Stealing out lines_ends should be safe because this gcode_result is processed only once (see the 1st if in this function). - std::move(const_cast&>(gcode_result.lines_ends))); + m_sequential_view.gcode_window.load_gcode(gcode_result.filename, gcode_result.lines_ends); if (wxGetApp().is_gcode_viewer()) m_custom_gcode_per_print_z = gcode_result.custom_gcode_per_print_z; @@ -993,11 +991,13 @@ void GCodeViewer::render() render_toolpaths(); render_shells(); float legend_height = 0.0f; - render_legend(legend_height); - if (m_sequential_view.current.last != m_sequential_view.endpoints.last) { - m_sequential_view.marker.set_world_position(m_sequential_view.current_position); - m_sequential_view.marker.set_world_offset(m_sequential_view.current_offset); - m_sequential_view.render(legend_height); + if (!m_layers.empty()) { + render_legend(legend_height); + if (m_sequential_view.current.last != m_sequential_view.endpoints.last) { + m_sequential_view.marker.set_world_position(m_sequential_view.current_position); + m_sequential_view.marker.set_world_offset(m_sequential_view.current_offset); + m_sequential_view.render(legend_height); + } } #if ENABLE_GCODE_VIEWER_STATISTICS render_statistics(); diff --git a/src/slic3r/GUI/GCodeViewer.hpp b/src/slic3r/GUI/GCodeViewer.hpp index e2ee6dcd0..51adcab5a 100644 --- a/src/slic3r/GUI/GCodeViewer.hpp +++ b/src/slic3r/GUI/GCodeViewer.hpp @@ -1,909 +1,909 @@ -#ifndef slic3r_GCodeViewer_hpp_ -#define slic3r_GCodeViewer_hpp_ - -#include "3DScene.hpp" -#include "libslic3r/GCode/GCodeProcessor.hpp" -#include "GLModel.hpp" - -#include - -#include -#include -#include -#include - -namespace Slic3r { - -class Print; -class TriangleMesh; - -namespace GUI { - -class GCodeViewer -{ - using IBufferType = unsigned short; - using VertexBuffer = std::vector; - using MultiVertexBuffer = std::vector; - using IndexBuffer = std::vector; - using MultiIndexBuffer = std::vector; - using InstanceBuffer = std::vector; - using InstanceIdBuffer = std::vector; - using InstancesOffsets = std::vector; - - static const std::vector Extrusion_Role_Colors; - static const std::vector Options_Colors; - static const std::vector Travel_Colors; - static const std::vector Range_Colors; - static const ColorRGBA Wipe_Color; - static const ColorRGBA Neutral_Color; - - enum class EOptionsColors : unsigned char - { - Retractions, - Unretractions, - Seams, - ToolChanges, - ColorChanges, - PausePrints, - CustomGCodes - }; - - // vbo buffer containing vertices data used to render a specific toolpath type - struct VBuffer - { - enum class EFormat : unsigned char - { - // vertex format: 3 floats -> position.x|position.y|position.z - Position, - // vertex format: 4 floats -> position.x|position.y|position.z|normal.x - PositionNormal1, - // vertex format: 6 floats -> position.x|position.y|position.z|normal.x|normal.y|normal.z - PositionNormal3 - }; - - EFormat format{ EFormat::Position }; - // vbos id - std::vector vbos; - // sizes of the buffers, in bytes, used in export to obj - std::vector sizes; - // count of vertices, updated after data are sent to gpu - size_t count{ 0 }; - - size_t data_size_bytes() const { return count * vertex_size_bytes(); } - // We set 65536 as max count of vertices inside a vertex buffer to allow - // to use unsigned short in place of unsigned int for indices in the index buffer, to save memory - size_t max_size_bytes() const { return 65536 * vertex_size_bytes(); } - - size_t vertex_size_floats() const { return position_size_floats() + normal_size_floats(); } - size_t vertex_size_bytes() const { return vertex_size_floats() * sizeof(float); } - - size_t position_offset_floats() const { return 0; } - size_t position_offset_bytes() const { return position_offset_floats() * sizeof(float); } - - size_t position_size_floats() const { return 3; } - size_t position_size_bytes() const { return position_size_floats() * sizeof(float); } - - size_t normal_offset_floats() const { - assert(format == EFormat::PositionNormal1 || format == EFormat::PositionNormal3); - return position_size_floats(); - } - size_t normal_offset_bytes() const { return normal_offset_floats() * sizeof(float); } - - size_t normal_size_floats() const { - switch (format) - { - case EFormat::PositionNormal1: { return 1; } - case EFormat::PositionNormal3: { return 3; } - default: { return 0; } - } - } - size_t normal_size_bytes() const { return normal_size_floats() * sizeof(float); } - - void reset(); - }; - - // buffer containing instances data used to render a toolpaths using instanced or batched models - // instance record format: - // instanced models: 5 floats -> position.x|position.y|position.z|width|height (which are sent to the shader as -> vec3 (offset) + vec2 (scales) in GLModel::render_instanced()) - // batched models: 3 floats -> position.x|position.y|position.z - struct InstanceVBuffer - { - // ranges used to render only subparts of the intances - struct Ranges - { - struct Range - { - // offset in bytes of the 1st instance to render - unsigned int offset; - // count of instances to render - unsigned int count; - // vbo id - unsigned int vbo{ 0 }; - // Color to apply to the instances - ColorRGBA color; - }; - - std::vector ranges; - - void reset(); - }; - - enum class EFormat : unsigned char - { - InstancedModel, - BatchedModel - }; - - EFormat format; - - // cpu-side buffer containing all instances data - InstanceBuffer buffer; - // indices of the moves for all instances - std::vector s_ids; - // position offsets, used to show the correct value of the tool position - InstancesOffsets offsets; - Ranges render_ranges; - - size_t data_size_bytes() const { return s_ids.size() * instance_size_bytes(); } - - size_t instance_size_floats() const { - switch (format) - { - case EFormat::InstancedModel: { return 5; } - case EFormat::BatchedModel: { return 3; } - default: { return 0; } - } - } - size_t instance_size_bytes() const { return instance_size_floats() * sizeof(float); } - - void reset(); - }; - - // ibo buffer containing indices data (for lines/triangles) used to render a specific toolpath type - struct IBuffer - { - // id of the associated vertex buffer - unsigned int vbo{ 0 }; - // ibo id - unsigned int ibo{ 0 }; - // count of indices, updated after data are sent to gpu - size_t count{ 0 }; - - void reset(); - }; - - // Used to identify different toolpath sub-types inside a IBuffer - struct Path - { - struct Endpoint - { - // index of the buffer in the multibuffer vector - // the buffer type may change: - // it is the vertex buffer while extracting vertices data, - // the index buffer while extracting indices data - unsigned int b_id{ 0 }; - // index into the buffer - size_t i_id{ 0 }; - // move id - size_t s_id{ 0 }; - Vec3f position{ Vec3f::Zero() }; - }; - - struct Sub_Path - { - Endpoint first; - Endpoint last; - - bool contains(size_t s_id) const { - return first.s_id <= s_id && s_id <= last.s_id; - } - }; - - EMoveType type{ EMoveType::Noop }; - ExtrusionRole role{ erNone }; - float delta_extruder{ 0.0f }; - float height{ 0.0f }; - float width{ 0.0f }; - float feedrate{ 0.0f }; - float fan_speed{ 0.0f }; - float temperature{ 0.0f }; - float volumetric_rate{ 0.0f }; - unsigned char extruder_id{ 0 }; - unsigned char cp_color_id{ 0 }; - std::vector sub_paths; - -#if ENABLE_VOLUMETRIC_RATE_TOOLPATHS_RECALC - bool matches(const GCodeProcessorResult::MoveVertex& move, bool account_for_volumetric_rate) const; -#else - bool matches(const GCodeProcessorResult::MoveVertex& move) const; -#endif // ENABLE_VOLUMETRIC_RATE_TOOLPATHS_RECALC - size_t vertices_count() const { - return sub_paths.empty() ? 0 : sub_paths.back().last.s_id - sub_paths.front().first.s_id + 1; - } - bool contains(size_t s_id) const { - return sub_paths.empty() ? false : sub_paths.front().first.s_id <= s_id && s_id <= sub_paths.back().last.s_id; - } - int get_id_of_sub_path_containing(size_t s_id) const { - if (sub_paths.empty()) - return -1; - else { - for (int i = 0; i < static_cast(sub_paths.size()); ++i) { - if (sub_paths[i].contains(s_id)) - return i; - } - return -1; - } - } - void add_sub_path(const GCodeProcessorResult::MoveVertex& move, unsigned int b_id, size_t i_id, size_t s_id) { - Endpoint endpoint = { b_id, i_id, s_id, move.position }; - sub_paths.push_back({ endpoint , endpoint }); - } - }; - - // Used to batch the indices needed to render the paths - struct RenderPath - { - // Index of the parent tbuffer - unsigned char tbuffer_id; - // Render path property - ColorRGBA color; - // Index of the buffer in TBuffer::indices - unsigned int ibuffer_id; - // Render path content - // Index of the path in TBuffer::paths - unsigned int path_id; - std::vector sizes; - std::vector offsets; // use size_t because we need an unsigned integer whose size matches pointer's size (used in the call glMultiDrawElements()) - bool contains(size_t offset) const { - for (size_t i = 0; i < offsets.size(); ++i) { - if (offsets[i] <= offset && offset <= offsets[i] + static_cast(sizes[i] * sizeof(IBufferType))) - return true; - } - return false; - } - }; - struct RenderPathPropertyLower { - bool operator() (const RenderPath &l, const RenderPath &r) const { - if (l.tbuffer_id < r.tbuffer_id) - return true; - if (l.color < r.color) - return true; - else if (l.color > r.color) - return false; - return l.ibuffer_id < r.ibuffer_id; - } - }; - struct RenderPathPropertyEqual { - bool operator() (const RenderPath &l, const RenderPath &r) const { - return l.tbuffer_id == r.tbuffer_id && l.ibuffer_id == r.ibuffer_id && l.color == r.color; - } - }; - - // buffer containing data for rendering a specific toolpath type - struct TBuffer - { - enum class ERenderPrimitiveType : unsigned char - { - Line, - Triangle, - InstancedModel, - BatchedModel - }; - - ERenderPrimitiveType render_primitive_type; - - // buffers for point, line and triangle primitive types - VBuffer vertices; - std::vector indices; - - struct Model - { - GLModel model; - ColorRGBA color; - InstanceVBuffer instances; - GLModel::Geometry data; - - void reset(); - }; - - // contain the buffer for model primitive types - Model model; - - std::string shader; - std::vector paths; - std::vector render_paths; - bool visible{ false }; - - void reset(); - - // b_id index of buffer contained in this->indices - // i_id index of first index contained in this->indices[b_id] - // s_id index of first vertex contained in this->vertices - void add_path(const GCodeProcessorResult::MoveVertex& move, unsigned int b_id, size_t i_id, size_t s_id); - - unsigned int max_vertices_per_segment() const { - switch (render_primitive_type) - { - case ERenderPrimitiveType::Line: { return 2; } - case ERenderPrimitiveType::Triangle: { return 8; } - default: { return 0; } - } - } - - size_t max_vertices_per_segment_size_floats() const { return vertices.vertex_size_floats() * static_cast(max_vertices_per_segment()); } - size_t max_vertices_per_segment_size_bytes() const { return max_vertices_per_segment_size_floats() * sizeof(float); } - unsigned int indices_per_segment() const { - switch (render_primitive_type) - { - case ERenderPrimitiveType::Line: { return 2; } - case ERenderPrimitiveType::Triangle: { return 30; } // 3 indices x 10 triangles - default: { return 0; } - } - } - size_t indices_per_segment_size_bytes() const { return static_cast(indices_per_segment() * sizeof(IBufferType)); } - unsigned int max_indices_per_segment() const { - switch (render_primitive_type) - { - case ERenderPrimitiveType::Line: { return 2; } - case ERenderPrimitiveType::Triangle: { return 36; } // 3 indices x 12 triangles - default: { return 0; } - } - } - size_t max_indices_per_segment_size_bytes() const { return max_indices_per_segment() * sizeof(IBufferType); } - - bool has_data() const { - switch (render_primitive_type) - { - case ERenderPrimitiveType::Line: - case ERenderPrimitiveType::Triangle: { - return !vertices.vbos.empty() && vertices.vbos.front() != 0 && !indices.empty() && indices.front().ibo != 0; - } - case ERenderPrimitiveType::InstancedModel: { return model.model.is_initialized() && !model.instances.buffer.empty(); } - case ERenderPrimitiveType::BatchedModel: { -#if ENABLE_LEGACY_OPENGL_REMOVAL - return !model.data.vertices.empty() && !model.data.indices.empty() && -#else - return model.data.vertices_count() > 0 && model.data.indices_count() && -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - !vertices.vbos.empty() && vertices.vbos.front() != 0 && !indices.empty() && indices.front().ibo != 0; - } - default: { return false; } - } - } - }; - - // helper to render shells - struct Shells - { - GLVolumeCollection volumes; - bool visible{ false }; - }; - -#if ENABLE_SHOW_TOOLPATHS_COG - // helper to render center of gravity - class COG - { - GLModel m_model; - bool m_visible{ false }; - // whether or not to render the model with fixed screen size - bool m_fixed_size{ true }; - double m_total_mass{ 0.0 }; - Vec3d m_position{ Vec3d::Zero() }; - - public: - void render(); - - void reset() { - m_position = Vec3d::Zero(); - m_total_mass = 0.0; - } - - bool is_visible() const { return m_visible; } - void set_visible(bool visible) { m_visible = visible; } - - void add_segment(const Vec3d& v1, const Vec3d& v2, double mass) { - assert(mass > 0.0); - m_position += mass * 0.5 * (v1 + v2); - m_total_mass += mass; - } - - Vec3d cog() const { return (m_total_mass > 0.0) ? (Vec3d)(m_position / m_total_mass) : Vec3d::Zero(); } - - private: - void init() { - if (m_model.is_initialized()) - return; - - const float radius = m_fixed_size ? 10.0f : 1.0f; - -#if ENABLE_LEGACY_OPENGL_REMOVAL - m_model.init_from(smooth_sphere(32, radius)); -#else - m_model.init_from(its_make_sphere(radius, PI / 32.0)); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - } - }; -#endif // ENABLE_SHOW_TOOLPATHS_COG - - // helper to render extrusion paths - struct Extrusions - { - struct Range - { -#if ENABLE_PREVIEW_LAYER_TIME - enum class EType : unsigned char - { - Linear, - Logarithmic - }; -#endif // ENABLE_PREVIEW_LAYER_TIME - - float min; - float max; - unsigned int count; - - Range() { reset(); } - - void update_from(const float value) { - if (value != max && value != min) - ++count; - min = std::min(min, value); - max = std::max(max, value); - } - void reset() { min = FLT_MAX; max = -FLT_MAX; count = 0; } - -#if ENABLE_PREVIEW_LAYER_TIME - float step_size(EType type = EType::Linear) const; - ColorRGBA get_color_at(float value, EType type = EType::Linear) const; -#else - float step_size() const { return (max - min) / (static_cast(Range_Colors.size()) - 1.0f); } - ColorRGBA get_color_at(float value) const; -#endif // ENABLE_PREVIEW_LAYER_TIME - }; - - struct Ranges - { - // Color mapping by layer height. - Range height; - // Color mapping by extrusion width. - Range width; - // Color mapping by feedrate. - Range feedrate; - // Color mapping by fan speed. - Range fan_speed; - // Color mapping by volumetric extrusion rate. - Range volumetric_rate; - // Color mapping by extrusion temperature. - Range temperature; -#if ENABLE_PREVIEW_LAYER_TIME - // Color mapping by layer time. - std::array(PrintEstimatedStatistics::ETimeMode::Count)> layer_time; -#endif // ENABLE_PREVIEW_LAYER_TIME - - void reset() { - height.reset(); - width.reset(); - feedrate.reset(); - fan_speed.reset(); - volumetric_rate.reset(); - temperature.reset(); -#if ENABLE_PREVIEW_LAYER_TIME - for (auto& range : layer_time) { - range.reset(); - } -#endif // ENABLE_PREVIEW_LAYER_TIME - } - }; - - unsigned int role_visibility_flags{ 0 }; - Ranges ranges; - - void reset_role_visibility_flags() { - role_visibility_flags = 0; - for (unsigned int i = 0; i < erCount; ++i) { - role_visibility_flags |= 1 << i; - } - } - - void reset_ranges() { ranges.reset(); } - }; - - class Layers - { - public: - struct Range - { - size_t first{ 0 }; - size_t last{ 0 }; - - bool operator == (const Range& other) const { return first == other.first && last == other.last; } - bool operator != (const Range& other) const { return !operator==(other); } - bool contains(size_t id) const { return first <= id && id <= last; } - }; - - private: - std::vector m_zs; - std::vector m_ranges; - - public: - void append(double z, const Range& range) { - m_zs.emplace_back(z); - m_ranges.emplace_back(range); - } - - void reset() { - m_zs = std::vector(); - m_ranges = std::vector(); - } - - size_t size() const { return m_zs.size(); } - bool empty() const { return m_zs.empty(); } - const std::vector& get_zs() const { return m_zs; } - const std::vector& get_ranges() const { return m_ranges; } - std::vector& get_ranges() { return m_ranges; } - double get_z_at(unsigned int id) const { return (id < m_zs.size()) ? m_zs[id] : 0.0; } - Range get_range_at(unsigned int id) const { return (id < m_ranges.size()) ? m_ranges[id] : Range(); } - - bool operator != (const Layers& other) const { - if (m_zs != other.m_zs) - return true; - if (m_ranges != other.m_ranges) - return true; - return false; - } - }; - - // used to render the toolpath caps of the current sequential range - // (i.e. when sliding on the horizontal slider) - struct SequentialRangeCap - { - TBuffer* buffer{ nullptr }; - unsigned int ibo{ 0 }; - unsigned int vbo{ 0 }; - ColorRGBA color; - - ~SequentialRangeCap(); - bool is_renderable() const { return buffer != nullptr; } - void reset(); - size_t indices_count() const { return 6; } - }; - -#if ENABLE_GCODE_VIEWER_STATISTICS - struct Statistics - { - // time - int64_t results_time{ 0 }; - int64_t load_time{ 0 }; - int64_t load_vertices{ 0 }; - int64_t smooth_vertices{ 0 }; - int64_t load_indices{ 0 }; - int64_t refresh_time{ 0 }; - int64_t refresh_paths_time{ 0 }; - // opengl calls - int64_t gl_multi_lines_calls_count{ 0 }; - int64_t gl_multi_triangles_calls_count{ 0 }; - int64_t gl_triangles_calls_count{ 0 }; - int64_t gl_instanced_models_calls_count{ 0 }; - int64_t gl_batched_models_calls_count{ 0 }; - // memory - int64_t results_size{ 0 }; - int64_t total_vertices_gpu_size{ 0 }; - int64_t total_indices_gpu_size{ 0 }; - int64_t total_instances_gpu_size{ 0 }; - int64_t max_vbuffer_gpu_size{ 0 }; - int64_t max_ibuffer_gpu_size{ 0 }; - int64_t paths_size{ 0 }; - int64_t render_paths_size{ 0 }; - int64_t models_instances_size{ 0 }; - // other - int64_t travel_segments_count{ 0 }; - int64_t wipe_segments_count{ 0 }; - int64_t extrude_segments_count{ 0 }; - int64_t instances_count{ 0 }; - int64_t batched_count{ 0 }; - int64_t vbuffers_count{ 0 }; - int64_t ibuffers_count{ 0 }; - - void reset_all() { - reset_times(); - reset_opengl(); - reset_sizes(); - reset_others(); - } - - void reset_times() { - results_time = 0; - load_time = 0; - load_vertices = 0; - smooth_vertices = 0; - load_indices = 0; - refresh_time = 0; - refresh_paths_time = 0; - } - - void reset_opengl() { - gl_multi_lines_calls_count = 0; - gl_multi_triangles_calls_count = 0; - gl_triangles_calls_count = 0; - gl_instanced_models_calls_count = 0; - gl_batched_models_calls_count = 0; - } - - void reset_sizes() { - results_size = 0; - total_vertices_gpu_size = 0; - total_indices_gpu_size = 0; - total_instances_gpu_size = 0; - max_vbuffer_gpu_size = 0; - max_ibuffer_gpu_size = 0; - paths_size = 0; - render_paths_size = 0; - models_instances_size = 0; - } - - void reset_others() { - travel_segments_count = 0; - wipe_segments_count = 0; - extrude_segments_count = 0; - instances_count = 0; - batched_count = 0; - vbuffers_count = 0; - ibuffers_count = 0; - } - }; -#endif // ENABLE_GCODE_VIEWER_STATISTICS - -public: - struct SequentialView - { - class Marker - { - GLModel m_model; - Vec3f m_world_position; - Transform3f m_world_transform; - // for seams, the position of the marker is on the last endpoint of the toolpath containing it - // the offset is used to show the correct value of tool position in the "ToolPosition" window - // see implementation of render() method - Vec3f m_world_offset; - float m_z_offset{ 0.5f }; - bool m_visible{ true }; - - public: - void init(); - - const BoundingBoxf3& get_bounding_box() const { return m_model.get_bounding_box(); } - - void set_world_position(const Vec3f& position); - void set_world_offset(const Vec3f& offset) { m_world_offset = offset; } - - bool is_visible() const { return m_visible; } - void set_visible(bool visible) { m_visible = visible; } - - void render(); - }; - - class GCodeWindow - { - struct Line - { - std::string command; - std::string parameters; - std::string comment; - }; - bool m_visible{ true }; - uint64_t m_selected_line_id{ 0 }; - size_t m_last_lines_size{ 0 }; - std::string m_filename; - boost::iostreams::mapped_file_source m_file; - // map for accessing data in file by line number - std::vector m_lines_ends; - // current visible lines - std::vector m_lines; - - public: - GCodeWindow() = default; - ~GCodeWindow() { stop_mapping_file(); } - void load_gcode(const std::string& filename, std::vector &&lines_ends); - void reset() { - stop_mapping_file(); - m_lines_ends.clear(); - m_lines.clear(); - m_filename.clear(); - } - - void toggle_visibility() { m_visible = !m_visible; } - - void render(float top, float bottom, uint64_t curr_line_id) const; - - void stop_mapping_file(); - }; - - struct Endpoints - { - size_t first{ 0 }; - size_t last{ 0 }; - }; - - bool skip_invisible_moves{ false }; - Endpoints endpoints; - Endpoints current; - Endpoints last_current; - Endpoints global; - Vec3f current_position{ Vec3f::Zero() }; - Vec3f current_offset{ Vec3f::Zero() }; - Marker marker; - GCodeWindow gcode_window; - std::vector gcode_ids; - - void render(float legend_height); - }; - - enum class EViewType : unsigned char - { - FeatureType, - Height, - Width, - Feedrate, - FanSpeed, - Temperature, - VolumetricRate, -#if ENABLE_PREVIEW_LAYER_TIME - LayerTimeLinear, - LayerTimeLogarithmic, -#endif // ENABLE_PREVIEW_LAYER_TIME - Tool, - ColorPrint, - Count - }; - -private: - bool m_gl_data_initialized{ false }; - unsigned int m_last_result_id{ 0 }; -#if ENABLE_VOLUMETRIC_RATE_TOOLPATHS_RECALC - EViewType m_last_view_type{ EViewType::Count }; -#endif // ENABLE_VOLUMETRIC_RATE_TOOLPATHS_RECALC - size_t m_moves_count{ 0 }; - std::vector m_buffers{ static_cast(EMoveType::Extrude) }; - // bounding box of toolpaths - BoundingBoxf3 m_paths_bounding_box; - // bounding box of toolpaths + marker tools - BoundingBoxf3 m_max_bounding_box; - float m_max_print_height{ 0.0f }; - std::vector m_tool_colors; - Layers m_layers; - std::array m_layers_z_range; - std::vector m_roles; - size_t m_extruders_count; - std::vector m_extruder_ids; - std::vector m_filament_diameters; - std::vector m_filament_densities; - Extrusions m_extrusions; - SequentialView m_sequential_view; - Shells m_shells; -#if ENABLE_SHOW_TOOLPATHS_COG - COG m_cog; -#endif // ENABLE_SHOW_TOOLPATHS_COG - EViewType m_view_type{ EViewType::FeatureType }; - bool m_legend_enabled{ true }; -#if ENABLE_PREVIEW_LAYOUT - struct LegendResizer - { - bool dirty{ true }; - void reset() { dirty = true; } - }; - LegendResizer m_legend_resizer; -#endif // ENABLE_PREVIEW_LAYOUT - PrintEstimatedStatistics m_print_statistics; - PrintEstimatedStatistics::ETimeMode m_time_estimate_mode{ PrintEstimatedStatistics::ETimeMode::Normal }; -#if ENABLE_GCODE_VIEWER_STATISTICS - Statistics m_statistics; -#endif // ENABLE_GCODE_VIEWER_STATISTICS - std::array m_detected_point_sizes = { 0.0f, 0.0f }; - GCodeProcessorResult::SettingsIds m_settings_ids; - std::array m_sequential_range_caps; -#if ENABLE_PREVIEW_LAYER_TIME - std::array, static_cast(PrintEstimatedStatistics::ETimeMode::Count)> m_layers_times; -#endif // ENABLE_PREVIEW_LAYER_TIME - - std::vector m_custom_gcode_per_print_z; - - bool m_contained_in_bed{ true }; - -public: - GCodeViewer(); - ~GCodeViewer() { reset(); } - - void init(); - - // extract rendering data from the given parameters -#if ENABLE_LEGACY_OPENGL_REMOVAL - void load(const GCodeProcessorResult& gcode_result, const Print& print); -#else - void load(const GCodeProcessorResult& gcode_result, const Print& print, bool initialized); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - // recalculate ranges in dependence of what is visible and sets tool/print colors - void refresh(const GCodeProcessorResult& gcode_result, const std::vector& str_tool_colors); -#if ENABLE_PREVIEW_LAYOUT - void refresh_render_paths(bool keep_sequential_current_first, bool keep_sequential_current_last) const; -#else - void refresh_render_paths(); -#endif // ENABLE_PREVIEW_LAYOUT - void update_shells_color_by_extruder(const DynamicPrintConfig* config); - - void reset(); - void render(); -#if ENABLE_SHOW_TOOLPATHS_COG - void render_cog() { m_cog.render(); } -#endif // ENABLE_SHOW_TOOLPATHS_COG - - bool has_data() const { return !m_roles.empty(); } - bool can_export_toolpaths() const; - - const BoundingBoxf3& get_paths_bounding_box() const { return m_paths_bounding_box; } - const BoundingBoxf3& get_max_bounding_box() const { return m_max_bounding_box; } - const std::vector& get_layers_zs() const { return m_layers.get_zs(); } - - const SequentialView& get_sequential_view() const { return m_sequential_view; } - void update_sequential_view_current(unsigned int first, unsigned int last); - - bool is_contained_in_bed() const { return m_contained_in_bed; } - - EViewType get_view_type() const { return m_view_type; } - void set_view_type(EViewType type) { - if (type == EViewType::Count) - type = EViewType::FeatureType; - - m_view_type = type; - } - - bool is_toolpath_move_type_visible(EMoveType type) const; - void set_toolpath_move_type_visible(EMoveType type, bool visible); - unsigned int get_toolpath_role_visibility_flags() const { return m_extrusions.role_visibility_flags; } - void set_toolpath_role_visibility_flags(unsigned int flags) { m_extrusions.role_visibility_flags = flags; } - unsigned int get_options_visibility_flags() const; - void set_options_visibility_from_flags(unsigned int flags); - void set_layers_z_range(const std::array& layers_z_range); - - bool is_legend_enabled() const { return m_legend_enabled; } - void enable_legend(bool enable) { m_legend_enabled = enable; } - - void export_toolpaths_to_obj(const char* filename) const; - - void toggle_gcode_window_visibility() { m_sequential_view.gcode_window.toggle_visibility(); } - - std::vector& get_custom_gcode_per_print_z() { return m_custom_gcode_per_print_z; } - size_t get_extruders_count() { return m_extruders_count; } - -#if ENABLE_PREVIEW_LAYOUT - void invalidate_legend() { m_legend_resizer.reset(); } -#endif // ENABLE_PREVIEW_LAYOUT - -private: - void load_toolpaths(const GCodeProcessorResult& gcode_result); -#if ENABLE_LEGACY_OPENGL_REMOVAL - void load_shells(const Print& print); -#else - void load_shells(const Print& print, bool initialized); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL -#if !ENABLE_PREVIEW_LAYOUT - void refresh_render_paths(bool keep_sequential_current_first, bool keep_sequential_current_last) const; -#endif // !ENABLE_PREVIEW_LAYOUT - void render_toolpaths(); - void render_shells(); - void render_legend(float& legend_height); -#if ENABLE_GCODE_VIEWER_STATISTICS - void render_statistics(); -#endif // ENABLE_GCODE_VIEWER_STATISTICS - bool is_visible(ExtrusionRole role) const { - return role < erCount && (m_extrusions.role_visibility_flags & (1 << role)) != 0; - } - bool is_visible(const Path& path) const { return is_visible(path.role); } - void log_memory_used(const std::string& label, int64_t additional = 0) const; - ColorRGBA option_color(EMoveType move_type) const; -}; - -} // namespace GUI -} // namespace Slic3r - -#endif // slic3r_GCodeViewer_hpp_ - +#ifndef slic3r_GCodeViewer_hpp_ +#define slic3r_GCodeViewer_hpp_ + +#include "3DScene.hpp" +#include "libslic3r/GCode/GCodeProcessor.hpp" +#include "GLModel.hpp" + +#include + +#include +#include +#include +#include + +namespace Slic3r { + +class Print; +class TriangleMesh; + +namespace GUI { + +class GCodeViewer +{ + using IBufferType = unsigned short; + using VertexBuffer = std::vector; + using MultiVertexBuffer = std::vector; + using IndexBuffer = std::vector; + using MultiIndexBuffer = std::vector; + using InstanceBuffer = std::vector; + using InstanceIdBuffer = std::vector; + using InstancesOffsets = std::vector; + + static const std::vector Extrusion_Role_Colors; + static const std::vector Options_Colors; + static const std::vector Travel_Colors; + static const std::vector Range_Colors; + static const ColorRGBA Wipe_Color; + static const ColorRGBA Neutral_Color; + + enum class EOptionsColors : unsigned char + { + Retractions, + Unretractions, + Seams, + ToolChanges, + ColorChanges, + PausePrints, + CustomGCodes + }; + + // vbo buffer containing vertices data used to render a specific toolpath type + struct VBuffer + { + enum class EFormat : unsigned char + { + // vertex format: 3 floats -> position.x|position.y|position.z + Position, + // vertex format: 4 floats -> position.x|position.y|position.z|normal.x + PositionNormal1, + // vertex format: 6 floats -> position.x|position.y|position.z|normal.x|normal.y|normal.z + PositionNormal3 + }; + + EFormat format{ EFormat::Position }; + // vbos id + std::vector vbos; + // sizes of the buffers, in bytes, used in export to obj + std::vector sizes; + // count of vertices, updated after data are sent to gpu + size_t count{ 0 }; + + size_t data_size_bytes() const { return count * vertex_size_bytes(); } + // We set 65536 as max count of vertices inside a vertex buffer to allow + // to use unsigned short in place of unsigned int for indices in the index buffer, to save memory + size_t max_size_bytes() const { return 65536 * vertex_size_bytes(); } + + size_t vertex_size_floats() const { return position_size_floats() + normal_size_floats(); } + size_t vertex_size_bytes() const { return vertex_size_floats() * sizeof(float); } + + size_t position_offset_floats() const { return 0; } + size_t position_offset_bytes() const { return position_offset_floats() * sizeof(float); } + + size_t position_size_floats() const { return 3; } + size_t position_size_bytes() const { return position_size_floats() * sizeof(float); } + + size_t normal_offset_floats() const { + assert(format == EFormat::PositionNormal1 || format == EFormat::PositionNormal3); + return position_size_floats(); + } + size_t normal_offset_bytes() const { return normal_offset_floats() * sizeof(float); } + + size_t normal_size_floats() const { + switch (format) + { + case EFormat::PositionNormal1: { return 1; } + case EFormat::PositionNormal3: { return 3; } + default: { return 0; } + } + } + size_t normal_size_bytes() const { return normal_size_floats() * sizeof(float); } + + void reset(); + }; + + // buffer containing instances data used to render a toolpaths using instanced or batched models + // instance record format: + // instanced models: 5 floats -> position.x|position.y|position.z|width|height (which are sent to the shader as -> vec3 (offset) + vec2 (scales) in GLModel::render_instanced()) + // batched models: 3 floats -> position.x|position.y|position.z + struct InstanceVBuffer + { + // ranges used to render only subparts of the intances + struct Ranges + { + struct Range + { + // offset in bytes of the 1st instance to render + unsigned int offset; + // count of instances to render + unsigned int count; + // vbo id + unsigned int vbo{ 0 }; + // Color to apply to the instances + ColorRGBA color; + }; + + std::vector ranges; + + void reset(); + }; + + enum class EFormat : unsigned char + { + InstancedModel, + BatchedModel + }; + + EFormat format; + + // cpu-side buffer containing all instances data + InstanceBuffer buffer; + // indices of the moves for all instances + std::vector s_ids; + // position offsets, used to show the correct value of the tool position + InstancesOffsets offsets; + Ranges render_ranges; + + size_t data_size_bytes() const { return s_ids.size() * instance_size_bytes(); } + + size_t instance_size_floats() const { + switch (format) + { + case EFormat::InstancedModel: { return 5; } + case EFormat::BatchedModel: { return 3; } + default: { return 0; } + } + } + size_t instance_size_bytes() const { return instance_size_floats() * sizeof(float); } + + void reset(); + }; + + // ibo buffer containing indices data (for lines/triangles) used to render a specific toolpath type + struct IBuffer + { + // id of the associated vertex buffer + unsigned int vbo{ 0 }; + // ibo id + unsigned int ibo{ 0 }; + // count of indices, updated after data are sent to gpu + size_t count{ 0 }; + + void reset(); + }; + + // Used to identify different toolpath sub-types inside a IBuffer + struct Path + { + struct Endpoint + { + // index of the buffer in the multibuffer vector + // the buffer type may change: + // it is the vertex buffer while extracting vertices data, + // the index buffer while extracting indices data + unsigned int b_id{ 0 }; + // index into the buffer + size_t i_id{ 0 }; + // move id + size_t s_id{ 0 }; + Vec3f position{ Vec3f::Zero() }; + }; + + struct Sub_Path + { + Endpoint first; + Endpoint last; + + bool contains(size_t s_id) const { + return first.s_id <= s_id && s_id <= last.s_id; + } + }; + + EMoveType type{ EMoveType::Noop }; + ExtrusionRole role{ erNone }; + float delta_extruder{ 0.0f }; + float height{ 0.0f }; + float width{ 0.0f }; + float feedrate{ 0.0f }; + float fan_speed{ 0.0f }; + float temperature{ 0.0f }; + float volumetric_rate{ 0.0f }; + unsigned char extruder_id{ 0 }; + unsigned char cp_color_id{ 0 }; + std::vector sub_paths; + +#if ENABLE_VOLUMETRIC_RATE_TOOLPATHS_RECALC + bool matches(const GCodeProcessorResult::MoveVertex& move, bool account_for_volumetric_rate) const; +#else + bool matches(const GCodeProcessorResult::MoveVertex& move) const; +#endif // ENABLE_VOLUMETRIC_RATE_TOOLPATHS_RECALC + size_t vertices_count() const { + return sub_paths.empty() ? 0 : sub_paths.back().last.s_id - sub_paths.front().first.s_id + 1; + } + bool contains(size_t s_id) const { + return sub_paths.empty() ? false : sub_paths.front().first.s_id <= s_id && s_id <= sub_paths.back().last.s_id; + } + int get_id_of_sub_path_containing(size_t s_id) const { + if (sub_paths.empty()) + return -1; + else { + for (int i = 0; i < static_cast(sub_paths.size()); ++i) { + if (sub_paths[i].contains(s_id)) + return i; + } + return -1; + } + } + void add_sub_path(const GCodeProcessorResult::MoveVertex& move, unsigned int b_id, size_t i_id, size_t s_id) { + Endpoint endpoint = { b_id, i_id, s_id, move.position }; + sub_paths.push_back({ endpoint , endpoint }); + } + }; + + // Used to batch the indices needed to render the paths + struct RenderPath + { + // Index of the parent tbuffer + unsigned char tbuffer_id; + // Render path property + ColorRGBA color; + // Index of the buffer in TBuffer::indices + unsigned int ibuffer_id; + // Render path content + // Index of the path in TBuffer::paths + unsigned int path_id; + std::vector sizes; + std::vector offsets; // use size_t because we need an unsigned integer whose size matches pointer's size (used in the call glMultiDrawElements()) + bool contains(size_t offset) const { + for (size_t i = 0; i < offsets.size(); ++i) { + if (offsets[i] <= offset && offset <= offsets[i] + static_cast(sizes[i] * sizeof(IBufferType))) + return true; + } + return false; + } + }; + struct RenderPathPropertyLower { + bool operator() (const RenderPath &l, const RenderPath &r) const { + if (l.tbuffer_id < r.tbuffer_id) + return true; + if (l.color < r.color) + return true; + else if (l.color > r.color) + return false; + return l.ibuffer_id < r.ibuffer_id; + } + }; + struct RenderPathPropertyEqual { + bool operator() (const RenderPath &l, const RenderPath &r) const { + return l.tbuffer_id == r.tbuffer_id && l.ibuffer_id == r.ibuffer_id && l.color == r.color; + } + }; + + // buffer containing data for rendering a specific toolpath type + struct TBuffer + { + enum class ERenderPrimitiveType : unsigned char + { + Line, + Triangle, + InstancedModel, + BatchedModel + }; + + ERenderPrimitiveType render_primitive_type; + + // buffers for point, line and triangle primitive types + VBuffer vertices; + std::vector indices; + + struct Model + { + GLModel model; + ColorRGBA color; + InstanceVBuffer instances; + GLModel::Geometry data; + + void reset(); + }; + + // contain the buffer for model primitive types + Model model; + + std::string shader; + std::vector paths; + std::vector render_paths; + bool visible{ false }; + + void reset(); + + // b_id index of buffer contained in this->indices + // i_id index of first index contained in this->indices[b_id] + // s_id index of first vertex contained in this->vertices + void add_path(const GCodeProcessorResult::MoveVertex& move, unsigned int b_id, size_t i_id, size_t s_id); + + unsigned int max_vertices_per_segment() const { + switch (render_primitive_type) + { + case ERenderPrimitiveType::Line: { return 2; } + case ERenderPrimitiveType::Triangle: { return 8; } + default: { return 0; } + } + } + + size_t max_vertices_per_segment_size_floats() const { return vertices.vertex_size_floats() * static_cast(max_vertices_per_segment()); } + size_t max_vertices_per_segment_size_bytes() const { return max_vertices_per_segment_size_floats() * sizeof(float); } + unsigned int indices_per_segment() const { + switch (render_primitive_type) + { + case ERenderPrimitiveType::Line: { return 2; } + case ERenderPrimitiveType::Triangle: { return 30; } // 3 indices x 10 triangles + default: { return 0; } + } + } + size_t indices_per_segment_size_bytes() const { return static_cast(indices_per_segment() * sizeof(IBufferType)); } + unsigned int max_indices_per_segment() const { + switch (render_primitive_type) + { + case ERenderPrimitiveType::Line: { return 2; } + case ERenderPrimitiveType::Triangle: { return 36; } // 3 indices x 12 triangles + default: { return 0; } + } + } + size_t max_indices_per_segment_size_bytes() const { return max_indices_per_segment() * sizeof(IBufferType); } + + bool has_data() const { + switch (render_primitive_type) + { + case ERenderPrimitiveType::Line: + case ERenderPrimitiveType::Triangle: { + return !vertices.vbos.empty() && vertices.vbos.front() != 0 && !indices.empty() && indices.front().ibo != 0; + } + case ERenderPrimitiveType::InstancedModel: { return model.model.is_initialized() && !model.instances.buffer.empty(); } + case ERenderPrimitiveType::BatchedModel: { +#if ENABLE_LEGACY_OPENGL_REMOVAL + return !model.data.vertices.empty() && !model.data.indices.empty() && +#else + return model.data.vertices_count() > 0 && model.data.indices_count() && +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + !vertices.vbos.empty() && vertices.vbos.front() != 0 && !indices.empty() && indices.front().ibo != 0; + } + default: { return false; } + } + } + }; + + // helper to render shells + struct Shells + { + GLVolumeCollection volumes; + bool visible{ false }; + }; + +#if ENABLE_SHOW_TOOLPATHS_COG + // helper to render center of gravity + class COG + { + GLModel m_model; + bool m_visible{ false }; + // whether or not to render the model with fixed screen size + bool m_fixed_size{ true }; + double m_total_mass{ 0.0 }; + Vec3d m_position{ Vec3d::Zero() }; + + public: + void render(); + + void reset() { + m_position = Vec3d::Zero(); + m_total_mass = 0.0; + } + + bool is_visible() const { return m_visible; } + void set_visible(bool visible) { m_visible = visible; } + + void add_segment(const Vec3d& v1, const Vec3d& v2, double mass) { + assert(mass > 0.0); + m_position += mass * 0.5 * (v1 + v2); + m_total_mass += mass; + } + + Vec3d cog() const { return (m_total_mass > 0.0) ? (Vec3d)(m_position / m_total_mass) : Vec3d::Zero(); } + + private: + void init() { + if (m_model.is_initialized()) + return; + + const float radius = m_fixed_size ? 10.0f : 1.0f; + +#if ENABLE_LEGACY_OPENGL_REMOVAL + m_model.init_from(smooth_sphere(32, radius)); +#else + m_model.init_from(its_make_sphere(radius, PI / 32.0)); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + } + }; +#endif // ENABLE_SHOW_TOOLPATHS_COG + + // helper to render extrusion paths + struct Extrusions + { + struct Range + { +#if ENABLE_PREVIEW_LAYER_TIME + enum class EType : unsigned char + { + Linear, + Logarithmic + }; +#endif // ENABLE_PREVIEW_LAYER_TIME + + float min; + float max; + unsigned int count; + + Range() { reset(); } + + void update_from(const float value) { + if (value != max && value != min) + ++count; + min = std::min(min, value); + max = std::max(max, value); + } + void reset() { min = FLT_MAX; max = -FLT_MAX; count = 0; } + +#if ENABLE_PREVIEW_LAYER_TIME + float step_size(EType type = EType::Linear) const; + ColorRGBA get_color_at(float value, EType type = EType::Linear) const; +#else + float step_size() const { return (max - min) / (static_cast(Range_Colors.size()) - 1.0f); } + ColorRGBA get_color_at(float value) const; +#endif // ENABLE_PREVIEW_LAYER_TIME + }; + + struct Ranges + { + // Color mapping by layer height. + Range height; + // Color mapping by extrusion width. + Range width; + // Color mapping by feedrate. + Range feedrate; + // Color mapping by fan speed. + Range fan_speed; + // Color mapping by volumetric extrusion rate. + Range volumetric_rate; + // Color mapping by extrusion temperature. + Range temperature; +#if ENABLE_PREVIEW_LAYER_TIME + // Color mapping by layer time. + std::array(PrintEstimatedStatistics::ETimeMode::Count)> layer_time; +#endif // ENABLE_PREVIEW_LAYER_TIME + + void reset() { + height.reset(); + width.reset(); + feedrate.reset(); + fan_speed.reset(); + volumetric_rate.reset(); + temperature.reset(); +#if ENABLE_PREVIEW_LAYER_TIME + for (auto& range : layer_time) { + range.reset(); + } +#endif // ENABLE_PREVIEW_LAYER_TIME + } + }; + + unsigned int role_visibility_flags{ 0 }; + Ranges ranges; + + void reset_role_visibility_flags() { + role_visibility_flags = 0; + for (unsigned int i = 0; i < erCount; ++i) { + role_visibility_flags |= 1 << i; + } + } + + void reset_ranges() { ranges.reset(); } + }; + + class Layers + { + public: + struct Range + { + size_t first{ 0 }; + size_t last{ 0 }; + + bool operator == (const Range& other) const { return first == other.first && last == other.last; } + bool operator != (const Range& other) const { return !operator==(other); } + bool contains(size_t id) const { return first <= id && id <= last; } + }; + + private: + std::vector m_zs; + std::vector m_ranges; + + public: + void append(double z, const Range& range) { + m_zs.emplace_back(z); + m_ranges.emplace_back(range); + } + + void reset() { + m_zs = std::vector(); + m_ranges = std::vector(); + } + + size_t size() const { return m_zs.size(); } + bool empty() const { return m_zs.empty(); } + const std::vector& get_zs() const { return m_zs; } + const std::vector& get_ranges() const { return m_ranges; } + std::vector& get_ranges() { return m_ranges; } + double get_z_at(unsigned int id) const { return (id < m_zs.size()) ? m_zs[id] : 0.0; } + Range get_range_at(unsigned int id) const { return (id < m_ranges.size()) ? m_ranges[id] : Range(); } + + bool operator != (const Layers& other) const { + if (m_zs != other.m_zs) + return true; + if (m_ranges != other.m_ranges) + return true; + return false; + } + }; + + // used to render the toolpath caps of the current sequential range + // (i.e. when sliding on the horizontal slider) + struct SequentialRangeCap + { + TBuffer* buffer{ nullptr }; + unsigned int ibo{ 0 }; + unsigned int vbo{ 0 }; + ColorRGBA color; + + ~SequentialRangeCap(); + bool is_renderable() const { return buffer != nullptr; } + void reset(); + size_t indices_count() const { return 6; } + }; + +#if ENABLE_GCODE_VIEWER_STATISTICS + struct Statistics + { + // time + int64_t results_time{ 0 }; + int64_t load_time{ 0 }; + int64_t load_vertices{ 0 }; + int64_t smooth_vertices{ 0 }; + int64_t load_indices{ 0 }; + int64_t refresh_time{ 0 }; + int64_t refresh_paths_time{ 0 }; + // opengl calls + int64_t gl_multi_lines_calls_count{ 0 }; + int64_t gl_multi_triangles_calls_count{ 0 }; + int64_t gl_triangles_calls_count{ 0 }; + int64_t gl_instanced_models_calls_count{ 0 }; + int64_t gl_batched_models_calls_count{ 0 }; + // memory + int64_t results_size{ 0 }; + int64_t total_vertices_gpu_size{ 0 }; + int64_t total_indices_gpu_size{ 0 }; + int64_t total_instances_gpu_size{ 0 }; + int64_t max_vbuffer_gpu_size{ 0 }; + int64_t max_ibuffer_gpu_size{ 0 }; + int64_t paths_size{ 0 }; + int64_t render_paths_size{ 0 }; + int64_t models_instances_size{ 0 }; + // other + int64_t travel_segments_count{ 0 }; + int64_t wipe_segments_count{ 0 }; + int64_t extrude_segments_count{ 0 }; + int64_t instances_count{ 0 }; + int64_t batched_count{ 0 }; + int64_t vbuffers_count{ 0 }; + int64_t ibuffers_count{ 0 }; + + void reset_all() { + reset_times(); + reset_opengl(); + reset_sizes(); + reset_others(); + } + + void reset_times() { + results_time = 0; + load_time = 0; + load_vertices = 0; + smooth_vertices = 0; + load_indices = 0; + refresh_time = 0; + refresh_paths_time = 0; + } + + void reset_opengl() { + gl_multi_lines_calls_count = 0; + gl_multi_triangles_calls_count = 0; + gl_triangles_calls_count = 0; + gl_instanced_models_calls_count = 0; + gl_batched_models_calls_count = 0; + } + + void reset_sizes() { + results_size = 0; + total_vertices_gpu_size = 0; + total_indices_gpu_size = 0; + total_instances_gpu_size = 0; + max_vbuffer_gpu_size = 0; + max_ibuffer_gpu_size = 0; + paths_size = 0; + render_paths_size = 0; + models_instances_size = 0; + } + + void reset_others() { + travel_segments_count = 0; + wipe_segments_count = 0; + extrude_segments_count = 0; + instances_count = 0; + batched_count = 0; + vbuffers_count = 0; + ibuffers_count = 0; + } + }; +#endif // ENABLE_GCODE_VIEWER_STATISTICS + +public: + struct SequentialView + { + class Marker + { + GLModel m_model; + Vec3f m_world_position; + Transform3f m_world_transform; + // for seams, the position of the marker is on the last endpoint of the toolpath containing it + // the offset is used to show the correct value of tool position in the "ToolPosition" window + // see implementation of render() method + Vec3f m_world_offset; + float m_z_offset{ 0.5f }; + bool m_visible{ true }; + + public: + void init(); + + const BoundingBoxf3& get_bounding_box() const { return m_model.get_bounding_box(); } + + void set_world_position(const Vec3f& position); + void set_world_offset(const Vec3f& offset) { m_world_offset = offset; } + + bool is_visible() const { return m_visible; } + void set_visible(bool visible) { m_visible = visible; } + + void render(); + }; + + class GCodeWindow + { + struct Line + { + std::string command; + std::string parameters; + std::string comment; + }; + bool m_visible{ true }; + uint64_t m_selected_line_id{ 0 }; + size_t m_last_lines_size{ 0 }; + std::string m_filename; + boost::iostreams::mapped_file_source m_file; + // map for accessing data in file by line number + std::vector m_lines_ends; + // current visible lines + std::vector m_lines; + + public: + GCodeWindow() = default; + ~GCodeWindow() { stop_mapping_file(); } + void load_gcode(const std::string& filename, const std::vector& lines_ends); + void reset() { + stop_mapping_file(); + m_lines_ends.clear(); + m_lines.clear(); + m_filename.clear(); + } + + void toggle_visibility() { m_visible = !m_visible; } + + void render(float top, float bottom, uint64_t curr_line_id) const; + + void stop_mapping_file(); + }; + + struct Endpoints + { + size_t first{ 0 }; + size_t last{ 0 }; + }; + + bool skip_invisible_moves{ false }; + Endpoints endpoints; + Endpoints current; + Endpoints last_current; + Endpoints global; + Vec3f current_position{ Vec3f::Zero() }; + Vec3f current_offset{ Vec3f::Zero() }; + Marker marker; + GCodeWindow gcode_window; + std::vector gcode_ids; + + void render(float legend_height); + }; + + enum class EViewType : unsigned char + { + FeatureType, + Height, + Width, + Feedrate, + FanSpeed, + Temperature, + VolumetricRate, +#if ENABLE_PREVIEW_LAYER_TIME + LayerTimeLinear, + LayerTimeLogarithmic, +#endif // ENABLE_PREVIEW_LAYER_TIME + Tool, + ColorPrint, + Count + }; + +private: + bool m_gl_data_initialized{ false }; + unsigned int m_last_result_id{ 0 }; +#if ENABLE_VOLUMETRIC_RATE_TOOLPATHS_RECALC + EViewType m_last_view_type{ EViewType::Count }; +#endif // ENABLE_VOLUMETRIC_RATE_TOOLPATHS_RECALC + size_t m_moves_count{ 0 }; + std::vector m_buffers{ static_cast(EMoveType::Extrude) }; + // bounding box of toolpaths + BoundingBoxf3 m_paths_bounding_box; + // bounding box of toolpaths + marker tools + BoundingBoxf3 m_max_bounding_box; + float m_max_print_height{ 0.0f }; + std::vector m_tool_colors; + Layers m_layers; + std::array m_layers_z_range; + std::vector m_roles; + size_t m_extruders_count; + std::vector m_extruder_ids; + std::vector m_filament_diameters; + std::vector m_filament_densities; + Extrusions m_extrusions; + SequentialView m_sequential_view; + Shells m_shells; +#if ENABLE_SHOW_TOOLPATHS_COG + COG m_cog; +#endif // ENABLE_SHOW_TOOLPATHS_COG + EViewType m_view_type{ EViewType::FeatureType }; + bool m_legend_enabled{ true }; +#if ENABLE_PREVIEW_LAYOUT + struct LegendResizer + { + bool dirty{ true }; + void reset() { dirty = true; } + }; + LegendResizer m_legend_resizer; +#endif // ENABLE_PREVIEW_LAYOUT + PrintEstimatedStatistics m_print_statistics; + PrintEstimatedStatistics::ETimeMode m_time_estimate_mode{ PrintEstimatedStatistics::ETimeMode::Normal }; +#if ENABLE_GCODE_VIEWER_STATISTICS + Statistics m_statistics; +#endif // ENABLE_GCODE_VIEWER_STATISTICS + std::array m_detected_point_sizes = { 0.0f, 0.0f }; + GCodeProcessorResult::SettingsIds m_settings_ids; + std::array m_sequential_range_caps; +#if ENABLE_PREVIEW_LAYER_TIME + std::array, static_cast(PrintEstimatedStatistics::ETimeMode::Count)> m_layers_times; +#endif // ENABLE_PREVIEW_LAYER_TIME + + std::vector m_custom_gcode_per_print_z; + + bool m_contained_in_bed{ true }; + +public: + GCodeViewer(); + ~GCodeViewer() { reset(); } + + void init(); + + // extract rendering data from the given parameters +#if ENABLE_LEGACY_OPENGL_REMOVAL + void load(const GCodeProcessorResult& gcode_result, const Print& print); +#else + void load(const GCodeProcessorResult& gcode_result, const Print& print, bool initialized); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + // recalculate ranges in dependence of what is visible and sets tool/print colors + void refresh(const GCodeProcessorResult& gcode_result, const std::vector& str_tool_colors); +#if ENABLE_PREVIEW_LAYOUT + void refresh_render_paths(bool keep_sequential_current_first, bool keep_sequential_current_last) const; +#else + void refresh_render_paths(); +#endif // ENABLE_PREVIEW_LAYOUT + void update_shells_color_by_extruder(const DynamicPrintConfig* config); + + void reset(); + void render(); +#if ENABLE_SHOW_TOOLPATHS_COG + void render_cog() { m_cog.render(); } +#endif // ENABLE_SHOW_TOOLPATHS_COG + + bool has_data() const { return !m_roles.empty(); } + bool can_export_toolpaths() const; + + const BoundingBoxf3& get_paths_bounding_box() const { return m_paths_bounding_box; } + const BoundingBoxf3& get_max_bounding_box() const { return m_max_bounding_box; } + const std::vector& get_layers_zs() const { return m_layers.get_zs(); } + + const SequentialView& get_sequential_view() const { return m_sequential_view; } + void update_sequential_view_current(unsigned int first, unsigned int last); + + bool is_contained_in_bed() const { return m_contained_in_bed; } + + EViewType get_view_type() const { return m_view_type; } + void set_view_type(EViewType type) { + if (type == EViewType::Count) + type = EViewType::FeatureType; + + m_view_type = type; + } + + bool is_toolpath_move_type_visible(EMoveType type) const; + void set_toolpath_move_type_visible(EMoveType type, bool visible); + unsigned int get_toolpath_role_visibility_flags() const { return m_extrusions.role_visibility_flags; } + void set_toolpath_role_visibility_flags(unsigned int flags) { m_extrusions.role_visibility_flags = flags; } + unsigned int get_options_visibility_flags() const; + void set_options_visibility_from_flags(unsigned int flags); + void set_layers_z_range(const std::array& layers_z_range); + + bool is_legend_enabled() const { return m_legend_enabled; } + void enable_legend(bool enable) { m_legend_enabled = enable; } + + void export_toolpaths_to_obj(const char* filename) const; + + void toggle_gcode_window_visibility() { m_sequential_view.gcode_window.toggle_visibility(); } + + std::vector& get_custom_gcode_per_print_z() { return m_custom_gcode_per_print_z; } + size_t get_extruders_count() { return m_extruders_count; } + +#if ENABLE_PREVIEW_LAYOUT + void invalidate_legend() { m_legend_resizer.reset(); } +#endif // ENABLE_PREVIEW_LAYOUT + +private: + void load_toolpaths(const GCodeProcessorResult& gcode_result); +#if ENABLE_LEGACY_OPENGL_REMOVAL + void load_shells(const Print& print); +#else + void load_shells(const Print& print, bool initialized); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL +#if !ENABLE_PREVIEW_LAYOUT + void refresh_render_paths(bool keep_sequential_current_first, bool keep_sequential_current_last) const; +#endif // !ENABLE_PREVIEW_LAYOUT + void render_toolpaths(); + void render_shells(); + void render_legend(float& legend_height); +#if ENABLE_GCODE_VIEWER_STATISTICS + void render_statistics(); +#endif // ENABLE_GCODE_VIEWER_STATISTICS + bool is_visible(ExtrusionRole role) const { + return role < erCount && (m_extrusions.role_visibility_flags & (1 << role)) != 0; + } + bool is_visible(const Path& path) const { return is_visible(path.role); } + void log_memory_used(const std::string& label, int64_t additional = 0) const; + ColorRGBA option_color(EMoveType move_type) const; +}; + +} // namespace GUI +} // namespace Slic3r + +#endif // slic3r_GCodeViewer_hpp_ + diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index 994080e06..3a5c89bea 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -1101,6 +1101,9 @@ wxDEFINE_EVENT(EVT_GLCANVAS_QUESTION_MARK, SimpleEvent); wxDEFINE_EVENT(EVT_GLCANVAS_INCREASE_INSTANCES, Event); wxDEFINE_EVENT(EVT_GLCANVAS_INSTANCE_MOVED, SimpleEvent); wxDEFINE_EVENT(EVT_GLCANVAS_INSTANCE_ROTATED, SimpleEvent); +#if ENABLE_WORLD_COORDINATE +wxDEFINE_EVENT(EVT_GLCANVAS_RESET_SKEW, SimpleEvent); +#endif // ENABLE_WORLD_COORDINATE wxDEFINE_EVENT(EVT_GLCANVAS_INSTANCE_SCALED, SimpleEvent); wxDEFINE_EVENT(EVT_GLCANVAS_FORCE_UPDATE, SimpleEvent); wxDEFINE_EVENT(EVT_GLCANVAS_WIPETOWER_MOVED, Vec3dEvent); @@ -2914,7 +2917,13 @@ void GLCanvas3D::on_key(wxKeyEvent& evt) else displacement = multiplier * direction; +#if ENABLE_WORLD_COORDINATE + TransformationType trafo_type; + trafo_type.set_relative(); + m_selection.translate(displacement, trafo_type); +#else m_selection.translate(displacement); +#endif // ENABLE_WORLD_COORDINATE m_dirty = true; } ); @@ -3584,7 +3593,13 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt) } } +#if ENABLE_WORLD_COORDINATE + TransformationType trafo_type; + trafo_type.set_relative(); + m_selection.translate(cur_pos - m_mouse.drag.start_position_3D, trafo_type); +#else m_selection.translate(cur_pos - m_mouse.drag.start_position_3D); +#endif // ENABLE_WORLD_COORDINATE if (current_printer_technology() == ptFFF && fff_print()->config().complete_objects) update_sequential_clearance(); wxGetApp().obj_manipul()->set_dirty(); @@ -3830,9 +3845,17 @@ void GLCanvas3D::do_move(const std::string& snapshot_type) ModelObject* model_object = m_model->objects[object_idx]; if (model_object != nullptr) { if (selection_mode == Selection::Instance) +#if ENABLE_WORLD_COORDINATE + model_object->instances[instance_idx]->set_transformation(v->get_instance_transformation()); +#else model_object->instances[instance_idx]->set_offset(v->get_instance_offset()); +#endif // ENABLE_WORLD_COORDINATE else if (selection_mode == Selection::Volume) +#if ENABLE_WORLD_COORDINATE + model_object->volumes[volume_idx]->set_transformation(v->get_volume_transformation()); +#else model_object->volumes[volume_idx]->set_offset(v->get_volume_offset()); +#endif // ENABLE_WORLD_COORDINATE object_moved = true; model_object->invalidate_bounding_box(); @@ -3912,8 +3935,8 @@ void GLCanvas3D::do_rotate(const std::string& snapshot_type) int object_idx = v->object_idx(); if (object_idx == 1000) { // the wipe tower #endif // ENABLE_WIPETOWER_OBJECTID_1000_REMOVAL - Vec3d offset = v->get_volume_offset(); - post_event(Vec3dEvent(EVT_GLCANVAS_WIPETOWER_ROTATED, Vec3d(offset(0), offset(1), v->get_volume_rotation()(2)))); + const Vec3d offset = v->get_volume_offset(); + post_event(Vec3dEvent(EVT_GLCANVAS_WIPETOWER_ROTATED, Vec3d(offset.x(), offset.y(), v->get_volume_rotation().z()))); } #if ENABLE_WIPETOWER_OBJECTID_1000_REMOVAL int object_idx = v->object_idx(); @@ -3921,8 +3944,8 @@ void GLCanvas3D::do_rotate(const std::string& snapshot_type) if (object_idx < 0 || (int)m_model->objects.size() <= object_idx) continue; - int instance_idx = v->instance_idx(); - int volume_idx = v->volume_idx(); + const int instance_idx = v->instance_idx(); + const int volume_idx = v->volume_idx(); done.insert(std::pair(object_idx, instance_idx)); @@ -3930,12 +3953,20 @@ void GLCanvas3D::do_rotate(const std::string& snapshot_type) ModelObject* model_object = m_model->objects[object_idx]; if (model_object != nullptr) { if (selection_mode == Selection::Instance) { +#if ENABLE_WORLD_COORDINATE + model_object->instances[instance_idx]->set_transformation(v->get_instance_transformation()); +#else model_object->instances[instance_idx]->set_rotation(v->get_instance_rotation()); model_object->instances[instance_idx]->set_offset(v->get_instance_offset()); +#endif // ENABLE_WORLD_COORDINATE } else if (selection_mode == Selection::Volume) { +#if ENABLE_WORLD_COORDINATE + model_object->volumes[volume_idx]->set_transformation(v->get_volume_transformation()); +#else model_object->volumes[volume_idx]->set_rotation(v->get_volume_rotation()); model_object->volumes[volume_idx]->set_offset(v->get_volume_offset()); +#endif // ENABLE_WORLD_COORDINATE } model_object->invalidate_bounding_box(); } @@ -3985,12 +4016,12 @@ void GLCanvas3D::do_scale(const std::string& snapshot_type) Selection::EMode selection_mode = m_selection.get_mode(); for (const GLVolume* v : m_volumes.volumes) { - int object_idx = v->object_idx(); + const int object_idx = v->object_idx(); if (object_idx < 0 || (int)m_model->objects.size() <= object_idx) continue; - int instance_idx = v->instance_idx(); - int volume_idx = v->volume_idx(); + const int instance_idx = v->instance_idx(); + const int volume_idx = v->volume_idx(); done.insert(std::pair(object_idx, instance_idx)); @@ -3998,13 +4029,22 @@ void GLCanvas3D::do_scale(const std::string& snapshot_type) ModelObject* model_object = m_model->objects[object_idx]; if (model_object != nullptr) { if (selection_mode == Selection::Instance) { +#if ENABLE_WORLD_COORDINATE + model_object->instances[instance_idx]->set_transformation(v->get_instance_transformation()); +#else model_object->instances[instance_idx]->set_scaling_factor(v->get_instance_scaling_factor()); model_object->instances[instance_idx]->set_offset(v->get_instance_offset()); +#endif // ENABLE_WORLD_COORDINATE } else if (selection_mode == Selection::Volume) { +#if ENABLE_WORLD_COORDINATE + model_object->instances[instance_idx]->set_transformation(v->get_instance_transformation()); + model_object->volumes[volume_idx]->set_transformation(v->get_volume_transformation()); +#else model_object->instances[instance_idx]->set_offset(v->get_instance_offset()); model_object->volumes[volume_idx]->set_scaling_factor(v->get_volume_scaling_factor()); model_object->volumes[volume_idx]->set_offset(v->get_volume_offset()); +#endif // ENABLE_WORLD_COORDINATE } model_object->invalidate_bounding_box(); } @@ -4013,10 +4053,10 @@ void GLCanvas3D::do_scale(const std::string& snapshot_type) // Fixes sinking/flying instances for (const std::pair& i : done) { ModelObject* m = m_model->objects[i.first]; - double shift_z = m->get_instance_min_z(i.second); + const double shift_z = m->get_instance_min_z(i.second); // leave sinking instances as sinking if (min_zs.empty() || min_zs.find({ i.first, i.second })->second >= SINKING_Z_THRESHOLD || shift_z > SINKING_Z_THRESHOLD) { - Vec3d shift(0.0, 0.0, -shift_z); + const Vec3d shift(0.0, 0.0, -shift_z); m_selection.translate(i.first, i.second, shift); m->translate_instance(i.second, shift); } @@ -4066,9 +4106,17 @@ void GLCanvas3D::do_mirror(const std::string& snapshot_type) ModelObject* model_object = m_model->objects[object_idx]; if (model_object != nullptr) { if (selection_mode == Selection::Instance) +#if ENABLE_WORLD_COORDINATE + model_object->instances[instance_idx]->set_transformation(v->get_instance_transformation()); +#else model_object->instances[instance_idx]->set_mirror(v->get_instance_mirror()); +#endif // ENABLE_WORLD_COORDINATE else if (selection_mode == Selection::Volume) +#if ENABLE_WORLD_COORDINATE + model_object->volumes[volume_idx]->set_transformation(v->get_volume_transformation()); +#else model_object->volumes[volume_idx]->set_mirror(v->get_volume_mirror()); +#endif // ENABLE_WORLD_COORDINATE model_object->invalidate_bounding_box(); } @@ -4092,6 +4140,44 @@ void GLCanvas3D::do_mirror(const std::string& snapshot_type) m_dirty = true; } +#if ENABLE_WORLD_COORDINATE +void GLCanvas3D::do_reset_skew(const std::string& snapshot_type) +{ + if (m_model == nullptr) + return; + + if (!snapshot_type.empty()) + wxGetApp().plater()->take_snapshot(_(snapshot_type)); + + std::set> done; // keeps track of modified instances + + const Selection::IndicesList& idxs = m_selection.get_volume_idxs(); + + for (unsigned int id : idxs) { + const GLVolume* v = m_volumes.volumes[id]; + int object_idx = v->object_idx(); + if (object_idx < 0 || (int)m_model->objects.size() <= object_idx) + continue; + + int instance_idx = v->instance_idx(); + int volume_idx = v->volume_idx(); + + done.insert(std::pair(object_idx, instance_idx)); + + ModelObject* model_object = m_model->objects[object_idx]; + if (model_object != nullptr) { + model_object->instances[instance_idx]->set_transformation(v->get_instance_transformation()); + model_object->volumes[volume_idx]->set_transformation(v->get_volume_transformation()); + model_object->invalidate_bounding_box(); + } + } + + post_event(SimpleEvent(EVT_GLCANVAS_RESET_SKEW)); + + m_dirty = true; +} +#endif // ENABLE_WORLD_COORDINATE + void GLCanvas3D::update_gizmos_on_off_state() { set_as_dirty(); @@ -5339,7 +5425,7 @@ void GLCanvas3D::_refresh_if_shown_on_screen() void GLCanvas3D::_picking_pass() { - if (m_picking_enabled && !m_mouse.dragging && m_mouse.position != Vec2d(DBL_MAX, DBL_MAX)) { + if (m_picking_enabled && !m_mouse.dragging && m_mouse.position != Vec2d(DBL_MAX, DBL_MAX) && !m_gizmos.is_dragging()) { m_hover_volume_idxs.clear(); // Render the object for picking. @@ -7352,12 +7438,13 @@ bool GLCanvas3D::_is_any_volume_outside() const void GLCanvas3D::_update_selection_from_hover() { bool ctrl_pressed = wxGetKeyState(WXK_CONTROL); + bool selection_changed = false; if (m_hover_volume_idxs.empty()) { - if (!ctrl_pressed && m_rectangle_selection.get_state() == GLSelectionRectangle::EState::Select) + if (!ctrl_pressed && m_rectangle_selection.get_state() == GLSelectionRectangle::EState::Select) { + selection_changed = ! m_selection.is_empty(); m_selection.remove_all(); - - return; + } } GLSelectionRectangle::EState state = m_rectangle_selection.get_state(); @@ -7370,7 +7457,6 @@ void GLCanvas3D::_update_selection_from_hover() } } - bool selection_changed = false; #if ENABLE_NEW_RECTANGLE_SELECTION if (!m_rectangle_selection.is_empty()) { #endif // ENABLE_NEW_RECTANGLE_SELECTION diff --git a/src/slic3r/GUI/GLCanvas3D.hpp b/src/slic3r/GUI/GLCanvas3D.hpp index 1876500e2..a5b2acb32 100644 --- a/src/slic3r/GUI/GLCanvas3D.hpp +++ b/src/slic3r/GUI/GLCanvas3D.hpp @@ -156,6 +156,9 @@ wxDECLARE_EVENT(EVT_GLCANVAS_INSTANCE_MOVED, SimpleEvent); wxDECLARE_EVENT(EVT_GLCANVAS_FORCE_UPDATE, SimpleEvent); wxDECLARE_EVENT(EVT_GLCANVAS_WIPETOWER_MOVED, Vec3dEvent); wxDECLARE_EVENT(EVT_GLCANVAS_INSTANCE_ROTATED, SimpleEvent); +#if ENABLE_WORLD_COORDINATE +wxDECLARE_EVENT(EVT_GLCANVAS_RESET_SKEW, SimpleEvent); +#endif // ENABLE_WORLD_COORDINATE wxDECLARE_EVENT(EVT_GLCANVAS_INSTANCE_SCALED, SimpleEvent); wxDECLARE_EVENT(EVT_GLCANVAS_WIPETOWER_ROTATED, Vec3dEvent); wxDECLARE_EVENT(EVT_GLCANVAS_ENABLE_ACTION_BUTTONS, Event); @@ -739,7 +742,11 @@ public: void update_volumes_colors_by_extruder(); +#if ENABLE_WORLD_COORDINATE + bool is_dragging() const { return m_gizmos.is_dragging() || (m_moving && !m_mouse.scene_position.isApprox(m_mouse.drag.start_position_3D)); } +#else bool is_dragging() const { return m_gizmos.is_dragging() || m_moving; } +#endif // ENABLE_WORLD_COORDINATE void render(); // printable_only == false -> render also non printable volumes as grayed @@ -807,6 +814,9 @@ public: void do_rotate(const std::string& snapshot_type); void do_scale(const std::string& snapshot_type); void do_mirror(const std::string& snapshot_type); +#if ENABLE_WORLD_COORDINATE + void do_reset_skew(const std::string& snapshot_type); +#endif // ENABLE_WORLD_COORDINATE void update_gizmos_on_off_state(); void reset_all_gizmos() { m_gizmos.reset_all_states(); } @@ -844,7 +854,6 @@ public: // Returns the view ray line, in world coordinate, at the given mouse position. Linef3 mouse_ray(const Point& mouse_pos); - void set_mouse_as_dragging() { m_mouse.dragging = true; } bool is_mouse_dragging() const { return m_mouse.dragging; } double get_size_proportional_to_max_bed_size(double factor) const; diff --git a/src/slic3r/GUI/GLTexture.cpp b/src/slic3r/GUI/GLTexture.cpp index 137a0a109..9c039376d 100644 --- a/src/slic3r/GUI/GLTexture.cpp +++ b/src/slic3r/GUI/GLTexture.cpp @@ -7,6 +7,7 @@ #include "GUI_App.hpp" #include "GLModel.hpp" #endif // ENABLE_LEGACY_OPENGL_REMOVAL +#include "BitmapCache.hpp" #include @@ -22,8 +23,8 @@ #define STB_DXT_IMPLEMENTATION #include "stb_dxt/stb_dxt.h" -#include "nanosvg/nanosvg.h" -#include "nanosvg/nanosvgrast.h" +#include +#include #include "libslic3r/Utils.hpp" @@ -204,7 +205,7 @@ bool GLTexture::load_from_svg_files_as_sprites_array(const std::vector -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include - -#include "libslic3r/Utils.hpp" -#include "libslic3r/Model.hpp" -#include "libslic3r/I18N.hpp" -#include "libslic3r/PresetBundle.hpp" -#include "libslic3r/Color.hpp" - -#include "GUI.hpp" -#include "GUI_Utils.hpp" -#include "3DScene.hpp" -#include "MainFrame.hpp" -#include "Plater.hpp" -#include "GLCanvas3D.hpp" - -#include "../Utils/PresetUpdater.hpp" -#include "../Utils/PrintHost.hpp" -#include "../Utils/Process.hpp" -#include "../Utils/MacDarkMode.hpp" -#include "../Utils/AppUpdater.hpp" -#include "slic3r/Config/Snapshot.hpp" -#include "ConfigSnapshotDialog.hpp" -#include "FirmwareDialog.hpp" -#include "Preferences.hpp" -#include "Tab.hpp" -#include "SysInfoDialog.hpp" -#include "KBShortcutsDialog.hpp" -#include "UpdateDialogs.hpp" -#include "Mouse3DController.hpp" -#include "RemovableDriveManager.hpp" -#include "InstanceCheck.hpp" -#include "NotificationManager.hpp" -#include "UnsavedChangesDialog.hpp" -#include "SavePresetDialog.hpp" -#include "PrintHostDialogs.hpp" -#include "DesktopIntegrationDialog.hpp" -#include "SendSystemInfoDialog.hpp" - -#include "BitmapCache.hpp" -#include "Notebook.hpp" - -#ifdef __WXMSW__ -#include -#include -#ifdef _MSW_DARK_MODE -#include -#endif // _MSW_DARK_MODE -#endif -#ifdef _WIN32 -#include -#endif - -#if ENABLE_THUMBNAIL_GENERATOR_DEBUG -#include -#include -#endif // ENABLE_THUMBNAIL_GENERATOR_DEBUG - -// Needed for forcing menu icons back under gtk2 and gtk3 -#if defined(__WXGTK20__) || defined(__WXGTK3__) - #include -#endif - -using namespace std::literals; - -namespace Slic3r { -namespace GUI { - -class MainFrame; - -class SplashScreen : public wxSplashScreen -{ -public: - SplashScreen(const wxBitmap& bitmap, long splashStyle, int milliseconds, wxPoint pos = wxDefaultPosition) - : wxSplashScreen(bitmap, splashStyle, milliseconds, static_cast(wxGetApp().mainframe), wxID_ANY, wxDefaultPosition, wxDefaultSize, -#ifdef __APPLE__ - wxSIMPLE_BORDER | wxFRAME_NO_TASKBAR | wxSTAY_ON_TOP -#else - wxSIMPLE_BORDER | wxFRAME_NO_TASKBAR -#endif // !__APPLE__ - ) - { - wxASSERT(bitmap.IsOk()); - - int init_dpi = get_dpi_for_window(this); - this->SetPosition(pos); - this->CenterOnScreen(); - int new_dpi = get_dpi_for_window(this); - - m_scale = (float)(new_dpi) / (float)(init_dpi); - m_main_bitmap = bitmap; - - scale_bitmap(m_main_bitmap, m_scale); - - // init constant texts and scale fonts - init_constant_text(); - - // this font will be used for the action string - m_action_font = m_constant_text.credits_font.Bold(); - - // draw logo and constant info text - Decorate(m_main_bitmap); - } - - void SetText(const wxString& text) - { - set_bitmap(m_main_bitmap); - if (!text.empty()) { - wxBitmap bitmap(m_main_bitmap); - - wxMemoryDC memDC; - memDC.SelectObject(bitmap); - - memDC.SetFont(m_action_font); - memDC.SetTextForeground(wxColour(237, 107, 33)); - memDC.DrawText(text, int(m_scale * 60), m_action_line_y_position); - - memDC.SelectObject(wxNullBitmap); - set_bitmap(bitmap); -#ifdef __WXOSX__ - // without this code splash screen wouldn't be updated under OSX - wxYield(); -#endif - } - } - - static wxBitmap MakeBitmap(wxBitmap bmp) - { - if (!bmp.IsOk()) - return wxNullBitmap; - - // create dark grey background for the splashscreen - // It will be 5/3 of the weight of the bitmap - int width = lround((double)5 / 3 * bmp.GetWidth()); - int height = bmp.GetHeight(); - - wxImage image(width, height); - unsigned char* imgdata_ = image.GetData(); - for (int i = 0; i < width * height; ++i) { - *imgdata_++ = 51; - *imgdata_++ = 51; - *imgdata_++ = 51; - } - - wxBitmap new_bmp(image); - - wxMemoryDC memDC; - memDC.SelectObject(new_bmp); - memDC.DrawBitmap(bmp, width - bmp.GetWidth(), 0, true); - - return new_bmp; - } - - void Decorate(wxBitmap& bmp) - { - if (!bmp.IsOk()) - return; - - // draw text to the box at the left of the splashscreen. - // this box will be 2/5 of the weight of the bitmap, and be at the left. - int width = lround(bmp.GetWidth() * 0.4); - - // load bitmap for logo - BitmapCache bmp_cache; - int logo_size = lround(width * 0.25); - wxBitmap logo_bmp = *bmp_cache.load_svg(wxGetApp().logo_name(), logo_size, logo_size); - - wxCoord margin = int(m_scale * 20); - - wxRect banner_rect(wxPoint(0, logo_size), wxPoint(width, bmp.GetHeight())); - banner_rect.Deflate(margin, 2 * margin); - - // use a memory DC to draw directly onto the bitmap - wxMemoryDC memDc(bmp); - - // draw logo - memDc.DrawBitmap(logo_bmp, margin, margin, true); - - // draw the (white) labels inside of our black box (at the left of the splashscreen) - memDc.SetTextForeground(wxColour(255, 255, 255)); - - memDc.SetFont(m_constant_text.title_font); - memDc.DrawLabel(m_constant_text.title, banner_rect, wxALIGN_TOP | wxALIGN_LEFT); - - int title_height = memDc.GetTextExtent(m_constant_text.title).GetY(); - banner_rect.SetTop(banner_rect.GetTop() + title_height); - banner_rect.SetHeight(banner_rect.GetHeight() - title_height); - - memDc.SetFont(m_constant_text.version_font); - memDc.DrawLabel(m_constant_text.version, banner_rect, wxALIGN_TOP | wxALIGN_LEFT); - int version_height = memDc.GetTextExtent(m_constant_text.version).GetY(); - - memDc.SetFont(m_constant_text.credits_font); - memDc.DrawLabel(m_constant_text.credits, banner_rect, wxALIGN_BOTTOM | wxALIGN_LEFT); - int credits_height = memDc.GetMultiLineTextExtent(m_constant_text.credits).GetY(); - int text_height = memDc.GetTextExtent("text").GetY(); - - // calculate position for the dynamic text - int logo_and_header_height = margin + logo_size + title_height + version_height; - m_action_line_y_position = logo_and_header_height + 0.5 * (bmp.GetHeight() - margin - credits_height - logo_and_header_height - text_height); - } - -private: - wxBitmap m_main_bitmap; - wxFont m_action_font; - int m_action_line_y_position; - float m_scale {1.0}; - - struct ConstantText - { - wxString title; - wxString version; - wxString credits; - - wxFont title_font; - wxFont version_font; - wxFont credits_font; - - void init(wxFont init_font) - { - // title - title = wxGetApp().is_editor() ? SLIC3R_APP_NAME : GCODEVIEWER_APP_NAME; - - // dynamically get the version to display - version = _L("Version") + " " + std::string(SLIC3R_VERSION); - - // credits infornation - credits = title + " " + - _L("is based on Slic3r by Alessandro Ranellucci and the RepRap community.") + "\n" + - _L("Developed by Prusa Research.")+ "\n\n" + - title + " " + _L("is licensed under the") + " " + _L("GNU Affero General Public License, version 3") + "\n\n" + - _L("Contributions by Vojtech Bubnik, Enrico Turri, Oleksandra Iushchenko, Tamas Meszaros, Lukas Matena, Vojtech Kral, David Kocik and numerous others.") + "\n\n" + - _L("Artwork model by M Boyer"); - - title_font = version_font = credits_font = init_font; - } - } - m_constant_text; - - void init_constant_text() - { - m_constant_text.init(get_default_font(this)); - - // As default we use a system font for current display. - // Scale fonts in respect to banner width - - int text_banner_width = lround(0.4 * m_main_bitmap.GetWidth()) - roundl(m_scale * 50); // banner_width - margins - - float title_font_scale = (float)text_banner_width / GetTextExtent(m_constant_text.title).GetX(); - scale_font(m_constant_text.title_font, title_font_scale > 3.5f ? 3.5f : title_font_scale); - - float version_font_scale = (float)text_banner_width / GetTextExtent(m_constant_text.version).GetX(); - scale_font(m_constant_text.version_font, version_font_scale > 2.f ? 2.f : version_font_scale); - - // The width of the credits information string doesn't respect to the banner width some times. - // So, scale credits_font in the respect to the longest string width - int longest_string_width = word_wrap_string(m_constant_text.credits); - float font_scale = (float)text_banner_width / longest_string_width; - scale_font(m_constant_text.credits_font, font_scale); - } - - void set_bitmap(wxBitmap& bmp) - { - m_window->SetBitmap(bmp); - m_window->Refresh(); - m_window->Update(); - } - - void scale_bitmap(wxBitmap& bmp, float scale) - { - if (scale == 1.0) - return; - - wxImage image = bmp.ConvertToImage(); - if (!image.IsOk() || image.GetWidth() == 0 || image.GetHeight() == 0) - return; - - int width = int(scale * image.GetWidth()); - int height = int(scale * image.GetHeight()); - image.Rescale(width, height, wxIMAGE_QUALITY_BILINEAR); - - bmp = wxBitmap(std::move(image)); - } - - void scale_font(wxFont& font, float scale) - { -#ifdef __WXMSW__ - // Workaround for the font scaling in respect to the current active display, - // not for the primary display, as it's implemented in Font.cpp - // See https://github.com/wxWidgets/wxWidgets/blob/master/src/msw/font.cpp - // void wxNativeFontInfo::SetFractionalPointSize(float pointSizeNew) - wxNativeFontInfo nfi= *font.GetNativeFontInfo(); - float pointSizeNew = scale * font.GetPointSize(); - nfi.lf.lfHeight = nfi.GetLogFontHeightAtPPI(pointSizeNew, get_dpi_for_window(this)); - nfi.pointSize = pointSizeNew; - font = wxFont(nfi); -#else - font.Scale(scale); -#endif //__WXMSW__ - } - - // wrap a string for the strings no longer then 55 symbols - // return extent of the longest string - int word_wrap_string(wxString& input) - { - size_t line_len = 55;// count of symbols in one line - int idx = -1; - size_t cur_len = 0; - - wxString longest_sub_string; - auto get_longest_sub_string = [input](wxString &longest_sub_str, size_t cur_len, size_t i) { - if (cur_len > longest_sub_str.Len()) - longest_sub_str = input.SubString(i - cur_len + 1, i); - }; - - for (size_t i = 0; i < input.Len(); i++) - { - cur_len++; - if (input[i] == ' ') - idx = i; - if (input[i] == '\n') - { - get_longest_sub_string(longest_sub_string, cur_len, i); - idx = -1; - cur_len = 0; - } - if (cur_len >= line_len && idx >= 0) - { - get_longest_sub_string(longest_sub_string, cur_len, i); - input[idx] = '\n'; - cur_len = i - static_cast(idx); - } - } - - return GetTextExtent(longest_sub_string).GetX(); - } -}; - - -#ifdef __linux__ -bool static check_old_linux_datadir(const wxString& app_name) { - // If we are on Linux and the datadir does not exist yet, look into the old - // location where the datadir was before version 2.3. If we find it there, - // tell the user that he might wanna migrate to the new location. - // (https://github.com/prusa3d/PrusaSlicer/issues/2911) - // To be precise, the datadir should exist, it is created when single instance - // lock happens. Instead of checking for existence, check the contents. - - namespace fs = boost::filesystem; - - std::string new_path = Slic3r::data_dir(); - - wxString dir; - if (! wxGetEnv(wxS("XDG_CONFIG_HOME"), &dir) || dir.empty() ) - dir = wxFileName::GetHomeDir() + wxS("/.config"); - std::string default_path = (dir + "/" + app_name).ToUTF8().data(); - - if (new_path != default_path) { - // This happens when the user specifies a custom --datadir. - // Do not show anything in that case. - return true; - } - - fs::path data_dir = fs::path(new_path); - if (! fs::is_directory(data_dir)) - return true; // This should not happen. - - int file_count = std::distance(fs::directory_iterator(data_dir), fs::directory_iterator()); - - if (file_count <= 1) { // just cache dir with an instance lock - std::string old_path = wxStandardPaths::Get().GetUserDataDir().ToUTF8().data(); - - if (fs::is_directory(old_path)) { - wxString msg = from_u8((boost::format(_u8L("Starting with %1% 2.3, configuration " - "directory on Linux has changed (according to XDG Base Directory Specification) to \n%2%.\n\n" - "This directory did not exist yet (maybe you run the new version for the first time).\nHowever, " - "an old %1% configuration directory was detected in \n%3%.\n\n" - "Consider moving the contents of the old directory to the new location in order to access " - "your profiles, etc.\nNote that if you decide to downgrade %1% in future, it will use the old " - "location again.\n\n" - "What do you want to do now?")) % SLIC3R_APP_NAME % new_path % old_path).str()); - wxString caption = from_u8((boost::format(_u8L("%s - BREAKING CHANGE")) % SLIC3R_APP_NAME).str()); - RichMessageDialog dlg(nullptr, msg, caption, wxYES_NO); - dlg.SetYesNoLabels(_L("Quit, I will move my data now"), _L("Start the application")); - if (dlg.ShowModal() != wxID_NO) - return false; - } - } else { - // If the new directory exists, be silent. The user likely already saw the message. - } - return true; -} -#endif - -#ifdef _WIN32 -#if 0 // External Updater is replaced with AppUpdater.cpp -static bool run_updater_win() -{ - // find updater exe - boost::filesystem::path path_updater = boost::dll::program_location().parent_path() / "prusaslicer-updater.exe"; - // run updater. Original args: /silent -restartapp prusa-slicer.exe -startappfirst - std::string msg; - bool res = create_process(path_updater, L"/silent", msg); - if (!res) - BOOST_LOG_TRIVIAL(error) << msg; - return res; -} -#endif // 0 -#endif // _WIN32 - -struct FileWildcards { - std::string_view title; - std::vector file_extensions; -}; - -static const FileWildcards file_wildcards_by_type[FT_SIZE] = { - /* FT_STL */ { "STL files"sv, { ".stl"sv } }, - /* FT_OBJ */ { "OBJ files"sv, { ".obj"sv } }, - /* FT_OBJECT */ { "Object files"sv, { ".stl"sv, ".obj"sv } }, - /* FT_AMF */ { "AMF files"sv, { ".amf"sv, ".zip.amf"sv, ".xml"sv } }, - /* FT_3MF */ { "3MF files"sv, { ".3mf"sv } }, - /* FT_GCODE */ { "G-code files"sv, { ".gcode"sv, ".gco"sv, ".g"sv, ".ngc"sv } }, - /* FT_MODEL */ { "Known files"sv, { ".stl"sv, ".obj"sv, ".3mf"sv, ".amf"sv, ".zip.amf"sv, ".xml"sv } }, - /* FT_PROJECT */ { "Project files"sv, { ".3mf"sv, ".amf"sv, ".zip.amf"sv } }, - /* FT_GALLERY */ { "Known files"sv, { ".stl"sv, ".obj"sv } }, - - /* FT_INI */ { "INI files"sv, { ".ini"sv } }, - /* FT_SVG */ { "SVG files"sv, { ".svg"sv } }, - - /* FT_TEX */ { "Texture"sv, { ".png"sv, ".svg"sv } }, - - /* FT_SL1 */ { "Masked SLA files"sv, { ".sl1"sv, ".sl1s"sv, ".pwmx"sv } }, -}; - -#if ENABLE_ALTERNATIVE_FILE_WILDCARDS_GENERATOR -wxString file_wildcards(FileType file_type) -{ - const FileWildcards& data = file_wildcards_by_type[file_type]; - std::string title; - std::string mask; - - // Generate cumulative first item - for (const std::string_view& ext : data.file_extensions) { - if (title.empty()) { - title = "*"; - title += ext; - mask = title; - } - else { - title += ", *"; - title += ext; - mask += ";*"; - mask += ext; - } - mask += ";*"; - mask += boost::to_upper_copy(std::string(ext)); - } - - wxString ret = GUI::format_wxstr("%s (%s)|%s", data.title, title, mask); - - // Adds an item for each of the extensions - if (data.file_extensions.size() > 1) { - for (const std::string_view& ext : data.file_extensions) { - title = "*"; - title += ext; - ret += GUI::format_wxstr("|%s (%s)|%s", data.title, title, title); - } - } - - return ret; -} -#else -// This function produces a Win32 file dialog file template mask to be consumed by wxWidgets on all platforms. -// The function accepts a custom extension parameter. If the parameter is provided, the custom extension -// will be added as a fist to the list. This is important for a "file save" dialog on OSX, which strips -// an extension from the provided initial file name and substitutes it with the default extension (the first one in the template). -wxString file_wildcards(FileType file_type, const std::string &custom_extension) -{ - const FileWildcards& data = file_wildcards_by_type[file_type]; - std::string title; - std::string mask; - std::string custom_ext_lower; - - if (! custom_extension.empty()) { - // Generate an extension into the title mask and into the list of extensions. - custom_ext_lower = boost::to_lower_copy(custom_extension); - const std::string custom_ext_upper = boost::to_upper_copy(custom_extension); - if (custom_ext_lower == custom_extension) { - // Add a lower case version. - title = std::string("*") + custom_ext_lower; - mask = title; - // Add an upper case version. - mask += ";*"; - mask += custom_ext_upper; - } else if (custom_ext_upper == custom_extension) { - // Add an upper case version. - title = std::string("*") + custom_ext_upper; - mask = title; - // Add a lower case version. - mask += ";*"; - mask += custom_ext_lower; - } else { - // Add the mixed case version only. - title = std::string("*") + custom_extension; - mask = title; - } - } - - for (const std::string_view &ext : data.file_extensions) - // Only add an extension if it was not added first as the custom extension. - if (ext != custom_ext_lower) { - if (title.empty()) { - title = "*"; - title += ext; - mask = title; - } else { - title += ", *"; - title += ext; - mask += ";*"; - mask += ext; - } - mask += ";*"; - mask += boost::to_upper_copy(std::string(ext)); - } - - return GUI::format_wxstr("%s (%s)|%s", data.title, title, mask); -} -#endif // ENABLE_ALTERNATIVE_FILE_WILDCARDS_GENERATOR - -static std::string libslic3r_translate_callback(const char *s) { return wxGetTranslation(wxString(s, wxConvUTF8)).utf8_str().data(); } - -#ifdef WIN32 -#if !wxVERSION_EQUAL_OR_GREATER_THAN(3,1,3) -static void register_win32_dpi_event() -{ - enum { WM_DPICHANGED_ = 0x02e0 }; - - wxWindow::MSWRegisterMessageHandler(WM_DPICHANGED_, [](wxWindow *win, WXUINT nMsg, WXWPARAM wParam, WXLPARAM lParam) { - const int dpi = wParam & 0xffff; - const auto rect = reinterpret_cast(lParam); - const wxRect wxrect(wxPoint(rect->top, rect->left), wxPoint(rect->bottom, rect->right)); - - DpiChangedEvent evt(EVT_DPI_CHANGED_SLICER, dpi, wxrect); - win->GetEventHandler()->AddPendingEvent(evt); - - return true; - }); -} -#endif // !wxVERSION_EQUAL_OR_GREATER_THAN - -static GUID GUID_DEVINTERFACE_HID = { 0x4D1E55B2, 0xF16F, 0x11CF, 0x88, 0xCB, 0x00, 0x11, 0x11, 0x00, 0x00, 0x30 }; - -static void register_win32_device_notification_event() -{ - wxWindow::MSWRegisterMessageHandler(WM_DEVICECHANGE, [](wxWindow *win, WXUINT /* nMsg */, WXWPARAM wParam, WXLPARAM lParam) { - // Some messages are sent to top level windows by default, some messages are sent to only registered windows, and we explictely register on MainFrame only. - auto main_frame = dynamic_cast(win); - auto plater = (main_frame == nullptr) ? nullptr : main_frame->plater(); - if (plater == nullptr) - // Maybe some other top level window like a dialog or maybe a pop-up menu? - return true; - PDEV_BROADCAST_HDR lpdb = (PDEV_BROADCAST_HDR)lParam; - switch (wParam) { - case DBT_DEVICEARRIVAL: - if (lpdb->dbch_devicetype == DBT_DEVTYP_VOLUME) - plater->GetEventHandler()->AddPendingEvent(VolumeAttachedEvent(EVT_VOLUME_ATTACHED)); - else if (lpdb->dbch_devicetype == DBT_DEVTYP_DEVICEINTERFACE) { - PDEV_BROADCAST_DEVICEINTERFACE lpdbi = (PDEV_BROADCAST_DEVICEINTERFACE)lpdb; -// if (lpdbi->dbcc_classguid == GUID_DEVINTERFACE_VOLUME) { -// printf("DBT_DEVICEARRIVAL %d - Media has arrived: %ws\n", msg_count, lpdbi->dbcc_name); - if (lpdbi->dbcc_classguid == GUID_DEVINTERFACE_HID) - plater->GetEventHandler()->AddPendingEvent(HIDDeviceAttachedEvent(EVT_HID_DEVICE_ATTACHED, boost::nowide::narrow(lpdbi->dbcc_name))); - } - break; - case DBT_DEVICEREMOVECOMPLETE: - if (lpdb->dbch_devicetype == DBT_DEVTYP_VOLUME) - plater->GetEventHandler()->AddPendingEvent(VolumeDetachedEvent(EVT_VOLUME_DETACHED)); - else if (lpdb->dbch_devicetype == DBT_DEVTYP_DEVICEINTERFACE) { - PDEV_BROADCAST_DEVICEINTERFACE lpdbi = (PDEV_BROADCAST_DEVICEINTERFACE)lpdb; -// if (lpdbi->dbcc_classguid == GUID_DEVINTERFACE_VOLUME) -// printf("DBT_DEVICEARRIVAL %d - Media was removed: %ws\n", msg_count, lpdbi->dbcc_name); - if (lpdbi->dbcc_classguid == GUID_DEVINTERFACE_HID) - plater->GetEventHandler()->AddPendingEvent(HIDDeviceDetachedEvent(EVT_HID_DEVICE_DETACHED, boost::nowide::narrow(lpdbi->dbcc_name))); - } - break; - default: - break; - } - return true; - }); - - wxWindow::MSWRegisterMessageHandler(MainFrame::WM_USER_MEDIACHANGED, [](wxWindow *win, WXUINT /* nMsg */, WXWPARAM wParam, WXLPARAM lParam) { - // Some messages are sent to top level windows by default, some messages are sent to only registered windows, and we explictely register on MainFrame only. - auto main_frame = dynamic_cast(win); - auto plater = (main_frame == nullptr) ? nullptr : main_frame->plater(); - if (plater == nullptr) - // Maybe some other top level window like a dialog or maybe a pop-up menu? - return true; - wchar_t sPath[MAX_PATH]; - if (lParam == SHCNE_MEDIAINSERTED || lParam == SHCNE_MEDIAREMOVED) { - struct _ITEMIDLIST* pidl = *reinterpret_cast(wParam); - if (! SHGetPathFromIDList(pidl, sPath)) { - BOOST_LOG_TRIVIAL(error) << "MediaInserted: SHGetPathFromIDList failed"; - return false; - } - } - switch (lParam) { - case SHCNE_MEDIAINSERTED: - { - //printf("SHCNE_MEDIAINSERTED %S\n", sPath); - plater->GetEventHandler()->AddPendingEvent(VolumeAttachedEvent(EVT_VOLUME_ATTACHED)); - break; - } - case SHCNE_MEDIAREMOVED: - { - //printf("SHCNE_MEDIAREMOVED %S\n", sPath); - plater->GetEventHandler()->AddPendingEvent(VolumeDetachedEvent(EVT_VOLUME_DETACHED)); - break; - } - default: -// printf("Unknown\n"); - break; - } - return true; - }); - - wxWindow::MSWRegisterMessageHandler(WM_INPUT, [](wxWindow *win, WXUINT /* nMsg */, WXWPARAM wParam, WXLPARAM lParam) { - auto main_frame = dynamic_cast(Slic3r::GUI::find_toplevel_parent(win)); - auto plater = (main_frame == nullptr) ? nullptr : main_frame->plater(); -// if (wParam == RIM_INPUTSINK && plater != nullptr && main_frame->IsActive()) { - if (wParam == RIM_INPUT && plater != nullptr && main_frame->IsActive()) { - RAWINPUT raw; - UINT rawSize = sizeof(RAWINPUT); - ::GetRawInputData((HRAWINPUT)lParam, RID_INPUT, &raw, &rawSize, sizeof(RAWINPUTHEADER)); - if (raw.header.dwType == RIM_TYPEHID && plater->get_mouse3d_controller().handle_raw_input_win32(raw.data.hid.bRawData, raw.data.hid.dwSizeHid)) - return true; - } - return false; - }); - - wxWindow::MSWRegisterMessageHandler(WM_COPYDATA, [](wxWindow* win, WXUINT /* nMsg */, WXWPARAM wParam, WXLPARAM lParam) { - COPYDATASTRUCT* copy_data_structure = { 0 }; - copy_data_structure = (COPYDATASTRUCT*)lParam; - if (copy_data_structure->dwData == 1) { - LPCWSTR arguments = (LPCWSTR)copy_data_structure->lpData; - Slic3r::GUI::wxGetApp().other_instance_message_handler()->handle_message(boost::nowide::narrow(arguments)); - } - return true; - }); -} -#endif // WIN32 - -static void generic_exception_handle() -{ - // Note: Some wxWidgets APIs use wxLogError() to report errors, eg. wxImage - // - see https://docs.wxwidgets.org/3.1/classwx_image.html#aa249e657259fe6518d68a5208b9043d0 - // - // wxLogError typically goes around exception handling and display an error dialog some time - // after an error is logged even if exception handling and OnExceptionInMainLoop() take place. - // This is why we use wxLogError() here as well instead of a custom dialog, because it accumulates - // errors if multiple have been collected and displays just one error message for all of them. - // Otherwise we would get multiple error messages for one missing png, for example. - // - // If a custom error message window (or some other solution) were to be used, it would be necessary - // to turn off wxLogError() usage in wx APIs, most notably in wxImage - // - see https://docs.wxwidgets.org/trunk/classwx_image.html#aa32e5d3507cc0f8c3330135bc0befc6a - - try { - throw; - } catch (const std::bad_alloc& ex) { - // bad_alloc in main thread is most likely fatal. Report immediately to the user (wxLogError would be delayed) - // and terminate the app so it is at least certain to happen now. - wxString errmsg = wxString::Format(_L("%s has encountered an error. It was likely caused by running out of memory. " - "If you are sure you have enough RAM on your system, this may also be a bug and we would " - "be glad if you reported it.\n\nThe application will now terminate."), SLIC3R_APP_NAME); - wxMessageBox(errmsg + "\n\n" + wxString(ex.what()), _L("Fatal error"), wxOK | wxICON_ERROR); - BOOST_LOG_TRIVIAL(error) << boost::format("std::bad_alloc exception: %1%") % ex.what(); - std::terminate(); - } catch (const boost::io::bad_format_string& ex) { - wxString errmsg = _L("PrusaSlicer has encountered a localization error. " - "Please report to PrusaSlicer team, what language was active and in which scenario " - "this issue happened. Thank you.\n\nThe application will now terminate."); - wxMessageBox(errmsg + "\n\n" + wxString(ex.what()), _L("Critical error"), wxOK | wxICON_ERROR); - BOOST_LOG_TRIVIAL(error) << boost::format("Uncaught exception: %1%") % ex.what(); - std::terminate(); - throw; - } catch (const std::exception& ex) { - wxLogError(format_wxstr(_L("Internal error: %1%"), ex.what())); - BOOST_LOG_TRIVIAL(error) << boost::format("Uncaught exception: %1%") % ex.what(); - throw; - } -} - -void GUI_App::post_init() -{ - assert(initialized()); - if (! this->initialized()) - throw Slic3r::RuntimeError("Calling post_init() while not yet initialized"); - - if (this->init_params->start_as_gcodeviewer) { - if (! this->init_params->input_files.empty()) - this->plater()->load_gcode(wxString::FromUTF8(this->init_params->input_files[0].c_str())); - } - else { - if (! this->init_params->preset_substitutions.empty()) - show_substitutions_info(this->init_params->preset_substitutions); - -#if 0 - // Load the cummulative config over the currently active profiles. - //FIXME if multiple configs are loaded, only the last one will have an effect. - // We need to decide what to do about loading of separate presets (just print preset, just filament preset etc). - // As of now only the full configs are supported here. - if (!m_print_config.empty()) - this->gui->mainframe->load_config(m_print_config); -#endif - if (! this->init_params->load_configs.empty()) - // Load the last config to give it a name at the UI. The name of the preset may be later - // changed by loading an AMF or 3MF. - //FIXME this is not strictly correct, as one may pass a print/filament/printer profile here instead of a full config. - this->mainframe->load_config_file(this->init_params->load_configs.back()); - // If loading a 3MF file, the config is loaded from the last one. - if (!this->init_params->input_files.empty()) { - const std::vector res = this->plater()->load_files(this->init_params->input_files, true, true); - if (!res.empty() && this->init_params->input_files.size() == 1) { - // Update application titlebar when opening a project file - const std::string& filename = this->init_params->input_files.front(); - if (boost::algorithm::iends_with(filename, ".amf") || - boost::algorithm::iends_with(filename, ".amf.xml") || - boost::algorithm::iends_with(filename, ".3mf")) - this->plater()->set_project_filename(from_u8(filename)); - } - } - if (! this->init_params->extra_config.empty()) - this->mainframe->load_config(this->init_params->extra_config); - } - - // show "Did you know" notification - if (app_config->get("show_hints") == "1" && ! is_gcode_viewer()) - plater_->get_notification_manager()->push_hint_notification(true); - - // The extra CallAfter() is needed because of Mac, where this is the only way - // to popup a modal dialog on start without screwing combo boxes. - // This is ugly but I honestly found no better way to do it. - // Neither wxShowEvent nor wxWindowCreateEvent work reliably. - if (this->preset_updater) { // G-Code Viewer does not initialize preset_updater. - if (! this->check_updates(false)) - // Configuration is not compatible and reconfigure was refused by the user. Application is closing. - return; - CallAfter([this] { - bool cw_showed = this->config_wizard_startup(); - this->preset_updater->sync(preset_bundle); - this->app_version_check(false); - if (! cw_showed) { - // The CallAfter is needed as well, without it, GL extensions did not show. - // Also, we only want to show this when the wizard does not, so the new user - // sees something else than "we want something" on the first start. - show_send_system_info_dialog_if_needed(); - } - }); - } - - // Set PrusaSlicer version and save to PrusaSlicer.ini or PrusaSlicerGcodeViewer.ini. - app_config->set("version", SLIC3R_VERSION); - app_config->save(); - -#ifdef _WIN32 - // Sets window property to mainframe so other instances can indentify it. - OtherInstanceMessageHandler::init_windows_properties(mainframe, m_instance_hash_int); -#endif //WIN32 -} - -IMPLEMENT_APP(GUI_App) - -GUI_App::GUI_App(EAppMode mode) - : wxApp() - , m_app_mode(mode) - , m_em_unit(10) - , m_imgui(new ImGuiWrapper()) - , m_removable_drive_manager(std::make_unique()) - , m_other_instance_message_handler(std::make_unique()) -{ - //app config initializes early becasuse it is used in instance checking in PrusaSlicer.cpp - this->init_app_config(); - // init app downloader after path to datadir is set - m_app_updater = std::make_unique(); -} - -GUI_App::~GUI_App() -{ - if (app_config != nullptr) - delete app_config; - - if (preset_bundle != nullptr) - delete preset_bundle; - - if (preset_updater != nullptr) - delete preset_updater; -} - -// If formatted for github, plaintext with OpenGL extensions enclosed into
. -// Otherwise HTML formatted for the system info dialog. -std::string GUI_App::get_gl_info(bool for_github) -{ - return OpenGLManager::get_gl_info().to_string(for_github); -} - -wxGLContext* GUI_App::init_glcontext(wxGLCanvas& canvas) -{ - return m_opengl_mgr.init_glcontext(canvas); -} - -bool GUI_App::init_opengl() -{ -#ifdef __linux__ - bool status = m_opengl_mgr.init_gl(); - m_opengl_initialized = true; - return status; -#else - return m_opengl_mgr.init_gl(); -#endif -} - -// gets path to PrusaSlicer.ini, returns semver from first line comment -static boost::optional parse_semver_from_ini(std::string path) -{ - std::ifstream stream(path); - std::stringstream buffer; - buffer << stream.rdbuf(); - std::string body = buffer.str(); - size_t start = body.find("PrusaSlicer "); - if (start == std::string::npos) - return boost::none; - body = body.substr(start + 12); - size_t end = body.find_first_of(" \n"); - if (end < body.size()) - body.resize(end); - return Semver::parse(body); -} - -void GUI_App::init_app_config() -{ - // Profiles for the alpha are stored into the PrusaSlicer-alpha directory to not mix with the current release. -// SetAppName(SLIC3R_APP_KEY); - SetAppName(SLIC3R_APP_KEY "-alpha"); -// SetAppName(SLIC3R_APP_KEY "-beta"); - - -// SetAppDisplayName(SLIC3R_APP_NAME); - - // Set the Slic3r data directory at the Slic3r XS module. - // Unix: ~/ .Slic3r - // Windows : "C:\Users\username\AppData\Roaming\Slic3r" or "C:\Documents and Settings\username\Application Data\Slic3r" - // Mac : "~/Library/Application Support/Slic3r" - - if (data_dir().empty()) { - #ifndef __linux__ - set_data_dir(wxStandardPaths::Get().GetUserDataDir().ToUTF8().data()); - #else - // Since version 2.3, config dir on Linux is in ${XDG_CONFIG_HOME}. - // https://github.com/prusa3d/PrusaSlicer/issues/2911 - wxString dir; - if (! wxGetEnv(wxS("XDG_CONFIG_HOME"), &dir) || dir.empty() ) - dir = wxFileName::GetHomeDir() + wxS("/.config"); - set_data_dir((dir + "/" + GetAppName()).ToUTF8().data()); - #endif - } else { - m_datadir_redefined = true; - } - - if (!app_config) - app_config = new AppConfig(is_editor() ? AppConfig::EAppMode::Editor : AppConfig::EAppMode::GCodeViewer); - - // load settings - m_app_conf_exists = app_config->exists(); - if (m_app_conf_exists) { - std::string error = app_config->load(); - if (!error.empty()) { - // Error while parsing config file. We'll customize the error message and rethrow to be displayed. - if (is_editor()) { - throw Slic3r::RuntimeError( - _u8L("Error parsing PrusaSlicer config file, it is probably corrupted. " - "Try to manually delete the file to recover from the error. Your user profiles will not be affected.") + - "\n\n" + app_config->config_path() + "\n\n" + error); - } - else { - throw Slic3r::RuntimeError( - _u8L("Error parsing PrusaGCodeViewer config file, it is probably corrupted. " - "Try to manually delete the file to recover from the error.") + - "\n\n" + app_config->config_path() + "\n\n" + error); - } - } - } -} - -// returns old config path to copy from if such exists, -// returns an empty string if such config path does not exists or if it cannot be loaded. -std::string GUI_App::check_older_app_config(Semver current_version, bool backup) -{ - std::string older_data_dir_path; - - // If the config folder is redefined - do not check - if (m_datadir_redefined) - return {}; - - // find other version app config (alpha / beta / release) - std::string config_path = app_config->config_path(); - boost::filesystem::path parent_file_path(config_path); - std::string filename = parent_file_path.filename().string(); - parent_file_path.remove_filename().remove_filename(); - - std::vector candidates; - - if (SLIC3R_APP_KEY "-alpha" != GetAppName()) candidates.emplace_back(parent_file_path / SLIC3R_APP_KEY "-alpha" / filename); - if (SLIC3R_APP_KEY "-beta" != GetAppName()) candidates.emplace_back(parent_file_path / SLIC3R_APP_KEY "-beta" / filename); - if (SLIC3R_APP_KEY != GetAppName()) candidates.emplace_back(parent_file_path / SLIC3R_APP_KEY / filename); - - Semver last_semver = current_version; - for (const auto& candidate : candidates) { - if (boost::filesystem::exists(candidate)) { - // parse - boost::optionalother_semver = parse_semver_from_ini(candidate.string()); - if (other_semver && *other_semver > last_semver) { - last_semver = *other_semver; - older_data_dir_path = candidate.parent_path().string(); - } - } - } - if (older_data_dir_path.empty()) - return {}; - BOOST_LOG_TRIVIAL(info) << "last app config file used: " << older_data_dir_path; - // ask about using older data folder - - InfoDialog msg(nullptr - , format_wxstr(_L("You are opening %1% version %2%."), SLIC3R_APP_NAME, SLIC3R_VERSION) - , backup ? - format_wxstr(_L( - "The active configuration was created by %1% %2%," - "\nwhile a newer configuration was found in %3%" - "\ncreated by %1% %4%." - "\n\nShall the newer configuration be imported?" - "\nIf so, your active configuration will be backed up before importing the new configuration." - ) - , SLIC3R_APP_NAME, current_version.to_string(), older_data_dir_path, last_semver.to_string()) - : format_wxstr(_L( - "An existing configuration was found in %3%" - "\ncreated by %1% %2%." - "\n\nShall this configuration be imported?" - ) - , SLIC3R_APP_NAME, last_semver.to_string(), older_data_dir_path) - , true, wxYES_NO); - - if (backup) { - msg.SetButtonLabel(wxID_YES, _L("Import")); - msg.SetButtonLabel(wxID_NO, _L("Don't import")); - } - - if (msg.ShowModal() == wxID_YES) { - std::string snapshot_id; - if (backup) { - const Config::Snapshot* snapshot{ nullptr }; - if (! GUI::Config::take_config_snapshot_cancel_on_error(*app_config, Config::Snapshot::SNAPSHOT_USER, "", - _u8L("Continue and import newer configuration?"), &snapshot)) - return {}; - if (snapshot) { - // Save snapshot ID before loading the alternate AppConfig, as loading the alternate AppConfig may fail. - snapshot_id = snapshot->id; - assert(! snapshot_id.empty()); - app_config->set("on_snapshot", snapshot_id); - } else - BOOST_LOG_TRIVIAL(error) << "Failed to take congiguration snapshot"; - } - - // load app config from older file - std::string error = app_config->load((boost::filesystem::path(older_data_dir_path) / filename).string()); - if (!error.empty()) { - // Error while parsing config file. We'll customize the error message and rethrow to be displayed. - if (is_editor()) { - throw Slic3r::RuntimeError( - _u8L("Error parsing PrusaSlicer config file, it is probably corrupted. " - "Try to manually delete the file to recover from the error. Your user profiles will not be affected.") + - "\n\n" + app_config->config_path() + "\n\n" + error); - } - else { - throw Slic3r::RuntimeError( - _u8L("Error parsing PrusaGCodeViewer config file, it is probably corrupted. " - "Try to manually delete the file to recover from the error.") + - "\n\n" + app_config->config_path() + "\n\n" + error); - } - } - if (!snapshot_id.empty()) - app_config->set("on_snapshot", snapshot_id); - m_app_conf_exists = true; - return older_data_dir_path; - } - return {}; -} - -void GUI_App::init_single_instance_checker(const std::string &name, const std::string &path) -{ - BOOST_LOG_TRIVIAL(debug) << "init wx instance checker " << name << " "<< path; - m_single_instance_checker = std::make_unique(boost::nowide::widen(name), boost::nowide::widen(path)); -} - -bool GUI_App::OnInit() -{ - try { - return on_init_inner(); - } catch (const std::exception&) { - generic_exception_handle(); - return false; - } -} - -bool GUI_App::on_init_inner() -{ - // Set initialization of image handlers before any UI actions - See GH issue #7469 - wxInitAllImageHandlers(); - -#if defined(_WIN32) && ! defined(_WIN64) - // Win32 32bit build. - if (wxPlatformInfo::Get().GetArchName().substr(0, 2) == "64") { - RichMessageDialog dlg(nullptr, - _L("You are running a 32 bit build of PrusaSlicer on 64-bit Windows." - "\n32 bit build of PrusaSlicer will likely not be able to utilize all the RAM available in the system." - "\nPlease download and install a 64 bit build of PrusaSlicer from https://www.prusa3d.cz/prusaslicer/." - "\nDo you wish to continue?"), - "PrusaSlicer", wxICON_QUESTION | wxYES_NO); - if (dlg.ShowModal() != wxID_YES) - return false; - } -#endif // _WIN64 - - // Forcing back menu icons under gtk2 and gtk3. Solution is based on: - // https://docs.gtk.org/gtk3/class.Settings.html - // see also https://docs.wxwidgets.org/3.0/classwx_menu_item.html#a2b5d6bcb820b992b1e4709facbf6d4fb - // TODO: Find workaround for GTK4 -#if defined(__WXGTK20__) || defined(__WXGTK3__) - g_object_set (gtk_settings_get_default (), "gtk-menu-images", TRUE, NULL); -#endif - - // Verify resources path - const wxString resources_dir = from_u8(Slic3r::resources_dir()); - wxCHECK_MSG(wxDirExists(resources_dir), false, - wxString::Format("Resources path does not exist or is not a directory: %s", resources_dir)); - -#ifdef __linux__ - if (! check_old_linux_datadir(GetAppName())) { - std::cerr << "Quitting, user chose to move their data to new location." << std::endl; - return false; - } -#endif - - // Enable this to get the default Win32 COMCTRL32 behavior of static boxes. -// wxSystemOptions::SetOption("msw.staticbox.optimized-paint", 0); - // Enable this to disable Windows Vista themes for all wxNotebooks. The themes seem to lead to terrible - // performance when working on high resolution multi-display setups. -// wxSystemOptions::SetOption("msw.notebook.themed-background", 0); - -// Slic3r::debugf "wxWidgets version %s, Wx version %s\n", wxVERSION_STRING, wxVERSION; - - // !!! Initialization of UI settings as a language, application color mode, fonts... have to be done before first UI action. - // Like here, before the show InfoDialog in check_older_app_config() - - // If load_language() fails, the application closes. - load_language(wxString(), true); -#ifdef _MSW_DARK_MODE - bool init_dark_color_mode = app_config->get("dark_color_mode") == "1"; - bool init_sys_menu_enabled = app_config->get("sys_menu_enabled") == "1"; - NppDarkMode::InitDarkMode(init_dark_color_mode, init_sys_menu_enabled); -#endif - // initialize label colors and fonts - init_label_colours(); - init_fonts(); - - std::string older_data_dir_path; - if (m_app_conf_exists) { - if (app_config->orig_version().valid() && app_config->orig_version() < *Semver::parse(SLIC3R_VERSION)) - // Only copying configuration if it was saved with a newer slicer than the one currently running. - older_data_dir_path = check_older_app_config(app_config->orig_version(), true); - } else { - // No AppConfig exists, fresh install. Always try to copy from an alternate location, don't make backup of the current configuration. - older_data_dir_path = check_older_app_config(Semver(), false); - } - -#ifdef _MSW_DARK_MODE - // app_config can be updated in check_older_app_config(), so check if dark_color_mode and sys_menu_enabled was changed - if (bool new_dark_color_mode = app_config->get("dark_color_mode") == "1"; - init_dark_color_mode != new_dark_color_mode) { - NppDarkMode::SetDarkMode(new_dark_color_mode); - init_label_colours(); - update_label_colours_from_appconfig(); - } - if (bool new_sys_menu_enabled = app_config->get("sys_menu_enabled") == "1"; - init_sys_menu_enabled != new_sys_menu_enabled) - NppDarkMode::SetSystemMenuForApp(new_sys_menu_enabled); -#endif - - if (is_editor()) { - std::string msg = Http::tls_global_init(); - std::string ssl_cert_store = app_config->get("tls_accepted_cert_store_location"); - bool ssl_accept = app_config->get("tls_cert_store_accepted") == "yes" && ssl_cert_store == Http::tls_system_cert_store(); - - if (!msg.empty() && !ssl_accept) { - RichMessageDialog - dlg(nullptr, - wxString::Format(_L("%s\nDo you want to continue?"), msg), - "PrusaSlicer", wxICON_QUESTION | wxYES_NO); - dlg.ShowCheckBox(_L("Remember my choice")); - if (dlg.ShowModal() != wxID_YES) return false; - - app_config->set("tls_cert_store_accepted", - dlg.IsCheckBoxChecked() ? "yes" : "no"); - app_config->set("tls_accepted_cert_store_location", - dlg.IsCheckBoxChecked() ? Http::tls_system_cert_store() : ""); - } - } - - SplashScreen* scrn = nullptr; - if (app_config->get("show_splash_screen") == "1") { - // make a bitmap with dark grey banner on the left side - wxBitmap bmp = SplashScreen::MakeBitmap(wxBitmap(from_u8(var(is_editor() ? "splashscreen.jpg" : "splashscreen-gcodepreview.jpg")), wxBITMAP_TYPE_JPEG)); - - // Detect position (display) to show the splash screen - // Now this position is equal to the mainframe position - wxPoint splashscreen_pos = wxDefaultPosition; - bool default_splashscreen_pos = true; - if (app_config->has("window_mainframe") && app_config->get("restore_win_position") == "1") { - auto metrics = WindowMetrics::deserialize(app_config->get("window_mainframe")); - default_splashscreen_pos = metrics == boost::none; - if (!default_splashscreen_pos) - splashscreen_pos = metrics->get_rect().GetPosition(); - } - - if (!default_splashscreen_pos) { - // workaround for crash related to the positioning of the window on secondary monitor - get_app_config()->set("restore_win_position", "crashed_at_splashscreen_pos"); - get_app_config()->save(); - } - - // create splash screen with updated bmp - scrn = new SplashScreen(bmp.IsOk() ? bmp : create_scaled_bitmap("PrusaSlicer", nullptr, 400), - wxSPLASH_CENTRE_ON_SCREEN | wxSPLASH_TIMEOUT, 4000, splashscreen_pos); - - if (!default_splashscreen_pos) - // revert "restore_win_position" value if application wasn't crashed - get_app_config()->set("restore_win_position", "1"); -#ifndef __linux__ - wxYield(); -#endif - scrn->SetText(_L("Loading configuration")+ dots); - } - - preset_bundle = new PresetBundle(); - - // just checking for existence of Slic3r::data_dir is not enough : it may be an empty directory - // supplied as argument to --datadir; in that case we should still run the wizard - preset_bundle->setup_directories(); - - if (! older_data_dir_path.empty()) { - preset_bundle->import_newer_configs(older_data_dir_path); - app_config->save(); - } - - if (is_editor()) { -#ifdef __WXMSW__ - if (app_config->get("associate_3mf") == "1") - associate_3mf_files(); - if (app_config->get("associate_stl") == "1") - associate_stl_files(); -#endif // __WXMSW__ - - preset_updater = new PresetUpdater(); - Bind(EVT_SLIC3R_VERSION_ONLINE, &GUI_App::on_version_read, this); - Bind(EVT_SLIC3R_EXPERIMENTAL_VERSION_ONLINE, [this](const wxCommandEvent& evt) { - if (this->plater_ != nullptr && app_config->get("notify_release") == "all") { - std::string evt_string = into_u8(evt.GetString()); - if (*Semver::parse(SLIC3R_VERSION) < *Semver::parse(evt_string)) { - auto notif_type = (evt_string.find("beta") != std::string::npos ? NotificationType::NewBetaAvailable : NotificationType::NewAlphaAvailable); - this->plater_->get_notification_manager()->push_notification( notif_type - , NotificationManager::NotificationLevel::ImportantNotificationLevel - , Slic3r::format(_u8L("New prerelease version %1% is available."), evt_string) - , _u8L("See Releases page.") - , [](wxEvtHandler* evnthndlr) {wxGetApp().open_browser_with_warning_dialog("https://github.com/prusa3d/PrusaSlicer/releases"); return true; } - ); - } - } - }); - Bind(EVT_SLIC3R_APP_DOWNLOAD_PROGRESS, [this](const wxCommandEvent& evt) { - //lm:This does not force a render. The progress bar only updateswhen the mouse is moved. - if (this->plater_ != nullptr) - this->plater_->get_notification_manager()->set_download_progress_percentage((float)std::stoi(into_u8(evt.GetString())) / 100.f ); - }); - - Bind(EVT_SLIC3R_APP_DOWNLOAD_FAILED, [this](const wxCommandEvent& evt) { - if (this->plater_ != nullptr) - this->plater_->get_notification_manager()->close_notification_of_type(NotificationType::AppDownload); - if(!evt.GetString().IsEmpty()) - show_error(nullptr, evt.GetString()); - }); - - Bind(EVT_SLIC3R_APP_OPEN_FAILED, [](const wxCommandEvent& evt) { - show_error(nullptr, evt.GetString()); - }); - } - else { -#ifdef __WXMSW__ - if (app_config->get("associate_gcode") == "1") - associate_gcode_files(); -#endif // __WXMSW__ - } - - // Suppress the '- default -' presets. - preset_bundle->set_default_suppressed(app_config->get("no_defaults") == "1"); - try { - // Enable all substitutions (in both user and system profiles), but log the substitutions in user profiles only. - // If there are substitutions in system profiles, then a "reconfigure" event shall be triggered, which will force - // installation of a compatible system preset, thus nullifying the system preset substitutions. - init_params->preset_substitutions = preset_bundle->load_presets(*app_config, ForwardCompatibilitySubstitutionRule::EnableSystemSilent); - } catch (const std::exception &ex) { - show_error(nullptr, ex.what()); - } - -#ifdef WIN32 -#if !wxVERSION_EQUAL_OR_GREATER_THAN(3,1,3) - register_win32_dpi_event(); -#endif // !wxVERSION_EQUAL_OR_GREATER_THAN - register_win32_device_notification_event(); -#endif // WIN32 - - // Let the libslic3r know the callback, which will translate messages on demand. - Slic3r::I18N::set_translate_callback(libslic3r_translate_callback); - - // application frame - if (scrn && is_editor()) - scrn->SetText(_L("Preparing settings tabs") + dots); - - mainframe = new MainFrame(); - // hide settings tabs after first Layout - if (is_editor()) - mainframe->select_tab(size_t(0)); - - sidebar().obj_list()->init_objects(); // propagate model objects to object list -// update_mode(); // !!! do that later - SetTopWindow(mainframe); - - plater_->init_notification_manager(); - - m_printhost_job_queue.reset(new PrintHostJobQueue(mainframe->printhost_queue_dlg())); - - if (is_gcode_viewer()) { - mainframe->update_layout(); - if (plater_ != nullptr) - // ensure the selected technology is ptFFF - plater_->set_printer_technology(ptFFF); - } - else - load_current_presets(); - - // Save the active profiles as a "saved into project". - update_saved_preset_from_current_preset(); - - if (plater_ != nullptr) { - // Save the names of active presets and project specific config into ProjectDirtyStateManager. - plater_->reset_project_dirty_initial_presets(); - // Update Project dirty state, update application title bar. - plater_->update_project_dirty_from_presets(); - } - - mainframe->Show(true); - - obj_list()->set_min_height(); - - update_mode(); // update view mode after fix of the object_list size - -#ifdef __APPLE__ - other_instance_message_handler()->bring_instance_forward(); -#endif //__APPLE__ - - Bind(wxEVT_IDLE, [this](wxIdleEvent& event) - { - if (! plater_) - return; - - this->obj_manipul()->update_if_dirty(); - - // An ugly solution to GH #5537 in which GUI_App::init_opengl (normally called from events wxEVT_PAINT - // and wxEVT_SET_FOCUS before GUI_App::post_init is called) wasn't called before GUI_App::post_init and OpenGL wasn't initialized. -#ifdef __linux__ - if (! m_post_initialized && m_opengl_initialized) { -#else - if (! m_post_initialized) { -#endif - m_post_initialized = true; -#ifdef WIN32 - this->mainframe->register_win32_callbacks(); -#endif - this->post_init(); - } - - if (m_post_initialized && app_config->dirty() && app_config->get("autosave") == "1") - app_config->save(); - }); - - m_initialized = true; - - if (const std::string& crash_reason = app_config->get("restore_win_position"); - boost::starts_with(crash_reason,"crashed")) - { - wxString preferences_item = _L("Restore window position on start"); - InfoDialog dialog(nullptr, - _L("PrusaSlicer started after a crash"), - format_wxstr(_L("PrusaSlicer crashed last time when attempting to set window position.\n" - "We are sorry for the inconvenience, it unfortunately happens with certain multiple-monitor setups.\n" - "More precise reason for the crash: \"%1%\".\n" - "For more information see our GitHub issue tracker: \"%2%\" and \"%3%\"\n\n" - "To avoid this problem, consider disabling \"%4%\" in \"Preferences\". " - "Otherwise, the application will most likely crash again next time."), - "" + from_u8(crash_reason) + "", - "#2939", - "#5573", - "" + preferences_item + ""), - true, wxYES_NO); - - dialog.SetButtonLabel(wxID_YES, format_wxstr(_L("Disable \"%1%\""), preferences_item)); - dialog.SetButtonLabel(wxID_NO, format_wxstr(_L("Leave \"%1%\" enabled") , preferences_item)); - - auto answer = dialog.ShowModal(); - if (answer == wxID_YES) - app_config->set("restore_win_position", "0"); - else if (answer == wxID_NO) - app_config->set("restore_win_position", "1"); - app_config->save(); - } - - return true; -} - -unsigned GUI_App::get_colour_approx_luma(const wxColour &colour) -{ - double r = colour.Red(); - double g = colour.Green(); - double b = colour.Blue(); - - return std::round(std::sqrt( - r * r * .241 + - g * g * .691 + - b * b * .068 - )); -} - -bool GUI_App::dark_mode() -{ -#if __APPLE__ - // The check for dark mode returns false positive on 10.12 and 10.13, - // which allowed setting dark menu bar and dock area, which is - // is detected as dark mode. We must run on at least 10.14 where the - // proper dark mode was first introduced. - return wxPlatformInfo::Get().CheckOSVersion(10, 14) && mac_dark_mode(); -#else - return wxGetApp().app_config->get("dark_color_mode") == "1" ? true : check_dark_mode(); - //const unsigned luma = get_colour_approx_luma(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); - //return luma < 128; -#endif -} - -const wxColour GUI_App::get_label_default_clr_system() -{ - return dark_mode() ? wxColour(115, 220, 103) : wxColour(26, 132, 57); -} - -const wxColour GUI_App::get_label_default_clr_modified() -{ - return dark_mode() ? wxColour(253, 111, 40) : wxColour(252, 77, 1); -} - -void GUI_App::init_label_colours() -{ - m_color_label_modified = get_label_default_clr_modified(); - m_color_label_sys = get_label_default_clr_system(); - - bool is_dark_mode = dark_mode(); -#ifdef _WIN32 - m_color_label_default = is_dark_mode ? wxColour(250, 250, 250): wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT); - m_color_highlight_label_default = is_dark_mode ? wxColour(230, 230, 230): wxSystemSettings::GetColour(/*wxSYS_COLOUR_HIGHLIGHTTEXT*/wxSYS_COLOUR_WINDOWTEXT); - m_color_highlight_default = is_dark_mode ? wxColour(78, 78, 78) : wxSystemSettings::GetColour(wxSYS_COLOUR_3DLIGHT); - m_color_hovered_btn_label = is_dark_mode ? wxColour(253, 111, 40) : wxColour(252, 77, 1); - m_color_default_btn_label = is_dark_mode ? wxColour(255, 181, 100): wxColour(203, 61, 0); - m_color_selected_btn_bg = is_dark_mode ? wxColour(95, 73, 62) : wxColour(228, 220, 216); -#else - m_color_label_default = wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT); -#endif - m_color_window_default = is_dark_mode ? wxColour(43, 43, 43) : wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW); -} - -void GUI_App::update_label_colours_from_appconfig() -{ - if (app_config->has("label_clr_sys")) { - auto str = app_config->get("label_clr_sys"); - if (str != "") - m_color_label_sys = wxColour(str); - } - - if (app_config->has("label_clr_modified")) { - auto str = app_config->get("label_clr_modified"); - if (str != "") - m_color_label_modified = wxColour(str); - } -} - -void GUI_App::update_label_colours() -{ - for (Tab* tab : tabs_list) - tab->update_label_colours(); -} - -#ifdef _WIN32 -static bool is_focused(HWND hWnd) -{ - HWND hFocusedWnd = ::GetFocus(); - return hFocusedWnd && hWnd == hFocusedWnd; -} - -static bool is_default(wxWindow* win) -{ - wxTopLevelWindow* tlw = find_toplevel_parent(win); - if (!tlw) - return false; - - return win == tlw->GetDefaultItem(); -} -#endif - -void GUI_App::UpdateDarkUI(wxWindow* window, bool highlited/* = false*/, bool just_font/* = false*/) -{ -#ifdef _WIN32 - bool is_focused_button = false; - bool is_default_button = false; - if (wxButton* btn = dynamic_cast(window)) { - if (!(btn->GetWindowStyle() & wxNO_BORDER)) { - btn->SetWindowStyle(btn->GetWindowStyle() | wxNO_BORDER); - highlited = true; - } - // button marking - { - auto mark_button = [this, btn, highlited](const bool mark) { - if (btn->GetLabel().IsEmpty()) - btn->SetBackgroundColour(mark ? m_color_selected_btn_bg : highlited ? m_color_highlight_default : m_color_window_default); - else - btn->SetForegroundColour(mark ? m_color_hovered_btn_label : (is_default(btn) ? m_color_default_btn_label : m_color_label_default)); - btn->Refresh(); - btn->Update(); - }; - - // hovering - btn->Bind(wxEVT_ENTER_WINDOW, [mark_button](wxMouseEvent& event) { mark_button(true); event.Skip(); }); - btn->Bind(wxEVT_LEAVE_WINDOW, [mark_button, btn](wxMouseEvent& event) { mark_button(is_focused(btn->GetHWND())); event.Skip(); }); - // focusing - btn->Bind(wxEVT_SET_FOCUS, [mark_button](wxFocusEvent& event) { mark_button(true); event.Skip(); }); - btn->Bind(wxEVT_KILL_FOCUS, [mark_button](wxFocusEvent& event) { mark_button(false); event.Skip(); }); - - is_focused_button = is_focused(btn->GetHWND()); - is_default_button = is_default(btn); - if (is_focused_button || is_default_button) - mark_button(is_focused_button); - } - } - else if (wxTextCtrl* text = dynamic_cast(window)) { - if (text->GetBorder() != wxBORDER_SIMPLE) - text->SetWindowStyle(text->GetWindowStyle() | wxBORDER_SIMPLE); - } - else if (wxCheckListBox* list = dynamic_cast(window)) { - list->SetWindowStyle(list->GetWindowStyle() | wxBORDER_SIMPLE); - list->SetBackgroundColour(highlited ? m_color_highlight_default : m_color_window_default); - for (size_t i = 0; i < list->GetCount(); i++) - if (wxOwnerDrawn* item = list->GetItem(i)) { - item->SetBackgroundColour(highlited ? m_color_highlight_default : m_color_window_default); - item->SetTextColour(m_color_label_default); - } - return; - } - else if (dynamic_cast(window)) - window->SetWindowStyle(window->GetWindowStyle() | wxBORDER_SIMPLE); - - if (!just_font) - window->SetBackgroundColour(highlited ? m_color_highlight_default : m_color_window_default); - if (!is_focused_button && !is_default_button) - window->SetForegroundColour(m_color_label_default); -#endif -} - -// recursive function for scaling fonts for all controls in Window -#ifdef _WIN32 -static void update_dark_children_ui(wxWindow* window, bool just_buttons_update = false) -{ - bool is_btn = dynamic_cast(window) != nullptr; - if (!(just_buttons_update && !is_btn)) - wxGetApp().UpdateDarkUI(window, is_btn); - - auto children = window->GetChildren(); - for (auto child : children) { - update_dark_children_ui(child); - } -} -#endif - -// Note: Don't use this function for Dialog contains ScalableButtons -void GUI_App::UpdateDlgDarkUI(wxDialog* dlg, bool just_buttons_update/* = false*/) -{ -#ifdef _WIN32 - update_dark_children_ui(dlg, just_buttons_update); -#endif -} -void GUI_App::UpdateDVCDarkUI(wxDataViewCtrl* dvc, bool highlited/* = false*/) -{ -#ifdef _WIN32 - UpdateDarkUI(dvc, highlited ? dark_mode() : false); -#ifdef _MSW_DARK_MODE - dvc->RefreshHeaderDarkMode(&m_normal_font); -#endif //_MSW_DARK_MODE - if (dvc->HasFlag(wxDV_ROW_LINES)) - dvc->SetAlternateRowColour(m_color_highlight_default); - if (dvc->GetBorder() != wxBORDER_SIMPLE) - dvc->SetWindowStyle(dvc->GetWindowStyle() | wxBORDER_SIMPLE); -#endif -} - -void GUI_App::UpdateAllStaticTextDarkUI(wxWindow* parent) -{ -#ifdef _WIN32 - wxGetApp().UpdateDarkUI(parent); - - auto children = parent->GetChildren(); - for (auto child : children) { - if (dynamic_cast(child)) - child->SetForegroundColour(m_color_label_default); - } -#endif -} - -void GUI_App::init_fonts() -{ - m_small_font = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT); - m_bold_font = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT).Bold(); - m_normal_font = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT); - -#ifdef __WXMAC__ - m_small_font.SetPointSize(11); - m_bold_font.SetPointSize(13); -#endif /*__WXMAC__*/ - - // wxSYS_OEM_FIXED_FONT and wxSYS_ANSI_FIXED_FONT use the same as - // DEFAULT in wxGtk. Use the TELETYPE family as a work-around - m_code_font = wxFont(wxFontInfo().Family(wxFONTFAMILY_TELETYPE)); - m_code_font.SetPointSize(m_normal_font.GetPointSize()); -} - -void GUI_App::update_fonts(const MainFrame *main_frame) -{ - /* Only normal and bold fonts are used for an application rescale, - * because of under MSW small and normal fonts are the same. - * To avoid same rescaling twice, just fill this values - * from rescaled MainFrame - */ - if (main_frame == nullptr) - main_frame = this->mainframe; - m_normal_font = main_frame->normal_font(); - m_small_font = m_normal_font; - m_bold_font = main_frame->normal_font().Bold(); - m_link_font = m_bold_font.Underlined(); - m_em_unit = main_frame->em_unit(); - m_code_font.SetPointSize(m_normal_font.GetPointSize()); -} - -void GUI_App::set_label_clr_modified(const wxColour& clr) -{ - if (m_color_label_modified == clr) - return; - m_color_label_modified = clr; - const std::string str = encode_color(ColorRGB(clr.Red(), clr.Green(), clr.Blue())); - app_config->set("label_clr_modified", str); - app_config->save(); -} - -void GUI_App::set_label_clr_sys(const wxColour& clr) -{ - if (m_color_label_sys == clr) - return; - m_color_label_sys = clr; - const std::string str = encode_color(ColorRGB(clr.Red(), clr.Green(), clr.Blue())); - app_config->set("label_clr_sys", str); - app_config->save(); -} - -bool GUI_App::tabs_as_menu() const -{ - return app_config->get("tabs_as_menu") == "1"; // || dark_mode(); -} - -wxSize GUI_App::get_min_size() const -{ - return wxSize(76*m_em_unit, 49 * m_em_unit); -} - -float GUI_App::toolbar_icon_scale(const bool is_limited/* = false*/) const -{ -#ifdef __APPLE__ - const float icon_sc = 1.0f; // for Retina display will be used its own scale -#else - const float icon_sc = m_em_unit*0.1f; -#endif // __APPLE__ - - const std::string& use_val = app_config->get("use_custom_toolbar_size"); - const std::string& val = app_config->get("custom_toolbar_size"); - const std::string& auto_val = app_config->get("auto_toolbar_size"); - - if (val.empty() || auto_val.empty() || use_val.empty()) - return icon_sc; - - int int_val = use_val == "0" ? 100 : atoi(val.c_str()); - // correct value in respect to auto_toolbar_size - int_val = std::min(atoi(auto_val.c_str()), int_val); - - if (is_limited && int_val < 50) - int_val = 50; - - return 0.01f * int_val * icon_sc; -} - -void GUI_App::set_auto_toolbar_icon_scale(float scale) const -{ -#ifdef __APPLE__ - const float icon_sc = 1.0f; // for Retina display will be used its own scale -#else - const float icon_sc = m_em_unit * 0.1f; -#endif // __APPLE__ - - long int_val = std::min(int(std::lround(scale / icon_sc * 100)), 100); - std::string val = std::to_string(int_val); - - app_config->set("auto_toolbar_size", val); -} - -// check user printer_presets for the containing information about "Print Host upload" -void GUI_App::check_printer_presets() -{ - std::vector preset_names = PhysicalPrinter::presets_with_print_host_information(preset_bundle->printers); - if (preset_names.empty()) - return; - - wxString msg_text = _L("You have the following presets with saved options for \"Print Host upload\"") + ":"; - for (const std::string& preset_name : preset_names) - msg_text += "\n \"" + from_u8(preset_name) + "\","; - msg_text.RemoveLast(); - msg_text += "\n\n" + _L("But since this version of PrusaSlicer we don't show this information in Printer Settings anymore.\n" - "Settings will be available in physical printers settings.") + "\n\n" + - _L("By default new Printer devices will be named as \"Printer N\" during its creation.\n" - "Note: This name can be changed later from the physical printers settings"); - - //wxMessageDialog(nullptr, msg_text, _L("Information"), wxOK | wxICON_INFORMATION).ShowModal(); - MessageDialog(nullptr, msg_text, _L("Information"), wxOK | wxICON_INFORMATION).ShowModal(); - - preset_bundle->physical_printers.load_printers_from_presets(preset_bundle->printers); -} - -void GUI_App::recreate_GUI(const wxString& msg_name) -{ - m_is_recreating_gui = true; - - mainframe->shutdown(); - - wxProgressDialog dlg(msg_name, msg_name, 100, nullptr, wxPD_AUTO_HIDE); - dlg.Pulse(); - dlg.Update(10, _L("Recreating") + dots); - - MainFrame *old_main_frame = mainframe; - mainframe = new MainFrame(); - if (is_editor()) - // hide settings tabs after first Layout - mainframe->select_tab(size_t(0)); - // Propagate model objects to object list. - sidebar().obj_list()->init_objects(); - SetTopWindow(mainframe); - - dlg.Update(30, _L("Recreating") + dots); - old_main_frame->Destroy(); - - dlg.Update(80, _L("Loading of current presets") + dots); - m_printhost_job_queue.reset(new PrintHostJobQueue(mainframe->printhost_queue_dlg())); - load_current_presets(); - mainframe->Show(true); - - dlg.Update(90, _L("Loading of a mode view") + dots); - - obj_list()->set_min_height(); - update_mode(); - - // #ys_FIXME_delete_after_testing Do we still need this ? -// CallAfter([]() { -// // Run the config wizard, don't offer the "reset user profile" checkbox. -// config_wizard_startup(true); -// }); - - m_is_recreating_gui = false; -} - -void GUI_App::system_info() -{ - SysInfoDialog dlg; - dlg.ShowModal(); -} - -void GUI_App::keyboard_shortcuts() -{ - KBShortcutsDialog dlg; - dlg.ShowModal(); -} - -// static method accepting a wxWindow object as first parameter -bool GUI_App::catch_error(std::function cb, - // wxMessageDialog* message_dialog, - const std::string& err /*= ""*/) -{ - if (!err.empty()) { - if (cb) - cb(); - // if (message_dialog) - // message_dialog->(err, "Error", wxOK | wxICON_ERROR); - show_error(/*this*/nullptr, err); - return true; - } - return false; -} - -// static method accepting a wxWindow object as first parameter -void fatal_error(wxWindow* parent) -{ - show_error(parent, ""); - // exit 1; // #ys_FIXME -} - -#ifdef _WIN32 - -#ifdef _MSW_DARK_MODE -static void update_scrolls(wxWindow* window) -{ - wxWindowList::compatibility_iterator node = window->GetChildren().GetFirst(); - while (node) - { - wxWindow* win = node->GetData(); - if (dynamic_cast(win) || - dynamic_cast(win) || - dynamic_cast(win)) - NppDarkMode::SetDarkExplorerTheme(win->GetHWND()); - - update_scrolls(win); - node = node->GetNext(); - } -} -#endif //_MSW_DARK_MODE - - -#ifdef _MSW_DARK_MODE -void GUI_App::force_menu_update() -{ - NppDarkMode::SetSystemMenuForApp(app_config->get("sys_menu_enabled") == "1"); -} -#endif //_MSW_DARK_MODE - -void GUI_App::force_colors_update() -{ -#ifdef _MSW_DARK_MODE - NppDarkMode::SetDarkMode(app_config->get("dark_color_mode") == "1"); - if (WXHWND wxHWND = wxToolTip::GetToolTipCtrl()) - NppDarkMode::SetDarkExplorerTheme((HWND)wxHWND); - NppDarkMode::SetDarkTitleBar(mainframe->GetHWND()); - NppDarkMode::SetDarkTitleBar(mainframe->m_settings_dialog.GetHWND()); -#endif //_MSW_DARK_MODE - m_force_colors_update = true; -} -#endif //_WIN32 - -// Called after the Preferences dialog is closed and the program settings are saved. -// Update the UI based on the current preferences. -void GUI_App::update_ui_from_settings() -{ - update_label_colours(); -#ifdef _WIN32 - // Upadte UI colors before Update UI from settings - if (m_force_colors_update) { - m_force_colors_update = false; - mainframe->force_color_changed(); - mainframe->diff_dialog.force_color_changed(); - mainframe->preferences_dialog->force_color_changed(); - mainframe->printhost_queue_dlg()->force_color_changed(); -#ifdef _MSW_DARK_MODE - update_scrolls(mainframe); - if (mainframe->is_dlg_layout()) { - // update for tabs bar - UpdateDarkUI(&mainframe->m_settings_dialog); - mainframe->m_settings_dialog.Fit(); - mainframe->m_settings_dialog.Refresh(); - // update scrollbars - update_scrolls(&mainframe->m_settings_dialog); - } -#endif //_MSW_DARK_MODE - } -#endif - mainframe->update_ui_from_settings(); -} - -void GUI_App::persist_window_geometry(wxTopLevelWindow *window, bool default_maximized) -{ - const std::string name = into_u8(window->GetName()); - - window->Bind(wxEVT_CLOSE_WINDOW, [=](wxCloseEvent &event) { - window_pos_save(window, name); - event.Skip(); - }); - - window_pos_restore(window, name, default_maximized); - - on_window_geometry(window, [=]() { - window_pos_sanitize(window); - }); -} - -void GUI_App::load_project(wxWindow *parent, wxString& input_file) const -{ - input_file.Clear(); - wxFileDialog dialog(parent ? parent : GetTopWindow(), - _L("Choose one file (3MF/AMF):"), - app_config->get_last_dir(), "", - file_wildcards(FT_PROJECT), wxFD_OPEN | wxFD_FILE_MUST_EXIST); - - if (dialog.ShowModal() == wxID_OK) - input_file = dialog.GetPath(); -} - -void GUI_App::import_model(wxWindow *parent, wxArrayString& input_files) const -{ - input_files.Clear(); - wxFileDialog dialog(parent ? parent : GetTopWindow(), - _L("Choose one or more files (STL/OBJ/AMF/3MF/PRUSA):"), - from_u8(app_config->get_last_dir()), "", - file_wildcards(FT_MODEL), wxFD_OPEN | wxFD_MULTIPLE | wxFD_FILE_MUST_EXIST); - - if (dialog.ShowModal() == wxID_OK) - dialog.GetPaths(input_files); -} - -void GUI_App::load_gcode(wxWindow* parent, wxString& input_file) const -{ - input_file.Clear(); - wxFileDialog dialog(parent ? parent : GetTopWindow(), - _L("Choose one file (GCODE/.GCO/.G/.ngc/NGC):"), - app_config->get_last_dir(), "", - file_wildcards(FT_GCODE), wxFD_OPEN | wxFD_FILE_MUST_EXIST); - - if (dialog.ShowModal() == wxID_OK) - input_file = dialog.GetPath(); -} - -bool GUI_App::switch_language() -{ - if (select_language()) { - recreate_GUI(_L("Changing of an application language") + dots); - return true; - } else { - return false; - } -} - -#ifdef __linux__ -static const wxLanguageInfo* linux_get_existing_locale_language(const wxLanguageInfo* language, - const wxLanguageInfo* system_language) -{ - constexpr size_t max_len = 50; - char path[max_len] = ""; - std::vector locales; - const std::string lang_prefix = into_u8(language->CanonicalName.BeforeFirst('_')); - - // Call locale -a so we can parse the output to get the list of available locales - // We expect lines such as "en_US.utf8". Pick ones starting with the language code - // we are switching to. Lines with different formatting will be removed later. - FILE* fp = popen("locale -a", "r"); - if (fp != NULL) { - while (fgets(path, max_len, fp) != NULL) { - std::string line(path); - line = line.substr(0, line.find('\n')); - if (boost::starts_with(line, lang_prefix)) - locales.push_back(line); - } - pclose(fp); - } - - // locales now contain all candidates for this language. - // Sort them so ones containing anything about UTF-8 are at the end. - std::sort(locales.begin(), locales.end(), [](const std::string& a, const std::string& b) - { - auto has_utf8 = [](const std::string & s) { - auto S = boost::to_upper_copy(s); - return S.find("UTF8") != std::string::npos || S.find("UTF-8") != std::string::npos; - }; - return ! has_utf8(a) && has_utf8(b); - }); - - // Remove the suffix behind a dot, if there is one. - for (std::string& s : locales) - s = s.substr(0, s.find(".")); - - // We just hope that dear Linux "locale -a" returns country codes - // in ISO 3166-1 alpha-2 code (two letter) format. - // https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes - // To be sure, remove anything not looking as expected - // (any number of lowercase letters, underscore, two uppercase letters). - locales.erase(std::remove_if(locales.begin(), - locales.end(), - [](const std::string& s) { - return ! std::regex_match(s, - std::regex("^[a-z]+_[A-Z]{2}$")); - }), - locales.end()); - - // Is there a candidate matching a country code of a system language? Move it to the end, - // while maintaining the order of matches, so that the best match ends up at the very end. - std::string system_country = "_" + into_u8(system_language->CanonicalName.AfterFirst('_')).substr(0, 2); - int cnt = locales.size(); - for (int i=0; iempty()) { - const std::string &locale = *it; - const wxLanguageInfo* lang = wxLocale::FindLanguageInfo(from_u8(locale)); - if (wxLocale::IsAvailable(lang->Language)) - return lang; - } - return language; -} -#endif - -int GUI_App::GetSingleChoiceIndex(const wxString& message, - const wxString& caption, - const wxArrayString& choices, - int initialSelection) -{ -#ifdef _WIN32 - wxSingleChoiceDialog dialog(nullptr, message, caption, choices); - wxGetApp().UpdateDlgDarkUI(&dialog); - - dialog.SetSelection(initialSelection); - return dialog.ShowModal() == wxID_OK ? dialog.GetSelection() : -1; -#else - return wxGetSingleChoiceIndex(message, caption, choices, initialSelection); -#endif -} - -// select language from the list of installed languages -bool GUI_App::select_language() -{ - wxArrayString translations = wxTranslations::Get()->GetAvailableTranslations(SLIC3R_APP_KEY); - std::vector language_infos; - language_infos.emplace_back(wxLocale::GetLanguageInfo(wxLANGUAGE_ENGLISH)); - for (size_t i = 0; i < translations.GetCount(); ++ i) { - const wxLanguageInfo *langinfo = wxLocale::FindLanguageInfo(translations[i]); - if (langinfo != nullptr) - language_infos.emplace_back(langinfo); - } - sort_remove_duplicates(language_infos); - std::sort(language_infos.begin(), language_infos.end(), [](const wxLanguageInfo* l, const wxLanguageInfo* r) { return l->Description < r->Description; }); - - wxArrayString names; - names.Alloc(language_infos.size()); - - // Some valid language should be selected since the application start up. - const wxLanguage current_language = wxLanguage(m_wxLocale->GetLanguage()); - int init_selection = -1; - int init_selection_alt = -1; - int init_selection_default = -1; - for (size_t i = 0; i < language_infos.size(); ++ i) { - if (wxLanguage(language_infos[i]->Language) == current_language) - // The dictionary matches the active language and country. - init_selection = i; - else if ((language_infos[i]->CanonicalName.BeforeFirst('_') == m_wxLocale->GetCanonicalName().BeforeFirst('_')) || - // if the active language is Slovak, mark the Czech language as active. - (language_infos[i]->CanonicalName.BeforeFirst('_') == "cs" && m_wxLocale->GetCanonicalName().BeforeFirst('_') == "sk")) - // The dictionary matches the active language, it does not necessarily match the country. - init_selection_alt = i; - if (language_infos[i]->CanonicalName.BeforeFirst('_') == "en") - // This will be the default selection if the active language does not match any dictionary. - init_selection_default = i; - names.Add(language_infos[i]->Description); - } - if (init_selection == -1) - // This is the dictionary matching the active language. - init_selection = init_selection_alt; - if (init_selection != -1) - // This is the language to highlight in the choice dialog initially. - init_selection_default = init_selection; - - const long index = GetSingleChoiceIndex(_L("Select the language"), _L("Language"), names, init_selection_default); - // Try to load a new language. - if (index != -1 && (init_selection == -1 || init_selection != index)) { - const wxLanguageInfo *new_language_info = language_infos[index]; - if (this->load_language(new_language_info->CanonicalName, false)) { - // Save language at application config. - // Which language to save as the selected dictionary language? - // 1) Hopefully the language set to wxTranslations by this->load_language(), but that API is weird and we don't want to rely on its - // stability in the future: - // wxTranslations::Get()->GetBestTranslation(SLIC3R_APP_KEY, wxLANGUAGE_ENGLISH); - // 2) Current locale language may not match the dictionary name, see GH issue #3901 - // m_wxLocale->GetCanonicalName() - // 3) new_language_info->CanonicalName is a safe bet. It points to a valid dictionary name. - app_config->set("translation_language", new_language_info->CanonicalName.ToUTF8().data()); - app_config->save(); - return true; - } - } - - return false; -} - -// Load gettext translation files and activate them at the start of the application, -// based on the "translation_language" key stored in the application config. -bool GUI_App::load_language(wxString language, bool initial) -{ - if (initial) { - // There is a static list of lookup path prefixes in wxWidgets. Add ours. - wxFileTranslationsLoader::AddCatalogLookupPathPrefix(from_u8(localization_dir())); - // Get the active language from PrusaSlicer.ini, or empty string if the key does not exist. - language = app_config->get("translation_language"); - if (! language.empty()) - BOOST_LOG_TRIVIAL(trace) << boost::format("translation_language provided by PrusaSlicer.ini: %1%") % language; - - // Get the system language. - { - const wxLanguage lang_system = wxLanguage(wxLocale::GetSystemLanguage()); - if (lang_system != wxLANGUAGE_UNKNOWN) { - m_language_info_system = wxLocale::GetLanguageInfo(lang_system); - BOOST_LOG_TRIVIAL(trace) << boost::format("System language detected (user locales and such): %1%") % m_language_info_system->CanonicalName.ToUTF8().data(); - } - } - { - // Allocating a temporary locale will switch the default wxTranslations to its internal wxTranslations instance. - wxLocale temp_locale; - // Set the current translation's language to default, otherwise GetBestTranslation() may not work (see the wxWidgets source code). - wxTranslations::Get()->SetLanguage(wxLANGUAGE_DEFAULT); - // Let the wxFileTranslationsLoader enumerate all translation dictionaries for PrusaSlicer - // and try to match them with the system specific "preferred languages". - // There seems to be a support for that on Windows and OSX, while on Linuxes the code just returns wxLocale::GetSystemLanguage(). - // The last parameter gets added to the list of detected dictionaries. This is a workaround - // for not having the English dictionary. Let's hope wxWidgets of various versions process this call the same way. - wxString best_language = wxTranslations::Get()->GetBestTranslation(SLIC3R_APP_KEY, wxLANGUAGE_ENGLISH); - if (! best_language.IsEmpty()) { - m_language_info_best = wxLocale::FindLanguageInfo(best_language); - BOOST_LOG_TRIVIAL(trace) << boost::format("Best translation language detected (may be different from user locales): %1%") % m_language_info_best->CanonicalName.ToUTF8().data(); - } - #ifdef __linux__ - wxString lc_all; - if (wxGetEnv("LC_ALL", &lc_all) && ! lc_all.IsEmpty()) { - // Best language returned by wxWidgets on Linux apparently does not respect LC_ALL. - // Disregard the "best" suggestion in case LC_ALL is provided. - m_language_info_best = nullptr; - } - #endif - } - } - - const wxLanguageInfo *language_info = language.empty() ? nullptr : wxLocale::FindLanguageInfo(language); - if (! language.empty() && (language_info == nullptr || language_info->CanonicalName.empty())) { - // Fix for wxWidgets issue, where the FindLanguageInfo() returns locales with undefined ANSII code (wxLANGUAGE_KONKANI or wxLANGUAGE_MANIPURI). - language_info = nullptr; - BOOST_LOG_TRIVIAL(error) << boost::format("Language code \"%1%\" is not supported") % language.ToUTF8().data(); - } - - if (language_info != nullptr && language_info->LayoutDirection == wxLayout_RightToLeft) { - BOOST_LOG_TRIVIAL(trace) << boost::format("The following language code requires right to left layout, which is not supported by PrusaSlicer: %1%") % language_info->CanonicalName.ToUTF8().data(); - language_info = nullptr; - } - - if (language_info == nullptr) { - // PrusaSlicer does not support the Right to Left languages yet. - if (m_language_info_system != nullptr && m_language_info_system->LayoutDirection != wxLayout_RightToLeft) - language_info = m_language_info_system; - if (m_language_info_best != nullptr && m_language_info_best->LayoutDirection != wxLayout_RightToLeft) - language_info = m_language_info_best; - if (language_info == nullptr) - language_info = wxLocale::GetLanguageInfo(wxLANGUAGE_ENGLISH_US); - } - - BOOST_LOG_TRIVIAL(trace) << boost::format("Switching wxLocales to %1%") % language_info->CanonicalName.ToUTF8().data(); - - // Alternate language code. - wxLanguage language_dict = wxLanguage(language_info->Language); - if (language_info->CanonicalName.BeforeFirst('_') == "sk") { - // Slovaks understand Czech well. Give them the Czech translation. - language_dict = wxLANGUAGE_CZECH; - BOOST_LOG_TRIVIAL(trace) << "Using Czech dictionaries for Slovak language"; - } - - // Select language for locales. This language may be different from the language of the dictionary. - if (language_info == m_language_info_best || language_info == m_language_info_system) { - // The current language matches user's default profile exactly. That's great. - } else if (m_language_info_best != nullptr && language_info->CanonicalName.BeforeFirst('_') == m_language_info_best->CanonicalName.BeforeFirst('_')) { - // Use whatever the operating system recommends, if it the language code of the dictionary matches the recommended language. - // This allows a Swiss guy to use a German dictionary without forcing him to German locales. - language_info = m_language_info_best; - } else if (m_language_info_system != nullptr && language_info->CanonicalName.BeforeFirst('_') == m_language_info_system->CanonicalName.BeforeFirst('_')) - language_info = m_language_info_system; - -#ifdef __linux__ - // If we can't find this locale , try to use different one for the language - // instead of just reporting that it is impossible to switch. - if (! wxLocale::IsAvailable(language_info->Language)) { - std::string original_lang = into_u8(language_info->CanonicalName); - language_info = linux_get_existing_locale_language(language_info, m_language_info_system); - BOOST_LOG_TRIVIAL(trace) << boost::format("Can't switch language to %1% (missing locales). Using %2% instead.") - % original_lang % language_info->CanonicalName.ToUTF8().data(); - } -#endif - - if (! wxLocale::IsAvailable(language_info->Language)) { - // Loading the language dictionary failed. - wxString message = "Switching PrusaSlicer to language " + language_info->CanonicalName + " failed."; -#if !defined(_WIN32) && !defined(__APPLE__) - // likely some linux system - message += "\nYou may need to reconfigure the missing locales, likely by running the \"locale-gen\" and \"dpkg-reconfigure locales\" commands.\n"; -#endif - if (initial) - message + "\n\nApplication will close."; - wxMessageBox(message, "PrusaSlicer - Switching language failed", wxOK | wxICON_ERROR); - if (initial) - std::exit(EXIT_FAILURE); - else - return false; - } - - // Release the old locales, create new locales. - //FIXME wxWidgets cause havoc if the current locale is deleted. We just forget it causing memory leaks for now. - m_wxLocale.release(); - m_wxLocale = Slic3r::make_unique(); - m_wxLocale->Init(language_info->Language); - // Override language at the active wxTranslations class (which is stored in the active m_wxLocale) - // to load possibly different dictionary, for example, load Czech dictionary for Slovak language. - wxTranslations::Get()->SetLanguage(language_dict); - m_wxLocale->AddCatalog(SLIC3R_APP_KEY); - m_imgui->set_language(into_u8(language_info->CanonicalName)); - //FIXME This is a temporary workaround, the correct solution is to switch to "C" locale during file import / export only. - //wxSetlocale(LC_NUMERIC, "C"); - Preset::update_suffix_modified((" (" + _L("modified") + ")").ToUTF8().data()); - return true; -} - -Tab* GUI_App::get_tab(Preset::Type type) -{ - for (Tab* tab: tabs_list) - if (tab->type() == type) - return tab->completed() ? tab : nullptr; // To avoid actions with no-completed Tab - return nullptr; -} - -ConfigOptionMode GUI_App::get_mode() -{ - if (!app_config->has("view_mode")) - return comSimple; - - const auto mode = app_config->get("view_mode"); - return mode == "expert" ? comExpert : - mode == "simple" ? comSimple : comAdvanced; -} - -void GUI_App::save_mode(const /*ConfigOptionMode*/int mode) -{ - const std::string mode_str = mode == comExpert ? "expert" : - mode == comSimple ? "simple" : "advanced"; - app_config->set("view_mode", mode_str); - app_config->save(); - update_mode(); -} - -// Update view mode according to selected menu -void GUI_App::update_mode() -{ - sidebar().update_mode(); - -#ifdef _MSW_DARK_MODE - if (!wxGetApp().tabs_as_menu()) - dynamic_cast(mainframe->m_tabpanel)->UpdateMode(); -#endif - - for (auto tab : tabs_list) - tab->update_mode(); - - plater()->update_menus(); - plater()->canvas3D()->update_gizmos_on_off_state(); -} - -void GUI_App::add_config_menu(wxMenuBar *menu) -{ - auto local_menu = new wxMenu(); - wxWindowID config_id_base = wxWindow::NewControlId(int(ConfigMenuCnt)); - - const auto config_wizard_name = _(ConfigWizard::name(true)); - const auto config_wizard_tooltip = from_u8((boost::format(_utf8(L("Run %s"))) % config_wizard_name).str()); - // Cmd+, is standard on OS X - what about other operating systems? - if (is_editor()) { - local_menu->Append(config_id_base + ConfigMenuWizard, config_wizard_name + dots, config_wizard_tooltip); - local_menu->Append(config_id_base + ConfigMenuSnapshots, _L("&Configuration Snapshots") + dots, _L("Inspect / activate configuration snapshots")); - local_menu->Append(config_id_base + ConfigMenuTakeSnapshot, _L("Take Configuration &Snapshot"), _L("Capture a configuration snapshot")); - local_menu->Append(config_id_base + ConfigMenuUpdateConf, _L("Check for Configuration Updates"), _L("Check for configuration updates")); - local_menu->Append(config_id_base + ConfigMenuUpdateApp, _L("Check for Application Updates"), _L("Check for new version of application")); -#if defined(__linux__) && defined(SLIC3R_DESKTOP_INTEGRATION) - //if (DesktopIntegrationDialog::integration_possible()) - local_menu->Append(config_id_base + ConfigMenuDesktopIntegration, _L("Desktop Integration"), _L("Desktop Integration")); -#endif //(__linux__) && defined(SLIC3R_DESKTOP_INTEGRATION) - local_menu->AppendSeparator(); - } - local_menu->Append(config_id_base + ConfigMenuPreferences, _L("&Preferences") + dots + -#ifdef __APPLE__ - "\tCtrl+,", -#else - "\tCtrl+P", -#endif - _L("Application preferences")); - wxMenu* mode_menu = nullptr; - if (is_editor()) { - local_menu->AppendSeparator(); - mode_menu = new wxMenu(); - mode_menu->AppendRadioItem(config_id_base + ConfigMenuModeSimple, _L("Simple"), _L("Simple View Mode")); -// mode_menu->AppendRadioItem(config_id_base + ConfigMenuModeAdvanced, _L("Advanced"), _L("Advanced View Mode")); - mode_menu->AppendRadioItem(config_id_base + ConfigMenuModeAdvanced, _CTX(L_CONTEXT("Advanced", "Mode"), "Mode"), _L("Advanced View Mode")); - mode_menu->AppendRadioItem(config_id_base + ConfigMenuModeExpert, _L("Expert"), _L("Expert View Mode")); - Bind(wxEVT_UPDATE_UI, [this](wxUpdateUIEvent& evt) { if (get_mode() == comSimple) evt.Check(true); }, config_id_base + ConfigMenuModeSimple); - Bind(wxEVT_UPDATE_UI, [this](wxUpdateUIEvent& evt) { if (get_mode() == comAdvanced) evt.Check(true); }, config_id_base + ConfigMenuModeAdvanced); - Bind(wxEVT_UPDATE_UI, [this](wxUpdateUIEvent& evt) { if (get_mode() == comExpert) evt.Check(true); }, config_id_base + ConfigMenuModeExpert); - - local_menu->AppendSubMenu(mode_menu, _L("Mode"), wxString::Format(_L("%s View Mode"), SLIC3R_APP_NAME)); - } - local_menu->AppendSeparator(); - local_menu->Append(config_id_base + ConfigMenuLanguage, _L("&Language")); - if (is_editor()) { - local_menu->AppendSeparator(); - local_menu->Append(config_id_base + ConfigMenuFlashFirmware, _L("Flash Printer &Firmware"), _L("Upload a firmware image into an Arduino based printer")); - // TODO: for when we're able to flash dictionaries - // local_menu->Append(config_id_base + FirmwareMenuDict, _L("Flash Language File"), _L("Upload a language dictionary file into a Prusa printer")); - } - - local_menu->Bind(wxEVT_MENU, [this, config_id_base](wxEvent &event) { - switch (event.GetId() - config_id_base) { - case ConfigMenuWizard: - run_wizard(ConfigWizard::RR_USER); - break; - case ConfigMenuUpdateConf: - check_updates(true); - break; - case ConfigMenuUpdateApp: - app_version_check(true); - break; -#ifdef __linux__ - case ConfigMenuDesktopIntegration: - show_desktop_integration_dialog(); - break; -#endif - case ConfigMenuTakeSnapshot: - // Take a configuration snapshot. - if (wxString action_name = _L("Taking a configuration snapshot"); - check_and_save_current_preset_changes(action_name, _L("Some presets are modified and the unsaved changes will not be captured by the configuration snapshot."), false, true)) { - wxTextEntryDialog dlg(nullptr, action_name, _L("Snapshot name")); - UpdateDlgDarkUI(&dlg); - - // set current normal font for dialog children, - // because of just dlg.SetFont(normal_font()) has no result; - for (auto child : dlg.GetChildren()) - child->SetFont(normal_font()); - - if (dlg.ShowModal() == wxID_OK) - if (const Config::Snapshot *snapshot = Config::take_config_snapshot_report_error( - *app_config, Config::Snapshot::SNAPSHOT_USER, dlg.GetValue().ToUTF8().data()); - snapshot != nullptr) - app_config->set("on_snapshot", snapshot->id); - } - break; - case ConfigMenuSnapshots: - if (check_and_save_current_preset_changes(_L("Loading a configuration snapshot"), "", false)) { - std::string on_snapshot; - if (Config::SnapshotDB::singleton().is_on_snapshot(*app_config)) - on_snapshot = app_config->get("on_snapshot"); - ConfigSnapshotDialog dlg(Slic3r::GUI::Config::SnapshotDB::singleton(), on_snapshot); - dlg.ShowModal(); - if (!dlg.snapshot_to_activate().empty()) { - if (! Config::SnapshotDB::singleton().is_on_snapshot(*app_config) && - ! Config::take_config_snapshot_cancel_on_error(*app_config, Config::Snapshot::SNAPSHOT_BEFORE_ROLLBACK, "", - GUI::format(_L("Continue to activate a configuration snapshot %1%?"), dlg.snapshot_to_activate()))) - break; - try { - app_config->set("on_snapshot", Config::SnapshotDB::singleton().restore_snapshot(dlg.snapshot_to_activate(), *app_config).id); - // Enable substitutions, log both user and system substitutions. There should not be any substitutions performed when loading system - // presets because compatibility of profiles shall be verified using the min_slic3r_version keys in config index, but users - // are known to be creative and mess with the config files in various ways. - if (PresetsConfigSubstitutions all_substitutions = preset_bundle->load_presets(*app_config, ForwardCompatibilitySubstitutionRule::Enable); - ! all_substitutions.empty()) - show_substitutions_info(all_substitutions); - - // Load the currently selected preset into the GUI, update the preset selection box. - load_current_presets(); - } catch (std::exception &ex) { - GUI::show_error(nullptr, _L("Failed to activate configuration snapshot.") + "\n" + into_u8(ex.what())); - } - } - } - break; - case ConfigMenuPreferences: - { - open_preferences(); - break; - } - case ConfigMenuLanguage: - { - /* Before change application language, let's check unsaved changes on 3D-Scene - * and draw user's attention to the application restarting after a language change - */ - { - // the dialog needs to be destroyed before the call to switch_language() - // or sometimes the application crashes into wxDialogBase() destructor - // so we put it into an inner scope - wxString title = is_editor() ? wxString(SLIC3R_APP_NAME) : wxString(GCODEVIEWER_APP_NAME); - title += " - " + _L("Language selection"); - //wxMessageDialog dialog(nullptr, - MessageDialog dialog(nullptr, - _L("Switching the language will trigger application restart.\n" - "You will lose content of the plater.") + "\n\n" + - _L("Do you want to proceed?"), - title, - wxICON_QUESTION | wxOK | wxCANCEL); - if (dialog.ShowModal() == wxID_CANCEL) - return; - } - - switch_language(); - break; - } - case ConfigMenuFlashFirmware: - FirmwareDialog::run(mainframe); - break; - default: - break; - } - }); - - using std::placeholders::_1; - - if (mode_menu != nullptr) { - auto modfn = [this](int mode, wxCommandEvent&) { if (get_mode() != mode) save_mode(mode); }; - mode_menu->Bind(wxEVT_MENU, std::bind(modfn, comSimple, _1), config_id_base + ConfigMenuModeSimple); - mode_menu->Bind(wxEVT_MENU, std::bind(modfn, comAdvanced, _1), config_id_base + ConfigMenuModeAdvanced); - mode_menu->Bind(wxEVT_MENU, std::bind(modfn, comExpert, _1), config_id_base + ConfigMenuModeExpert); - } - - menu->Append(local_menu, _L("&Configuration")); -} - -void GUI_App::open_preferences(const std::string& highlight_option /*= std::string()*/, const std::string& tab_name/*= std::string()*/) -{ - mainframe->preferences_dialog->show(highlight_option, tab_name); - - if (mainframe->preferences_dialog->recreate_GUI()) - recreate_GUI(_L("Restart application") + dots); - -#if ENABLE_GCODE_LINES_ID_IN_H_SLIDER - if (dlg.seq_top_layer_only_changed() || dlg.seq_seq_top_gcode_indices_changed()) -#else - if (mainframe->preferences_dialog->seq_top_layer_only_changed()) -#endif // ENABLE_GCODE_LINES_ID_IN_H_SLIDER - this->plater_->refresh_print(); - -#ifdef _WIN32 - if (is_editor()) { - if (app_config->get("associate_3mf") == "1") - associate_3mf_files(); - if (app_config->get("associate_stl") == "1") - associate_stl_files(); - } - else { - if (app_config->get("associate_gcode") == "1") - associate_gcode_files(); - } -#endif // _WIN32 - - if (mainframe->preferences_dialog->settings_layout_changed()) { - // hide full main_sizer for mainFrame - mainframe->GetSizer()->Show(false); - mainframe->update_layout(); - mainframe->select_tab(size_t(0)); - } -} - -bool GUI_App::has_unsaved_preset_changes() const -{ - PrinterTechnology printer_technology = preset_bundle->printers.get_edited_preset().printer_technology(); - for (const Tab* const tab : tabs_list) { - if (tab->supports_printer_technology(printer_technology) && tab->saved_preset_is_dirty()) - return true; - } - return false; -} - -bool GUI_App::has_current_preset_changes() const -{ - PrinterTechnology printer_technology = preset_bundle->printers.get_edited_preset().printer_technology(); - for (const Tab* const tab : tabs_list) { - if (tab->supports_printer_technology(printer_technology) && tab->current_preset_is_dirty()) - return true; - } - return false; -} - -void GUI_App::update_saved_preset_from_current_preset() -{ - PrinterTechnology printer_technology = preset_bundle->printers.get_edited_preset().printer_technology(); - for (Tab* tab : tabs_list) { - if (tab->supports_printer_technology(printer_technology)) - tab->update_saved_preset_from_current_preset(); - } -} - -std::vector GUI_App::get_active_preset_collections() const -{ - std::vector ret; - PrinterTechnology printer_technology = preset_bundle->printers.get_edited_preset().printer_technology(); - for (const Tab* tab : tabs_list) - if (tab->supports_printer_technology(printer_technology)) - ret.push_back(tab->get_presets()); - return ret; -} - -// To notify the user whether he is aware that some preset changes will be lost, -// UnsavedChangesDialog: "Discard / Save / Cancel" -// This is called when: -// - Close Application & Current project isn't saved -// - Load Project & Current project isn't saved -// - Undo / Redo with change of print technologie -// - Loading snapshot -// - Loading config_file/bundle -// UnsavedChangesDialog: "Don't save / Save / Cancel" -// This is called when: -// - Exporting config_bundle -// - Taking snapshot -bool GUI_App::check_and_save_current_preset_changes(const wxString& caption, const wxString& header, bool remember_choice/* = true*/, bool dont_save_insted_of_discard/* = false*/) -{ - if (has_current_preset_changes()) { - const std::string app_config_key = remember_choice ? "default_action_on_close_application" : ""; - int act_buttons = UnsavedChangesDialog::ActionButtons::SAVE; - if (dont_save_insted_of_discard) - act_buttons |= UnsavedChangesDialog::ActionButtons::DONT_SAVE; - UnsavedChangesDialog dlg(caption, header, app_config_key, act_buttons); - std::string act = app_config_key.empty() ? "none" : wxGetApp().app_config->get(app_config_key); - if (act == "none" && dlg.ShowModal() == wxID_CANCEL) - return false; - - if (dlg.save_preset()) // save selected changes - { - for (const std::pair& nt : dlg.get_names_and_types()) - preset_bundle->save_changes_for_preset(nt.first, nt.second, dlg.get_unselected_options(nt.second)); - - load_current_presets(false); - - // if we saved changes to the new presets, we should to - // synchronize config.ini with the current selections. - preset_bundle->export_selections(*app_config); - - MessageDialog(nullptr, _L_PLURAL("The preset modifications are successfully saved", - "The presets modifications are successfully saved", dlg.get_names_and_types().size())).ShowModal(); - } - } - - return true; -} - -void GUI_App::apply_keeped_preset_modifications() -{ - PrinterTechnology printer_technology = preset_bundle->printers.get_edited_preset().printer_technology(); - for (Tab* tab : tabs_list) { - if (tab->supports_printer_technology(printer_technology)) - tab->apply_config_from_cache(); - } - load_current_presets(false); -} - -// This is called when creating new project or load another project -// OR close ConfigWizard -// to ask the user what should we do with unsaved changes for presets. -// New Project => Current project is saved => UnsavedChangesDialog: "Keep / Discard / Cancel" -// => Current project isn't saved => UnsavedChangesDialog: "Keep / Discard / Save / Cancel" -// Close ConfigWizard => Current project is saved => UnsavedChangesDialog: "Keep / Discard / Save / Cancel" -// Note: no_nullptr postponed_apply_of_keeped_changes indicates that thie function is called after ConfigWizard is closed -bool GUI_App::check_and_keep_current_preset_changes(const wxString& caption, const wxString& header, int action_buttons, bool* postponed_apply_of_keeped_changes/* = nullptr*/) -{ - if (has_current_preset_changes()) { - bool is_called_from_configwizard = postponed_apply_of_keeped_changes != nullptr; - - const std::string app_config_key = is_called_from_configwizard ? "" : "default_action_on_new_project"; - UnsavedChangesDialog dlg(caption, header, app_config_key, action_buttons); - std::string act = app_config_key.empty() ? "none" : wxGetApp().app_config->get(app_config_key); - if (act == "none" && dlg.ShowModal() == wxID_CANCEL) - return false; - - auto reset_modifications = [this, is_called_from_configwizard]() { - if (is_called_from_configwizard) - return; // no need to discared changes. It will be done fromConfigWizard closing - - PrinterTechnology printer_technology = preset_bundle->printers.get_edited_preset().printer_technology(); - for (const Tab* const tab : tabs_list) { - if (tab->supports_printer_technology(printer_technology) && tab->current_preset_is_dirty()) - tab->m_presets->discard_current_changes(); - } - load_current_presets(false); - }; - - if (dlg.discard()) - reset_modifications(); - else // save selected changes - { - const auto& preset_names_and_types = dlg.get_names_and_types(); - if (dlg.save_preset()) { - for (const std::pair& nt : preset_names_and_types) - preset_bundle->save_changes_for_preset(nt.first, nt.second, dlg.get_unselected_options(nt.second)); - - // if we saved changes to the new presets, we should to - // synchronize config.ini with the current selections. - preset_bundle->export_selections(*app_config); - - wxString text = _L_PLURAL("The preset modifications are successfully saved", - "The presets modifications are successfully saved", preset_names_and_types.size()); - if (!is_called_from_configwizard) - text += "\n\n" + _L("For new project all modifications will be reseted"); - - MessageDialog(nullptr, text).ShowModal(); - reset_modifications(); - } - else if (dlg.transfer_changes() && (dlg.has_unselected_options() || is_called_from_configwizard)) { - // execute this part of code only if not all modifications are keeping to the new project - // OR this function is called when ConfigWizard is closed and "Keep modifications" is selected - for (const std::pair& nt : preset_names_and_types) { - Preset::Type type = nt.second; - Tab* tab = get_tab(type); - std::vector selected_options = dlg.get_selected_options(type); - if (type == Preset::TYPE_PRINTER) { - auto it = std::find(selected_options.begin(), selected_options.end(), "extruders_count"); - if (it != selected_options.end()) { - // erase "extruders_count" option from the list - selected_options.erase(it); - // cache the extruders count - static_cast(tab)->cache_extruder_cnt(); - } - } - tab->cache_config_diff(selected_options); - if (!is_called_from_configwizard) - tab->m_presets->discard_current_changes(); - } - if (is_called_from_configwizard) - *postponed_apply_of_keeped_changes = true; - else - apply_keeped_preset_modifications(); - } - } - } - - return true; -} - -bool GUI_App::can_load_project() -{ - int saved_project = plater()->save_project_if_dirty(_L("Loading a new project while the current project is modified.")); - if (saved_project == wxID_CANCEL || - (plater()->is_project_dirty() && saved_project == wxID_NO && - !check_and_save_current_preset_changes(_L("Project is loading"), _L("Opening new project while some presets are unsaved.")))) - return false; - return true; -} - -bool GUI_App::check_print_host_queue() -{ - wxString dirty; - std::vector> jobs; - // Get ongoing jobs from dialog - mainframe->m_printhost_queue_dlg->get_active_jobs(jobs); - if (jobs.empty()) - return true; - // Show dialog - wxString job_string = wxString(); - for (const auto& job : jobs) { - job_string += format_wxstr(" %1% : %2% \n", job.first, job.second); - } - wxString message; - message += _(L("The uploads are still ongoing")) + ":\n\n" + job_string +"\n" + _(L("Stop them and continue anyway?")); - //wxMessageDialog dialog(mainframe, - MessageDialog dialog(mainframe, - message, - wxString(SLIC3R_APP_NAME) + " - " + _(L("Ongoing uploads")), - wxICON_QUESTION | wxYES_NO | wxNO_DEFAULT); - if (dialog.ShowModal() == wxID_YES) - return true; - - // TODO: If already shown, bring forward - mainframe->m_printhost_queue_dlg->Show(); - return false; -} - -bool GUI_App::checked_tab(Tab* tab) -{ - bool ret = true; - if (find(tabs_list.begin(), tabs_list.end(), tab) == tabs_list.end()) - ret = false; - return ret; -} - -// Update UI / Tabs to reflect changes in the currently loaded presets -void GUI_App::load_current_presets(bool check_printer_presets_ /*= true*/) -{ - // check printer_presets for the containing information about "Print Host upload" - // and create physical printer from it, if any exists - if (check_printer_presets_) - check_printer_presets(); - - PrinterTechnology printer_technology = preset_bundle->printers.get_edited_preset().printer_technology(); - this->plater()->set_printer_technology(printer_technology); - for (Tab *tab : tabs_list) - if (tab->supports_printer_technology(printer_technology)) { - if (tab->type() == Preset::TYPE_PRINTER) { - static_cast(tab)->update_pages(); - // Mark the plater to update print bed by tab->load_current_preset() from Plater::on_config_change(). - this->plater()->force_print_bed_update(); - } - tab->load_current_preset(); - } -} - -bool GUI_App::OnExceptionInMainLoop() -{ - generic_exception_handle(); - return false; -} - -#ifdef __APPLE__ -// This callback is called from wxEntry()->wxApp::CallOnInit()->NSApplication run -// that is, before GUI_App::OnInit(), so we have a chance to switch GUI_App -// to a G-code viewer. -void GUI_App::OSXStoreOpenFiles(const wxArrayString &fileNames) -{ - size_t num_gcodes = 0; - for (const wxString &filename : fileNames) - if (is_gcode_file(into_u8(filename))) - ++ num_gcodes; - if (fileNames.size() == num_gcodes) { - // Opening PrusaSlicer by drag & dropping a G-Code onto PrusaSlicer icon in Finder, - // just G-codes were passed. Switch to G-code viewer mode. - m_app_mode = EAppMode::GCodeViewer; - unlock_lockfile(get_instance_hash_string() + ".lock", data_dir() + "/cache/"); - if(app_config != nullptr) - delete app_config; - app_config = nullptr; - init_app_config(); - } - wxApp::OSXStoreOpenFiles(fileNames); -} -// wxWidgets override to get an event on open files. -void GUI_App::MacOpenFiles(const wxArrayString &fileNames) -{ - std::vector files; - std::vector gcode_files; - std::vector non_gcode_files; - for (const auto& filename : fileNames) { - if (is_gcode_file(into_u8(filename))) - gcode_files.emplace_back(filename); - else { - files.emplace_back(into_u8(filename)); - non_gcode_files.emplace_back(filename); - } - } - if (m_app_mode == EAppMode::GCodeViewer) { - // Running in G-code viewer. - // Load the first G-code into the G-code viewer. - // Or if no G-codes, send other files to slicer. - if (! gcode_files.empty()) { - if (m_post_initialized) - this->plater()->load_gcode(gcode_files.front()); - else - this->init_params->input_files = { into_u8(gcode_files.front()) }; - } - if (!non_gcode_files.empty()) - start_new_slicer(non_gcode_files, true); - } else { - if (! files.empty()) { - if (m_post_initialized) { - wxArrayString input_files; - for (size_t i = 0; i < non_gcode_files.size(); ++i) - input_files.push_back(non_gcode_files[i]); - this->plater()->load_files(input_files); - } else { - for (const auto &f : non_gcode_files) - this->init_params->input_files.emplace_back(into_u8(f)); - } - } - for (const wxString &filename : gcode_files) - start_new_gcodeviewer(&filename); - } -} -#endif /* __APPLE */ - -Sidebar& GUI_App::sidebar() -{ - return plater_->sidebar(); -} - -ObjectManipulation* GUI_App::obj_manipul() -{ - // If this method is called before plater_ has been initialized, return nullptr (to avoid a crash) - return (plater_ != nullptr) ? sidebar().obj_manipul() : nullptr; -} - -ObjectSettings* GUI_App::obj_settings() -{ - return sidebar().obj_settings(); -} - -ObjectList* GUI_App::obj_list() -{ - return sidebar().obj_list(); -} - -ObjectLayers* GUI_App::obj_layers() -{ - return sidebar().obj_layers(); -} - -Plater* GUI_App::plater() -{ - return plater_; -} - -const Plater* GUI_App::plater() const -{ - return plater_; -} - -Model& GUI_App::model() -{ - return plater_->model(); -} -wxBookCtrlBase* GUI_App::tab_panel() const -{ - return mainframe->m_tabpanel; -} - -NotificationManager * GUI_App::notification_manager() -{ - return plater_->get_notification_manager(); -} - -// extruders count from selected printer preset -int GUI_App::extruders_cnt() const -{ - const Preset& preset = preset_bundle->printers.get_selected_preset(); - return preset.printer_technology() == ptSLA ? 1 : - preset.config.option("nozzle_diameter")->values.size(); -} - -// extruders count from edited printer preset -int GUI_App::extruders_edited_cnt() const -{ - const Preset& preset = preset_bundle->printers.get_edited_preset(); - return preset.printer_technology() == ptSLA ? 1 : - preset.config.option("nozzle_diameter")->values.size(); -} - -wxString GUI_App::current_language_code_safe() const -{ - // Translate the language code to a code, for which Prusa Research maintains translations. - const std::map mapping { - { "cs", "cs_CZ", }, - { "sk", "cs_CZ", }, - { "de", "de_DE", }, - { "es", "es_ES", }, - { "fr", "fr_FR", }, - { "it", "it_IT", }, - { "ja", "ja_JP", }, - { "ko", "ko_KR", }, - { "pl", "pl_PL", }, - { "uk", "uk_UA", }, - { "zh", "zh_CN", }, - { "ru", "ru_RU", }, - }; - wxString language_code = this->current_language_code().BeforeFirst('_'); - auto it = mapping.find(language_code); - if (it != mapping.end()) - language_code = it->second; - else - language_code = "en_US"; - return language_code; -} - -void GUI_App::open_web_page_localized(const std::string &http_address) -{ - open_browser_with_warning_dialog(http_address + "&lng=" + this->current_language_code_safe(), nullptr, false); -} - -// If we are switching from the FFF-preset to the SLA, we should to control the printed objects if they have a part(s). -// Because of we can't to print the multi-part objects with SLA technology. -bool GUI_App::may_switch_to_SLA_preset(const wxString& caption) -{ - if (model_has_multi_part_objects(model())) { - show_info(nullptr, - _L("It's impossible to print multi-part object(s) with SLA technology.") + "\n\n" + - _L("Please check your object list before preset changing."), - caption); - return false; - } - return true; -} - -bool GUI_App::run_wizard(ConfigWizard::RunReason reason, ConfigWizard::StartPage start_page) -{ - wxCHECK_MSG(mainframe != nullptr, false, "Internal error: Main frame not created / null"); - - if (reason == ConfigWizard::RR_USER) { - if (preset_updater->config_update(app_config->orig_version(), PresetUpdater::UpdateParams::FORCED_BEFORE_WIZARD) == PresetUpdater::R_ALL_CANCELED) - return false; - } - - auto wizard = new ConfigWizard(mainframe); - const bool res = wizard->run(reason, start_page); - - if (res) { - load_current_presets(); - - // #ysFIXME - delete after testing: This part of code looks redundant. All checks are inside ConfigWizard::priv::apply_config() - if (preset_bundle->printers.get_edited_preset().printer_technology() == ptSLA) - may_switch_to_SLA_preset(_L("Configuration is editing from ConfigWizard")); - } - - return res; -} - -void GUI_App::show_desktop_integration_dialog() -{ -#ifdef __linux__ - //wxCHECK_MSG(mainframe != nullptr, false, "Internal error: Main frame not created / null"); - DesktopIntegrationDialog dialog(mainframe); - dialog.ShowModal(); -#endif //__linux__ -} - -#if ENABLE_THUMBNAIL_GENERATOR_DEBUG -void GUI_App::gcode_thumbnails_debug() -{ - const std::string BEGIN_MASK = "; thumbnail begin"; - const std::string END_MASK = "; thumbnail end"; - std::string gcode_line; - bool reading_image = false; - unsigned int width = 0; - unsigned int height = 0; - - wxFileDialog dialog(GetTopWindow(), _L("Select a gcode file:"), "", "", "G-code files (*.gcode)|*.gcode;*.GCODE;", wxFD_OPEN | wxFD_FILE_MUST_EXIST); - if (dialog.ShowModal() != wxID_OK) - return; - - std::string in_filename = into_u8(dialog.GetPath()); - std::string out_path = boost::filesystem::path(in_filename).remove_filename().append(L"thumbnail").string(); - - boost::nowide::ifstream in_file(in_filename.c_str()); - std::vector rows; - std::string row; - if (in_file.good()) - { - while (std::getline(in_file, gcode_line)) - { - if (in_file.good()) - { - if (boost::starts_with(gcode_line, BEGIN_MASK)) - { - reading_image = true; - gcode_line = gcode_line.substr(BEGIN_MASK.length() + 1); - std::string::size_type x_pos = gcode_line.find('x'); - std::string width_str = gcode_line.substr(0, x_pos); - width = (unsigned int)::atoi(width_str.c_str()); - std::string height_str = gcode_line.substr(x_pos + 1); - height = (unsigned int)::atoi(height_str.c_str()); - row.clear(); - } - else if (reading_image && boost::starts_with(gcode_line, END_MASK)) - { - std::string out_filename = out_path + std::to_string(width) + "x" + std::to_string(height) + ".png"; - boost::nowide::ofstream out_file(out_filename.c_str(), std::ios::binary); - if (out_file.good()) - { - std::string decoded; - decoded.resize(boost::beast::detail::base64::decoded_size(row.size())); - decoded.resize(boost::beast::detail::base64::decode((void*)&decoded[0], row.data(), row.size()).first); - - out_file.write(decoded.c_str(), decoded.size()); - out_file.close(); - } - - reading_image = false; - width = 0; - height = 0; - rows.clear(); - } - else if (reading_image) - row += gcode_line.substr(2); - } - } - - in_file.close(); - } -} -#endif // ENABLE_THUMBNAIL_GENERATOR_DEBUG - -void GUI_App::window_pos_save(wxTopLevelWindow* window, const std::string &name) -{ - if (name.empty()) { return; } - const auto config_key = (boost::format("window_%1%") % name).str(); - - WindowMetrics metrics = WindowMetrics::from_window(window); - app_config->set(config_key, metrics.serialize()); - app_config->save(); -} - -void GUI_App::window_pos_restore(wxTopLevelWindow* window, const std::string &name, bool default_maximized) -{ - if (name.empty()) { return; } - const auto config_key = (boost::format("window_%1%") % name).str(); - - if (! app_config->has(config_key)) { - window->Maximize(default_maximized); - return; - } - - auto metrics = WindowMetrics::deserialize(app_config->get(config_key)); - if (! metrics) { - window->Maximize(default_maximized); - return; - } - - const wxRect& rect = metrics->get_rect(); - - if (app_config->get("restore_win_position") == "1") { - // workaround for crash related to the positioning of the window on secondary monitor - app_config->set("restore_win_position", (boost::format("crashed_at_%1%_pos") % name).str()); - app_config->save(); - window->SetPosition(rect.GetPosition()); - - // workaround for crash related to the positioning of the window on secondary monitor - app_config->set("restore_win_position", (boost::format("crashed_at_%1%_size") % name).str()); - app_config->save(); - window->SetSize(rect.GetSize()); - - // revert "restore_win_position" value if application wasn't crashed - app_config->set("restore_win_position", "1"); - app_config->save(); - } - else - window->CenterOnScreen(); - - window->Maximize(metrics->get_maximized()); -} - -void GUI_App::window_pos_sanitize(wxTopLevelWindow* window) -{ - /*unsigned*/int display_idx = wxDisplay::GetFromWindow(window); - wxRect display; - if (display_idx == wxNOT_FOUND) { - display = wxDisplay(0u).GetClientArea(); - window->Move(display.GetTopLeft()); - } else { - display = wxDisplay(display_idx).GetClientArea(); - } - - auto metrics = WindowMetrics::from_window(window); - metrics.sanitize_for_display(display); - if (window->GetScreenRect() != metrics.get_rect()) { - window->SetSize(metrics.get_rect()); - } -} - -bool GUI_App::config_wizard_startup() -{ - if (!m_app_conf_exists || preset_bundle->printers.only_default_printers()) { - run_wizard(ConfigWizard::RR_DATA_EMPTY); - return true; - } else if (get_app_config()->legacy_datadir()) { - // Looks like user has legacy pre-vendorbundle data directory, - // explain what this is and run the wizard - - MsgDataLegacy dlg; - dlg.ShowModal(); - - run_wizard(ConfigWizard::RR_DATA_LEGACY); - return true; - } - return false; -} - -bool GUI_App::check_updates(const bool verbose) -{ - PresetUpdater::UpdateResult updater_result; - try { - updater_result = preset_updater->config_update(app_config->orig_version(), verbose ? PresetUpdater::UpdateParams::SHOW_TEXT_BOX : PresetUpdater::UpdateParams::SHOW_NOTIFICATION); - if (updater_result == PresetUpdater::R_INCOMPAT_EXIT) { - mainframe->Close(); - // Applicaiton is closing. - return false; - } - else if (updater_result == PresetUpdater::R_INCOMPAT_CONFIGURED) { - m_app_conf_exists = true; - } - else if (verbose && updater_result == PresetUpdater::R_NOOP) { - MsgNoUpdates dlg; - dlg.ShowModal(); - } - } - catch (const std::exception & ex) { - show_error(nullptr, ex.what()); - } - // Applicaiton will continue. - return true; -} - -bool GUI_App::open_browser_with_warning_dialog(const wxString& url, wxWindow* parent/* = nullptr*/, bool force_remember_choice /*= true*/, int flags/* = 0*/) -{ - bool launch = true; - - // warning dialog containes a "Remember my choice" checkbox - std::string option_key = "suppress_hyperlinks"; - if (force_remember_choice || app_config->get(option_key).empty()) { - if (app_config->get(option_key).empty()) { - RichMessageDialog dialog(parent, _L("Open hyperlink in default browser?"), _L("PrusaSlicer: Open hyperlink"), wxICON_QUESTION | wxYES_NO); - dialog.ShowCheckBox(_L("Remember my choice")); - auto answer = dialog.ShowModal(); - launch = answer == wxID_YES; - if (dialog.IsCheckBoxChecked()) { - wxString preferences_item = _L("Suppress to open hyperlink in browser"); - wxString msg = - _L("PrusaSlicer will remember your choice.") + "\n\n" + - _L("You will not be asked about it again on hyperlinks hovering.") + "\n\n" + - format_wxstr(_L("Visit \"Preferences\" and check \"%1%\"\nto changes your choice."), preferences_item); - - MessageDialog msg_dlg(parent, msg, _L("PrusaSlicer: Don't ask me again"), wxOK | wxCANCEL | wxICON_INFORMATION); - if (msg_dlg.ShowModal() == wxID_CANCEL) - return false; - app_config->set(option_key, answer == wxID_NO ? "1" : "0"); - } - } - if (launch) - launch = app_config->get(option_key) != "1"; - } - // warning dialog doesn't containe a "Remember my choice" checkbox - // and will be shown only when "Suppress to open hyperlink in browser" is ON. - else if (app_config->get(option_key) == "1") { - MessageDialog dialog(parent, _L("Open hyperlink in default browser?"), _L("PrusaSlicer: Open hyperlink"), wxICON_QUESTION | wxYES_NO); - launch = dialog.ShowModal() == wxID_YES; - } - - return launch && wxLaunchDefaultBrowser(url, flags); -} - -// static method accepting a wxWindow object as first parameter -// void warning_catcher{ -// my($self, $message_dialog) = @_; -// return sub{ -// my $message = shift; -// return if $message = ~/ GLUquadricObjPtr | Attempt to free unreferenced scalar / ; -// my @params = ($message, 'Warning', wxOK | wxICON_WARNING); -// $message_dialog -// ? $message_dialog->(@params) -// : Wx::MessageDialog->new($self, @params)->ShowModal; -// }; -// } - -// Do we need this function??? -// void GUI_App::notify(message) { -// auto frame = GetTopWindow(); -// // try harder to attract user attention on OS X -// if (!frame->IsActive()) -// frame->RequestUserAttention(defined(__WXOSX__/*&Wx::wxMAC */)? wxUSER_ATTENTION_ERROR : wxUSER_ATTENTION_INFO); -// -// // There used to be notifier using a Growl application for OSX, but Growl is dead. -// // The notifier also supported the Linux X D - bus notifications, but that support was broken. -// //TODO use wxNotificationMessage ? -// } - - -#ifdef __WXMSW__ -static bool set_into_win_registry(HKEY hkeyHive, const wchar_t* pszVar, const wchar_t* pszValue) -{ - // see as reference: https://stackoverflow.com/questions/20245262/c-program-needs-an-file-association - wchar_t szValueCurrent[1000]; - DWORD dwType; - DWORD dwSize = sizeof(szValueCurrent); - - int iRC = ::RegGetValueW(hkeyHive, pszVar, nullptr, RRF_RT_ANY, &dwType, szValueCurrent, &dwSize); - - bool bDidntExist = iRC == ERROR_FILE_NOT_FOUND; - - if ((iRC != ERROR_SUCCESS) && !bDidntExist) - // an error occurred - return false; - - if (!bDidntExist) { - if (dwType != REG_SZ) - // invalid type - return false; - - if (::wcscmp(szValueCurrent, pszValue) == 0) - // value already set - return false; - } - - DWORD dwDisposition; - HKEY hkey; - iRC = ::RegCreateKeyExW(hkeyHive, pszVar, 0, 0, 0, KEY_ALL_ACCESS, nullptr, &hkey, &dwDisposition); - bool ret = false; - if (iRC == ERROR_SUCCESS) { - iRC = ::RegSetValueExW(hkey, L"", 0, REG_SZ, (BYTE*)pszValue, (::wcslen(pszValue) + 1) * sizeof(wchar_t)); - if (iRC == ERROR_SUCCESS) - ret = true; - } - - RegCloseKey(hkey); - return ret; -} - -void GUI_App::associate_3mf_files() -{ - wchar_t app_path[MAX_PATH]; - ::GetModuleFileNameW(nullptr, app_path, sizeof(app_path)); - - std::wstring prog_path = L"\"" + std::wstring(app_path) + L"\""; - std::wstring prog_id = L"Prusa.Slicer.1"; - std::wstring prog_desc = L"PrusaSlicer"; - std::wstring prog_command = prog_path + L" \"%1\""; - std::wstring reg_base = L"Software\\Classes"; - std::wstring reg_extension = reg_base + L"\\.3mf"; - std::wstring reg_prog_id = reg_base + L"\\" + prog_id; - std::wstring reg_prog_id_command = reg_prog_id + L"\\Shell\\Open\\Command"; - - bool is_new = false; - is_new |= set_into_win_registry(HKEY_CURRENT_USER, reg_extension.c_str(), prog_id.c_str()); - is_new |= set_into_win_registry(HKEY_CURRENT_USER, reg_prog_id.c_str(), prog_desc.c_str()); - is_new |= set_into_win_registry(HKEY_CURRENT_USER, reg_prog_id_command.c_str(), prog_command.c_str()); - - if (is_new) - // notify Windows only when any of the values gets changed - ::SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_IDLIST, nullptr, nullptr); -} - -void GUI_App::associate_stl_files() -{ - wchar_t app_path[MAX_PATH]; - ::GetModuleFileNameW(nullptr, app_path, sizeof(app_path)); - - std::wstring prog_path = L"\"" + std::wstring(app_path) + L"\""; - std::wstring prog_id = L"Prusa.Slicer.1"; - std::wstring prog_desc = L"PrusaSlicer"; - std::wstring prog_command = prog_path + L" \"%1\""; - std::wstring reg_base = L"Software\\Classes"; - std::wstring reg_extension = reg_base + L"\\.stl"; - std::wstring reg_prog_id = reg_base + L"\\" + prog_id; - std::wstring reg_prog_id_command = reg_prog_id + L"\\Shell\\Open\\Command"; - - bool is_new = false; - is_new |= set_into_win_registry(HKEY_CURRENT_USER, reg_extension.c_str(), prog_id.c_str()); - is_new |= set_into_win_registry(HKEY_CURRENT_USER, reg_prog_id.c_str(), prog_desc.c_str()); - is_new |= set_into_win_registry(HKEY_CURRENT_USER, reg_prog_id_command.c_str(), prog_command.c_str()); - - if (is_new) - // notify Windows only when any of the values gets changed - ::SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_IDLIST, nullptr, nullptr); -} - -void GUI_App::associate_gcode_files() -{ - wchar_t app_path[MAX_PATH]; - ::GetModuleFileNameW(nullptr, app_path, sizeof(app_path)); - - std::wstring prog_path = L"\"" + std::wstring(app_path) + L"\""; - std::wstring prog_id = L"PrusaSlicer.GCodeViewer.1"; - std::wstring prog_desc = L"PrusaSlicerGCodeViewer"; - std::wstring prog_command = prog_path + L" \"%1\""; - std::wstring reg_base = L"Software\\Classes"; - std::wstring reg_extension = reg_base + L"\\.gcode"; - std::wstring reg_prog_id = reg_base + L"\\" + prog_id; - std::wstring reg_prog_id_command = reg_prog_id + L"\\Shell\\Open\\Command"; - - bool is_new = false; - is_new |= set_into_win_registry(HKEY_CURRENT_USER, reg_extension.c_str(), prog_id.c_str()); - is_new |= set_into_win_registry(HKEY_CURRENT_USER, reg_prog_id.c_str(), prog_desc.c_str()); - is_new |= set_into_win_registry(HKEY_CURRENT_USER, reg_prog_id_command.c_str(), prog_command.c_str()); - - if (is_new) - // notify Windows only when any of the values gets changed - ::SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_IDLIST, nullptr, nullptr); -} -#endif // __WXMSW__ - - -void GUI_App::on_version_read(wxCommandEvent& evt) -{ - app_config->set("version_online", into_u8(evt.GetString())); - app_config->save(); - std::string opt = app_config->get("notify_release"); - if (this->plater_ == nullptr || (opt != "all" && opt != "release")) { - return; - } - if (*Semver::parse(SLIC3R_VERSION) >= *Semver::parse(into_u8(evt.GetString()))) { - return; - } - // notification - /* - this->plater_->get_notification_manager()->push_notification(NotificationType::NewAppAvailable - , NotificationManager::NotificationLevel::ImportantNotificationLevel - , Slic3r::format(_u8L("New release version %1% is available."), evt.GetString()) - , _u8L("See Download page.") - , [](wxEvtHandler* evnthndlr) {wxGetApp().open_web_page_localized("https://www.prusa3d.com/slicerweb"); return true; } - ); - */ - // updater - // read triggered_by_user that was set when calling GUI_App::app_version_check - app_updater(m_app_updater->get_triggered_by_user()); -} - -void GUI_App::app_updater(bool from_user) -{ - DownloadAppData app_data = m_app_updater->get_app_data(); - - if (from_user && (!app_data.version || *app_data.version <= *Semver::parse(SLIC3R_VERSION))) - { - BOOST_LOG_TRIVIAL(info) << "There is no newer version online."; - MsgNoAppUpdates no_update_dialog; - no_update_dialog.ShowModal(); - return; - - } - - assert(!app_data.url.empty()); - assert(!app_data.target_path.empty()); - - // dialog with new version info - AppUpdateAvailableDialog dialog(*Semver::parse(SLIC3R_VERSION), *app_data.version); - auto dialog_result = dialog.ShowModal(); - // checkbox "do not show again" - if (dialog.disable_version_check()) { - app_config->set("notify_release", "none"); - } - // Doesn't wish to update - if (dialog_result != wxID_OK) { - return; - } - // dialog with new version download (installer or app dependent on system) including path selection - AppUpdateDownloadDialog dwnld_dlg(*app_data.version, app_data.target_path); - dialog_result = dwnld_dlg.ShowModal(); - // Doesn't wish to download - if (dialog_result != wxID_OK) { - return; - } - app_data.target_path =dwnld_dlg.get_download_path(); - - // start download - this->plater_->get_notification_manager()->push_download_progress_notification(_utf8("Download"), std::bind(&AppUpdater::cancel_callback, this->m_app_updater.get())); - app_data.start_after = dwnld_dlg.run_after_download(); - m_app_updater->set_app_data(std::move(app_data)); - m_app_updater->sync_download(); -} - -void GUI_App::app_version_check(bool from_user) -{ - if (from_user) { - if (m_app_updater->get_download_ongoing()) { - MessageDialog msgdlg(nullptr, _L("Download of new version is already ongoing. Do you wish to continue?"), _L("Notice"), wxYES_NO); - if (msgdlg.ShowModal() != wxID_YES) - return; - } - } - std::string version_check_url = app_config->version_check_url(); - m_app_updater->sync_version(version_check_url, from_user); -} - -} // GUI -} //Slic3r +#include "libslic3r/Technologies.hpp" +#include "GUI_App.hpp" +#include "GUI_Init.hpp" +#include "GUI_ObjectList.hpp" +#include "GUI_ObjectManipulation.hpp" +#include "GUI_Factories.hpp" +#include "format.hpp" +#include "I18N.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "libslic3r/Utils.hpp" +#include "libslic3r/Model.hpp" +#include "libslic3r/I18N.hpp" +#include "libslic3r/PresetBundle.hpp" +#include "libslic3r/Color.hpp" + +#include "GUI.hpp" +#include "GUI_Utils.hpp" +#include "3DScene.hpp" +#include "MainFrame.hpp" +#include "Plater.hpp" +#include "GLCanvas3D.hpp" + +#include "../Utils/PresetUpdater.hpp" +#include "../Utils/PrintHost.hpp" +#include "../Utils/Process.hpp" +#include "../Utils/MacDarkMode.hpp" +#include "../Utils/AppUpdater.hpp" +#include "../Utils/WinRegistry.hpp" +#include "slic3r/Config/Snapshot.hpp" +#include "ConfigSnapshotDialog.hpp" +#include "FirmwareDialog.hpp" +#include "Preferences.hpp" +#include "Tab.hpp" +#include "SysInfoDialog.hpp" +#include "KBShortcutsDialog.hpp" +#include "UpdateDialogs.hpp" +#include "Mouse3DController.hpp" +#include "RemovableDriveManager.hpp" +#include "InstanceCheck.hpp" +#include "NotificationManager.hpp" +#include "UnsavedChangesDialog.hpp" +#include "SavePresetDialog.hpp" +#include "PrintHostDialogs.hpp" +#include "DesktopIntegrationDialog.hpp" +#include "SendSystemInfoDialog.hpp" + +#include "BitmapCache.hpp" +#include "Notebook.hpp" + +#ifdef __WXMSW__ +#include +#include +#ifdef _MSW_DARK_MODE +#include +#endif // _MSW_DARK_MODE +#endif +#ifdef _WIN32 +#include +#endif + +#if ENABLE_THUMBNAIL_GENERATOR_DEBUG +#include +#include +#endif // ENABLE_THUMBNAIL_GENERATOR_DEBUG + +// Needed for forcing menu icons back under gtk2 and gtk3 +#if defined(__WXGTK20__) || defined(__WXGTK3__) + #include +#endif + +using namespace std::literals; + +namespace Slic3r { +namespace GUI { + +class MainFrame; + +class SplashScreen : public wxSplashScreen +{ +public: + SplashScreen(const wxBitmap& bitmap, long splashStyle, int milliseconds, wxPoint pos = wxDefaultPosition) + : wxSplashScreen(bitmap, splashStyle, milliseconds, static_cast(wxGetApp().mainframe), wxID_ANY, wxDefaultPosition, wxDefaultSize, +#ifdef __APPLE__ + wxSIMPLE_BORDER | wxFRAME_NO_TASKBAR | wxSTAY_ON_TOP +#else + wxSIMPLE_BORDER | wxFRAME_NO_TASKBAR +#endif // !__APPLE__ + ) + { + wxASSERT(bitmap.IsOk()); + + int init_dpi = get_dpi_for_window(this); + this->SetPosition(pos); + this->CenterOnScreen(); + int new_dpi = get_dpi_for_window(this); + + m_scale = (float)(new_dpi) / (float)(init_dpi); + m_main_bitmap = bitmap; + + scale_bitmap(m_main_bitmap, m_scale); + + // init constant texts and scale fonts + init_constant_text(); + + // this font will be used for the action string + m_action_font = m_constant_text.credits_font.Bold(); + + // draw logo and constant info text + Decorate(m_main_bitmap); + } + + void SetText(const wxString& text) + { + set_bitmap(m_main_bitmap); + if (!text.empty()) { + wxBitmap bitmap(m_main_bitmap); + + wxMemoryDC memDC; + memDC.SelectObject(bitmap); + + memDC.SetFont(m_action_font); + memDC.SetTextForeground(wxColour(237, 107, 33)); + memDC.DrawText(text, int(m_scale * 60), m_action_line_y_position); + + memDC.SelectObject(wxNullBitmap); + set_bitmap(bitmap); +#ifdef __WXOSX__ + // without this code splash screen wouldn't be updated under OSX + wxYield(); +#endif + } + } + + static wxBitmap MakeBitmap(wxBitmap bmp) + { + if (!bmp.IsOk()) + return wxNullBitmap; + + // create dark grey background for the splashscreen + // It will be 5/3 of the weight of the bitmap + int width = lround((double)5 / 3 * bmp.GetWidth()); + int height = bmp.GetHeight(); + + wxImage image(width, height); + unsigned char* imgdata_ = image.GetData(); + for (int i = 0; i < width * height; ++i) { + *imgdata_++ = 51; + *imgdata_++ = 51; + *imgdata_++ = 51; + } + + wxBitmap new_bmp(image); + + wxMemoryDC memDC; + memDC.SelectObject(new_bmp); + memDC.DrawBitmap(bmp, width - bmp.GetWidth(), 0, true); + + return new_bmp; + } + + void Decorate(wxBitmap& bmp) + { + if (!bmp.IsOk()) + return; + + // draw text to the box at the left of the splashscreen. + // this box will be 2/5 of the weight of the bitmap, and be at the left. + int width = lround(bmp.GetWidth() * 0.4); + + // load bitmap for logo + BitmapCache bmp_cache; + int logo_size = lround(width * 0.25); + wxBitmap logo_bmp = *bmp_cache.load_svg(wxGetApp().logo_name(), logo_size, logo_size); + + wxCoord margin = int(m_scale * 20); + + wxRect banner_rect(wxPoint(0, logo_size), wxPoint(width, bmp.GetHeight())); + banner_rect.Deflate(margin, 2 * margin); + + // use a memory DC to draw directly onto the bitmap + wxMemoryDC memDc(bmp); + + // draw logo + memDc.DrawBitmap(logo_bmp, margin, margin, true); + + // draw the (white) labels inside of our black box (at the left of the splashscreen) + memDc.SetTextForeground(wxColour(255, 255, 255)); + + memDc.SetFont(m_constant_text.title_font); + memDc.DrawLabel(m_constant_text.title, banner_rect, wxALIGN_TOP | wxALIGN_LEFT); + + int title_height = memDc.GetTextExtent(m_constant_text.title).GetY(); + banner_rect.SetTop(banner_rect.GetTop() + title_height); + banner_rect.SetHeight(banner_rect.GetHeight() - title_height); + + memDc.SetFont(m_constant_text.version_font); + memDc.DrawLabel(m_constant_text.version, banner_rect, wxALIGN_TOP | wxALIGN_LEFT); + int version_height = memDc.GetTextExtent(m_constant_text.version).GetY(); + + memDc.SetFont(m_constant_text.credits_font); + memDc.DrawLabel(m_constant_text.credits, banner_rect, wxALIGN_BOTTOM | wxALIGN_LEFT); + int credits_height = memDc.GetMultiLineTextExtent(m_constant_text.credits).GetY(); + int text_height = memDc.GetTextExtent("text").GetY(); + + // calculate position for the dynamic text + int logo_and_header_height = margin + logo_size + title_height + version_height; + m_action_line_y_position = logo_and_header_height + 0.5 * (bmp.GetHeight() - margin - credits_height - logo_and_header_height - text_height); + } + +private: + wxBitmap m_main_bitmap; + wxFont m_action_font; + int m_action_line_y_position; + float m_scale {1.0}; + + struct ConstantText + { + wxString title; + wxString version; + wxString credits; + + wxFont title_font; + wxFont version_font; + wxFont credits_font; + + void init(wxFont init_font) + { + // title + title = wxGetApp().is_editor() ? SLIC3R_APP_NAME : GCODEVIEWER_APP_NAME; + + // dynamically get the version to display + version = _L("Version") + " " + std::string(SLIC3R_VERSION); + + // credits infornation + credits = title + " " + + _L("is based on Slic3r by Alessandro Ranellucci and the RepRap community.") + "\n" + + _L("Developed by Prusa Research.")+ "\n\n" + + title + " " + _L("is licensed under the") + " " + _L("GNU Affero General Public License, version 3") + "\n\n" + + _L("Contributions by Vojtech Bubnik, Enrico Turri, Oleksandra Iushchenko, Tamas Meszaros, Lukas Matena, Vojtech Kral, David Kocik and numerous others.") + "\n\n" + + _L("Artwork model by M Boyer"); + + title_font = version_font = credits_font = init_font; + } + } + m_constant_text; + + void init_constant_text() + { + m_constant_text.init(get_default_font(this)); + + // As default we use a system font for current display. + // Scale fonts in respect to banner width + + int text_banner_width = lround(0.4 * m_main_bitmap.GetWidth()) - roundl(m_scale * 50); // banner_width - margins + + float title_font_scale = (float)text_banner_width / GetTextExtent(m_constant_text.title).GetX(); + scale_font(m_constant_text.title_font, title_font_scale > 3.5f ? 3.5f : title_font_scale); + + float version_font_scale = (float)text_banner_width / GetTextExtent(m_constant_text.version).GetX(); + scale_font(m_constant_text.version_font, version_font_scale > 2.f ? 2.f : version_font_scale); + + // The width of the credits information string doesn't respect to the banner width some times. + // So, scale credits_font in the respect to the longest string width + int longest_string_width = word_wrap_string(m_constant_text.credits); + float font_scale = (float)text_banner_width / longest_string_width; + scale_font(m_constant_text.credits_font, font_scale); + } + + void set_bitmap(wxBitmap& bmp) + { + m_window->SetBitmap(bmp); + m_window->Refresh(); + m_window->Update(); + } + + void scale_bitmap(wxBitmap& bmp, float scale) + { + if (scale == 1.0) + return; + + wxImage image = bmp.ConvertToImage(); + if (!image.IsOk() || image.GetWidth() == 0 || image.GetHeight() == 0) + return; + + int width = int(scale * image.GetWidth()); + int height = int(scale * image.GetHeight()); + image.Rescale(width, height, wxIMAGE_QUALITY_BILINEAR); + + bmp = wxBitmap(std::move(image)); + } + + void scale_font(wxFont& font, float scale) + { +#ifdef __WXMSW__ + // Workaround for the font scaling in respect to the current active display, + // not for the primary display, as it's implemented in Font.cpp + // See https://github.com/wxWidgets/wxWidgets/blob/master/src/msw/font.cpp + // void wxNativeFontInfo::SetFractionalPointSize(float pointSizeNew) + wxNativeFontInfo nfi= *font.GetNativeFontInfo(); + float pointSizeNew = wxDisplay(this).GetScaleFactor() * scale * font.GetPointSize(); + nfi.lf.lfHeight = nfi.GetLogFontHeightAtPPI(pointSizeNew, get_dpi_for_window(this)); + nfi.pointSize = pointSizeNew; + font = wxFont(nfi); +#else + font.Scale(scale); +#endif //__WXMSW__ + } + + // wrap a string for the strings no longer then 55 symbols + // return extent of the longest string + int word_wrap_string(wxString& input) + { + size_t line_len = 55;// count of symbols in one line + int idx = -1; + size_t cur_len = 0; + + wxString longest_sub_string; + auto get_longest_sub_string = [input](wxString &longest_sub_str, size_t cur_len, size_t i) { + if (cur_len > longest_sub_str.Len()) + longest_sub_str = input.SubString(i - cur_len + 1, i); + }; + + for (size_t i = 0; i < input.Len(); i++) + { + cur_len++; + if (input[i] == ' ') + idx = i; + if (input[i] == '\n') + { + get_longest_sub_string(longest_sub_string, cur_len, i); + idx = -1; + cur_len = 0; + } + if (cur_len >= line_len && idx >= 0) + { + get_longest_sub_string(longest_sub_string, cur_len, i); + input[idx] = '\n'; + cur_len = i - static_cast(idx); + } + } + + return GetTextExtent(longest_sub_string).GetX(); + } +}; + + +#ifdef __linux__ +bool static check_old_linux_datadir(const wxString& app_name) { + // If we are on Linux and the datadir does not exist yet, look into the old + // location where the datadir was before version 2.3. If we find it there, + // tell the user that he might wanna migrate to the new location. + // (https://github.com/prusa3d/PrusaSlicer/issues/2911) + // To be precise, the datadir should exist, it is created when single instance + // lock happens. Instead of checking for existence, check the contents. + + namespace fs = boost::filesystem; + + std::string new_path = Slic3r::data_dir(); + + wxString dir; + if (! wxGetEnv(wxS("XDG_CONFIG_HOME"), &dir) || dir.empty() ) + dir = wxFileName::GetHomeDir() + wxS("/.config"); + std::string default_path = (dir + "/" + app_name).ToUTF8().data(); + + if (new_path != default_path) { + // This happens when the user specifies a custom --datadir. + // Do not show anything in that case. + return true; + } + + fs::path data_dir = fs::path(new_path); + if (! fs::is_directory(data_dir)) + return true; // This should not happen. + + int file_count = std::distance(fs::directory_iterator(data_dir), fs::directory_iterator()); + + if (file_count <= 1) { // just cache dir with an instance lock + std::string old_path = wxStandardPaths::Get().GetUserDataDir().ToUTF8().data(); + + if (fs::is_directory(old_path)) { + wxString msg = from_u8((boost::format(_u8L("Starting with %1% 2.3, configuration " + "directory on Linux has changed (according to XDG Base Directory Specification) to \n%2%.\n\n" + "This directory did not exist yet (maybe you run the new version for the first time).\nHowever, " + "an old %1% configuration directory was detected in \n%3%.\n\n" + "Consider moving the contents of the old directory to the new location in order to access " + "your profiles, etc.\nNote that if you decide to downgrade %1% in future, it will use the old " + "location again.\n\n" + "What do you want to do now?")) % SLIC3R_APP_NAME % new_path % old_path).str()); + wxString caption = from_u8((boost::format(_u8L("%s - BREAKING CHANGE")) % SLIC3R_APP_NAME).str()); + RichMessageDialog dlg(nullptr, msg, caption, wxYES_NO); + dlg.SetYesNoLabels(_L("Quit, I will move my data now"), _L("Start the application")); + if (dlg.ShowModal() != wxID_NO) + return false; + } + } else { + // If the new directory exists, be silent. The user likely already saw the message. + } + return true; +} +#endif + +#ifdef _WIN32 +#if 0 // External Updater is replaced with AppUpdater.cpp +static bool run_updater_win() +{ + // find updater exe + boost::filesystem::path path_updater = boost::dll::program_location().parent_path() / "prusaslicer-updater.exe"; + // run updater. Original args: /silent -restartapp prusa-slicer.exe -startappfirst + std::string msg; + bool res = create_process(path_updater, L"/silent", msg); + if (!res) + BOOST_LOG_TRIVIAL(error) << msg; + return res; +} +#endif // 0 +#endif // _WIN32 + +struct FileWildcards { + std::string_view title; + std::vector file_extensions; +}; + +static const FileWildcards file_wildcards_by_type[FT_SIZE] = { + /* FT_STL */ { "STL files"sv, { ".stl"sv } }, + /* FT_OBJ */ { "OBJ files"sv, { ".obj"sv } }, + /* FT_OBJECT */ { "Object files"sv, { ".stl"sv, ".obj"sv } }, + /* FT_AMF */ { "AMF files"sv, { ".amf"sv, ".zip.amf"sv, ".xml"sv } }, + /* FT_3MF */ { "3MF files"sv, { ".3mf"sv } }, + /* FT_GCODE */ { "G-code files"sv, { ".gcode"sv, ".gco"sv, ".g"sv, ".ngc"sv } }, + /* FT_MODEL */ { "Known files"sv, { ".stl"sv, ".obj"sv, ".3mf"sv, ".amf"sv, ".zip.amf"sv, ".xml"sv } }, + /* FT_PROJECT */ { "Project files"sv, { ".3mf"sv, ".amf"sv, ".zip.amf"sv } }, + /* FT_GALLERY */ { "Known files"sv, { ".stl"sv, ".obj"sv } }, + + /* FT_INI */ { "INI files"sv, { ".ini"sv } }, + /* FT_SVG */ { "SVG files"sv, { ".svg"sv } }, + + /* FT_TEX */ { "Texture"sv, { ".png"sv, ".svg"sv } }, + + /* FT_SL1 */ { "Masked SLA files"sv, { ".sl1"sv, ".sl1s"sv, ".pwmx"sv } }, +}; + +#if ENABLE_ALTERNATIVE_FILE_WILDCARDS_GENERATOR +wxString file_wildcards(FileType file_type) +{ + const FileWildcards& data = file_wildcards_by_type[file_type]; + std::string title; + std::string mask; + + // Generate cumulative first item + for (const std::string_view& ext : data.file_extensions) { + if (title.empty()) { + title = "*"; + title += ext; + mask = title; + } + else { + title += ", *"; + title += ext; + mask += ";*"; + mask += ext; + } + mask += ";*"; + mask += boost::to_upper_copy(std::string(ext)); + } + + wxString ret = GUI::format_wxstr("%s (%s)|%s", data.title, title, mask); + + // Adds an item for each of the extensions + if (data.file_extensions.size() > 1) { + for (const std::string_view& ext : data.file_extensions) { + title = "*"; + title += ext; + ret += GUI::format_wxstr("|%s (%s)|%s", data.title, title, title); + } + } + + return ret; +} +#else +// This function produces a Win32 file dialog file template mask to be consumed by wxWidgets on all platforms. +// The function accepts a custom extension parameter. If the parameter is provided, the custom extension +// will be added as a fist to the list. This is important for a "file save" dialog on OSX, which strips +// an extension from the provided initial file name and substitutes it with the default extension (the first one in the template). +wxString file_wildcards(FileType file_type, const std::string &custom_extension) +{ + const FileWildcards& data = file_wildcards_by_type[file_type]; + std::string title; + std::string mask; + std::string custom_ext_lower; + + if (! custom_extension.empty()) { + // Generate an extension into the title mask and into the list of extensions. + custom_ext_lower = boost::to_lower_copy(custom_extension); + const std::string custom_ext_upper = boost::to_upper_copy(custom_extension); + if (custom_ext_lower == custom_extension) { + // Add a lower case version. + title = std::string("*") + custom_ext_lower; + mask = title; + // Add an upper case version. + mask += ";*"; + mask += custom_ext_upper; + } else if (custom_ext_upper == custom_extension) { + // Add an upper case version. + title = std::string("*") + custom_ext_upper; + mask = title; + // Add a lower case version. + mask += ";*"; + mask += custom_ext_lower; + } else { + // Add the mixed case version only. + title = std::string("*") + custom_extension; + mask = title; + } + } + + for (const std::string_view &ext : data.file_extensions) + // Only add an extension if it was not added first as the custom extension. + if (ext != custom_ext_lower) { + if (title.empty()) { + title = "*"; + title += ext; + mask = title; + } else { + title += ", *"; + title += ext; + mask += ";*"; + mask += ext; + } + mask += ";*"; + mask += boost::to_upper_copy(std::string(ext)); + } + + return GUI::format_wxstr("%s (%s)|%s", data.title, title, mask); +} +#endif // ENABLE_ALTERNATIVE_FILE_WILDCARDS_GENERATOR + +static std::string libslic3r_translate_callback(const char *s) { return wxGetTranslation(wxString(s, wxConvUTF8)).utf8_str().data(); } + +#ifdef WIN32 +#if !wxVERSION_EQUAL_OR_GREATER_THAN(3,1,3) +static void register_win32_dpi_event() +{ + enum { WM_DPICHANGED_ = 0x02e0 }; + + wxWindow::MSWRegisterMessageHandler(WM_DPICHANGED_, [](wxWindow *win, WXUINT nMsg, WXWPARAM wParam, WXLPARAM lParam) { + const int dpi = wParam & 0xffff; + const auto rect = reinterpret_cast(lParam); + const wxRect wxrect(wxPoint(rect->top, rect->left), wxPoint(rect->bottom, rect->right)); + + DpiChangedEvent evt(EVT_DPI_CHANGED_SLICER, dpi, wxrect); + win->GetEventHandler()->AddPendingEvent(evt); + + return true; + }); +} +#endif // !wxVERSION_EQUAL_OR_GREATER_THAN + +static GUID GUID_DEVINTERFACE_HID = { 0x4D1E55B2, 0xF16F, 0x11CF, 0x88, 0xCB, 0x00, 0x11, 0x11, 0x00, 0x00, 0x30 }; + +static void register_win32_device_notification_event() +{ + wxWindow::MSWRegisterMessageHandler(WM_DEVICECHANGE, [](wxWindow *win, WXUINT /* nMsg */, WXWPARAM wParam, WXLPARAM lParam) { + // Some messages are sent to top level windows by default, some messages are sent to only registered windows, and we explictely register on MainFrame only. + auto main_frame = dynamic_cast(win); + auto plater = (main_frame == nullptr) ? nullptr : main_frame->plater(); + if (plater == nullptr) + // Maybe some other top level window like a dialog or maybe a pop-up menu? + return true; + PDEV_BROADCAST_HDR lpdb = (PDEV_BROADCAST_HDR)lParam; + switch (wParam) { + case DBT_DEVICEARRIVAL: + if (lpdb->dbch_devicetype == DBT_DEVTYP_VOLUME) + plater->GetEventHandler()->AddPendingEvent(VolumeAttachedEvent(EVT_VOLUME_ATTACHED)); + else if (lpdb->dbch_devicetype == DBT_DEVTYP_DEVICEINTERFACE) { + PDEV_BROADCAST_DEVICEINTERFACE lpdbi = (PDEV_BROADCAST_DEVICEINTERFACE)lpdb; +// if (lpdbi->dbcc_classguid == GUID_DEVINTERFACE_VOLUME) { +// printf("DBT_DEVICEARRIVAL %d - Media has arrived: %ws\n", msg_count, lpdbi->dbcc_name); + if (lpdbi->dbcc_classguid == GUID_DEVINTERFACE_HID) + plater->GetEventHandler()->AddPendingEvent(HIDDeviceAttachedEvent(EVT_HID_DEVICE_ATTACHED, boost::nowide::narrow(lpdbi->dbcc_name))); + } + break; + case DBT_DEVICEREMOVECOMPLETE: + if (lpdb->dbch_devicetype == DBT_DEVTYP_VOLUME) + plater->GetEventHandler()->AddPendingEvent(VolumeDetachedEvent(EVT_VOLUME_DETACHED)); + else if (lpdb->dbch_devicetype == DBT_DEVTYP_DEVICEINTERFACE) { + PDEV_BROADCAST_DEVICEINTERFACE lpdbi = (PDEV_BROADCAST_DEVICEINTERFACE)lpdb; +// if (lpdbi->dbcc_classguid == GUID_DEVINTERFACE_VOLUME) +// printf("DBT_DEVICEARRIVAL %d - Media was removed: %ws\n", msg_count, lpdbi->dbcc_name); + if (lpdbi->dbcc_classguid == GUID_DEVINTERFACE_HID) + plater->GetEventHandler()->AddPendingEvent(HIDDeviceDetachedEvent(EVT_HID_DEVICE_DETACHED, boost::nowide::narrow(lpdbi->dbcc_name))); + } + break; + default: + break; + } + return true; + }); + + wxWindow::MSWRegisterMessageHandler(MainFrame::WM_USER_MEDIACHANGED, [](wxWindow *win, WXUINT /* nMsg */, WXWPARAM wParam, WXLPARAM lParam) { + // Some messages are sent to top level windows by default, some messages are sent to only registered windows, and we explictely register on MainFrame only. + auto main_frame = dynamic_cast(win); + auto plater = (main_frame == nullptr) ? nullptr : main_frame->plater(); + if (plater == nullptr) + // Maybe some other top level window like a dialog or maybe a pop-up menu? + return true; + wchar_t sPath[MAX_PATH]; + if (lParam == SHCNE_MEDIAINSERTED || lParam == SHCNE_MEDIAREMOVED) { + struct _ITEMIDLIST* pidl = *reinterpret_cast(wParam); + if (! SHGetPathFromIDList(pidl, sPath)) { + BOOST_LOG_TRIVIAL(error) << "MediaInserted: SHGetPathFromIDList failed"; + return false; + } + } + switch (lParam) { + case SHCNE_MEDIAINSERTED: + { + //printf("SHCNE_MEDIAINSERTED %S\n", sPath); + plater->GetEventHandler()->AddPendingEvent(VolumeAttachedEvent(EVT_VOLUME_ATTACHED)); + break; + } + case SHCNE_MEDIAREMOVED: + { + //printf("SHCNE_MEDIAREMOVED %S\n", sPath); + plater->GetEventHandler()->AddPendingEvent(VolumeDetachedEvent(EVT_VOLUME_DETACHED)); + break; + } + default: +// printf("Unknown\n"); + break; + } + return true; + }); + + wxWindow::MSWRegisterMessageHandler(WM_INPUT, [](wxWindow *win, WXUINT /* nMsg */, WXWPARAM wParam, WXLPARAM lParam) { + auto main_frame = dynamic_cast(Slic3r::GUI::find_toplevel_parent(win)); + auto plater = (main_frame == nullptr) ? nullptr : main_frame->plater(); +// if (wParam == RIM_INPUTSINK && plater != nullptr && main_frame->IsActive()) { + if (wParam == RIM_INPUT && plater != nullptr && main_frame->IsActive()) { + RAWINPUT raw; + UINT rawSize = sizeof(RAWINPUT); + ::GetRawInputData((HRAWINPUT)lParam, RID_INPUT, &raw, &rawSize, sizeof(RAWINPUTHEADER)); + if (raw.header.dwType == RIM_TYPEHID && plater->get_mouse3d_controller().handle_raw_input_win32(raw.data.hid.bRawData, raw.data.hid.dwSizeHid)) + return true; + } + return false; + }); + + wxWindow::MSWRegisterMessageHandler(WM_COPYDATA, [](wxWindow* win, WXUINT /* nMsg */, WXWPARAM wParam, WXLPARAM lParam) { + COPYDATASTRUCT* copy_data_structure = { 0 }; + copy_data_structure = (COPYDATASTRUCT*)lParam; + if (copy_data_structure->dwData == 1) { + LPCWSTR arguments = (LPCWSTR)copy_data_structure->lpData; + Slic3r::GUI::wxGetApp().other_instance_message_handler()->handle_message(boost::nowide::narrow(arguments)); + } + return true; + }); +} +#endif // WIN32 + +static void generic_exception_handle() +{ + // Note: Some wxWidgets APIs use wxLogError() to report errors, eg. wxImage + // - see https://docs.wxwidgets.org/3.1/classwx_image.html#aa249e657259fe6518d68a5208b9043d0 + // + // wxLogError typically goes around exception handling and display an error dialog some time + // after an error is logged even if exception handling and OnExceptionInMainLoop() take place. + // This is why we use wxLogError() here as well instead of a custom dialog, because it accumulates + // errors if multiple have been collected and displays just one error message for all of them. + // Otherwise we would get multiple error messages for one missing png, for example. + // + // If a custom error message window (or some other solution) were to be used, it would be necessary + // to turn off wxLogError() usage in wx APIs, most notably in wxImage + // - see https://docs.wxwidgets.org/trunk/classwx_image.html#aa32e5d3507cc0f8c3330135bc0befc6a + + try { + throw; + } catch (const std::bad_alloc& ex) { + // bad_alloc in main thread is most likely fatal. Report immediately to the user (wxLogError would be delayed) + // and terminate the app so it is at least certain to happen now. + wxString errmsg = wxString::Format(_L("%s has encountered an error. It was likely caused by running out of memory. " + "If you are sure you have enough RAM on your system, this may also be a bug and we would " + "be glad if you reported it.\n\nThe application will now terminate."), SLIC3R_APP_NAME); + wxMessageBox(errmsg + "\n\n" + wxString(ex.what()), _L("Fatal error"), wxOK | wxICON_ERROR); + BOOST_LOG_TRIVIAL(error) << boost::format("std::bad_alloc exception: %1%") % ex.what(); + std::terminate(); + } catch (const boost::io::bad_format_string& ex) { + wxString errmsg = _L("PrusaSlicer has encountered a localization error. " + "Please report to PrusaSlicer team, what language was active and in which scenario " + "this issue happened. Thank you.\n\nThe application will now terminate."); + wxMessageBox(errmsg + "\n\n" + wxString(ex.what()), _L("Critical error"), wxOK | wxICON_ERROR); + BOOST_LOG_TRIVIAL(error) << boost::format("Uncaught exception: %1%") % ex.what(); + std::terminate(); + throw; + } catch (const std::exception& ex) { + wxLogError(format_wxstr(_L("Internal error: %1%"), ex.what())); + BOOST_LOG_TRIVIAL(error) << boost::format("Uncaught exception: %1%") % ex.what(); + throw; + } +} + +void GUI_App::post_init() +{ + assert(initialized()); + if (! this->initialized()) + throw Slic3r::RuntimeError("Calling post_init() while not yet initialized"); + + if (this->is_gcode_viewer()) { + if (! this->init_params->input_files.empty()) + this->plater()->load_gcode(wxString::FromUTF8(this->init_params->input_files[0].c_str())); + } + else { + if (! this->init_params->preset_substitutions.empty()) + show_substitutions_info(this->init_params->preset_substitutions); + +#if 0 + // Load the cummulative config over the currently active profiles. + //FIXME if multiple configs are loaded, only the last one will have an effect. + // We need to decide what to do about loading of separate presets (just print preset, just filament preset etc). + // As of now only the full configs are supported here. + if (!m_print_config.empty()) + this->gui->mainframe->load_config(m_print_config); +#endif + if (! this->init_params->load_configs.empty()) + // Load the last config to give it a name at the UI. The name of the preset may be later + // changed by loading an AMF or 3MF. + //FIXME this is not strictly correct, as one may pass a print/filament/printer profile here instead of a full config. + this->mainframe->load_config_file(this->init_params->load_configs.back()); + // If loading a 3MF file, the config is loaded from the last one. + if (!this->init_params->input_files.empty()) { + const std::vector res = this->plater()->load_files(this->init_params->input_files, true, true); + if (!res.empty() && this->init_params->input_files.size() == 1) { + // Update application titlebar when opening a project file + const std::string& filename = this->init_params->input_files.front(); + if (boost::algorithm::iends_with(filename, ".amf") || + boost::algorithm::iends_with(filename, ".amf.xml") || + boost::algorithm::iends_with(filename, ".3mf")) + this->plater()->set_project_filename(from_u8(filename)); + } + } + if (! this->init_params->extra_config.empty()) + this->mainframe->load_config(this->init_params->extra_config); + } + + // show "Did you know" notification + if (app_config->get("show_hints") == "1" && ! is_gcode_viewer()) + plater_->get_notification_manager()->push_hint_notification(true); + + // The extra CallAfter() is needed because of Mac, where this is the only way + // to popup a modal dialog on start without screwing combo boxes. + // This is ugly but I honestly found no better way to do it. + // Neither wxShowEvent nor wxWindowCreateEvent work reliably. + if (this->preset_updater) { // G-Code Viewer does not initialize preset_updater. + if (! this->check_updates(false)) + // Configuration is not compatible and reconfigure was refused by the user. Application is closing. + return; + CallAfter([this] { + bool cw_showed = this->config_wizard_startup(); + this->preset_updater->sync(preset_bundle); + this->app_version_check(false); + if (! cw_showed) { + // The CallAfter is needed as well, without it, GL extensions did not show. + // Also, we only want to show this when the wizard does not, so the new user + // sees something else than "we want something" on the first start. + show_send_system_info_dialog_if_needed(); + } + }); + } + + // Set PrusaSlicer version and save to PrusaSlicer.ini or PrusaSlicerGcodeViewer.ini. + app_config->set("version", SLIC3R_VERSION); + app_config->save(); + +#ifdef _WIN32 + // Sets window property to mainframe so other instances can indentify it. + OtherInstanceMessageHandler::init_windows_properties(mainframe, m_instance_hash_int); +#endif //WIN32 +} + +IMPLEMENT_APP(GUI_App) + +GUI_App::GUI_App(EAppMode mode) + : wxApp() + , m_app_mode(mode) + , m_em_unit(10) + , m_imgui(new ImGuiWrapper()) + , m_removable_drive_manager(std::make_unique()) + , m_other_instance_message_handler(std::make_unique()) +{ + //app config initializes early becasuse it is used in instance checking in PrusaSlicer.cpp + this->init_app_config(); + // init app downloader after path to datadir is set + m_app_updater = std::make_unique(); +} + +GUI_App::~GUI_App() +{ + if (app_config != nullptr) + delete app_config; + + if (preset_bundle != nullptr) + delete preset_bundle; + + if (preset_updater != nullptr) + delete preset_updater; +} + +// If formatted for github, plaintext with OpenGL extensions enclosed into
. +// Otherwise HTML formatted for the system info dialog. +std::string GUI_App::get_gl_info(bool for_github) +{ + return OpenGLManager::get_gl_info().to_string(for_github); +} + +wxGLContext* GUI_App::init_glcontext(wxGLCanvas& canvas) +{ + return m_opengl_mgr.init_glcontext(canvas); +} + +bool GUI_App::init_opengl() +{ +#ifdef __linux__ + bool status = m_opengl_mgr.init_gl(); + m_opengl_initialized = true; + return status; +#else + return m_opengl_mgr.init_gl(); +#endif +} + +// gets path to PrusaSlicer.ini, returns semver from first line comment +static boost::optional parse_semver_from_ini(std::string path) +{ + std::ifstream stream(path); + std::stringstream buffer; + buffer << stream.rdbuf(); + std::string body = buffer.str(); + size_t start = body.find("PrusaSlicer "); + if (start == std::string::npos) + return boost::none; + body = body.substr(start + 12); + size_t end = body.find_first_of(" \n"); + if (end < body.size()) + body.resize(end); + return Semver::parse(body); +} + +void GUI_App::init_app_config() +{ + // Profiles for the alpha are stored into the PrusaSlicer-alpha directory to not mix with the current release. +// SetAppName(SLIC3R_APP_KEY); + SetAppName(SLIC3R_APP_KEY "-alpha"); +// SetAppName(SLIC3R_APP_KEY "-beta"); + + +// SetAppDisplayName(SLIC3R_APP_NAME); + + // Set the Slic3r data directory at the Slic3r XS module. + // Unix: ~/ .Slic3r + // Windows : "C:\Users\username\AppData\Roaming\Slic3r" or "C:\Documents and Settings\username\Application Data\Slic3r" + // Mac : "~/Library/Application Support/Slic3r" + + if (data_dir().empty()) { + #ifndef __linux__ + set_data_dir(wxStandardPaths::Get().GetUserDataDir().ToUTF8().data()); + #else + // Since version 2.3, config dir on Linux is in ${XDG_CONFIG_HOME}. + // https://github.com/prusa3d/PrusaSlicer/issues/2911 + wxString dir; + if (! wxGetEnv(wxS("XDG_CONFIG_HOME"), &dir) || dir.empty() ) + dir = wxFileName::GetHomeDir() + wxS("/.config"); + set_data_dir((dir + "/" + GetAppName()).ToUTF8().data()); + #endif + } else { + m_datadir_redefined = true; + } + + if (!app_config) + app_config = new AppConfig(is_editor() ? AppConfig::EAppMode::Editor : AppConfig::EAppMode::GCodeViewer); + + // load settings + m_app_conf_exists = app_config->exists(); + if (m_app_conf_exists) { + std::string error = app_config->load(); + if (!error.empty()) { + // Error while parsing config file. We'll customize the error message and rethrow to be displayed. + if (is_editor()) { + throw Slic3r::RuntimeError( + _u8L("Error parsing PrusaSlicer config file, it is probably corrupted. " + "Try to manually delete the file to recover from the error. Your user profiles will not be affected.") + + "\n\n" + app_config->config_path() + "\n\n" + error); + } + else { + throw Slic3r::RuntimeError( + _u8L("Error parsing PrusaGCodeViewer config file, it is probably corrupted. " + "Try to manually delete the file to recover from the error.") + + "\n\n" + app_config->config_path() + "\n\n" + error); + } + } + } +} + +// returns old config path to copy from if such exists, +// returns an empty string if such config path does not exists or if it cannot be loaded. +std::string GUI_App::check_older_app_config(Semver current_version, bool backup) +{ + std::string older_data_dir_path; + + // If the config folder is redefined - do not check + if (m_datadir_redefined) + return {}; + + // find other version app config (alpha / beta / release) + std::string config_path = app_config->config_path(); + boost::filesystem::path parent_file_path(config_path); + std::string filename = parent_file_path.filename().string(); + parent_file_path.remove_filename().remove_filename(); + + std::vector candidates; + + if (SLIC3R_APP_KEY "-alpha" != GetAppName()) candidates.emplace_back(parent_file_path / SLIC3R_APP_KEY "-alpha" / filename); + if (SLIC3R_APP_KEY "-beta" != GetAppName()) candidates.emplace_back(parent_file_path / SLIC3R_APP_KEY "-beta" / filename); + if (SLIC3R_APP_KEY != GetAppName()) candidates.emplace_back(parent_file_path / SLIC3R_APP_KEY / filename); + + Semver last_semver = current_version; + for (const auto& candidate : candidates) { + if (boost::filesystem::exists(candidate)) { + // parse + boost::optionalother_semver = parse_semver_from_ini(candidate.string()); + if (other_semver && *other_semver > last_semver) { + last_semver = *other_semver; + older_data_dir_path = candidate.parent_path().string(); + } + } + } + if (older_data_dir_path.empty()) + return {}; + BOOST_LOG_TRIVIAL(info) << "last app config file used: " << older_data_dir_path; + // ask about using older data folder + + InfoDialog msg(nullptr + , format_wxstr(_L("You are opening %1% version %2%."), SLIC3R_APP_NAME, SLIC3R_VERSION) + , backup ? + format_wxstr(_L( + "The active configuration was created by %1% %2%," + "\nwhile a newer configuration was found in %3%" + "\ncreated by %1% %4%." + "\n\nShall the newer configuration be imported?" + "\nIf so, your active configuration will be backed up before importing the new configuration." + ) + , SLIC3R_APP_NAME, current_version.to_string(), older_data_dir_path, last_semver.to_string()) + : format_wxstr(_L( + "An existing configuration was found in %3%" + "\ncreated by %1% %2%." + "\n\nShall this configuration be imported?" + ) + , SLIC3R_APP_NAME, last_semver.to_string(), older_data_dir_path) + , true, wxYES_NO); + + if (backup) { + msg.SetButtonLabel(wxID_YES, _L("Import")); + msg.SetButtonLabel(wxID_NO, _L("Don't import")); + } + + if (msg.ShowModal() == wxID_YES) { + std::string snapshot_id; + if (backup) { + const Config::Snapshot* snapshot{ nullptr }; + if (! GUI::Config::take_config_snapshot_cancel_on_error(*app_config, Config::Snapshot::SNAPSHOT_USER, "", + _u8L("Continue and import newer configuration?"), &snapshot)) + return {}; + if (snapshot) { + // Save snapshot ID before loading the alternate AppConfig, as loading the alternate AppConfig may fail. + snapshot_id = snapshot->id; + assert(! snapshot_id.empty()); + app_config->set("on_snapshot", snapshot_id); + } else + BOOST_LOG_TRIVIAL(error) << "Failed to take congiguration snapshot"; + } + + // load app config from older file + std::string error = app_config->load((boost::filesystem::path(older_data_dir_path) / filename).string()); + if (!error.empty()) { + // Error while parsing config file. We'll customize the error message and rethrow to be displayed. + if (is_editor()) { + throw Slic3r::RuntimeError( + _u8L("Error parsing PrusaSlicer config file, it is probably corrupted. " + "Try to manually delete the file to recover from the error. Your user profiles will not be affected.") + + "\n\n" + app_config->config_path() + "\n\n" + error); + } + else { + throw Slic3r::RuntimeError( + _u8L("Error parsing PrusaGCodeViewer config file, it is probably corrupted. " + "Try to manually delete the file to recover from the error.") + + "\n\n" + app_config->config_path() + "\n\n" + error); + } + } + if (!snapshot_id.empty()) + app_config->set("on_snapshot", snapshot_id); + m_app_conf_exists = true; + return older_data_dir_path; + } + return {}; +} + +void GUI_App::init_single_instance_checker(const std::string &name, const std::string &path) +{ + BOOST_LOG_TRIVIAL(debug) << "init wx instance checker " << name << " "<< path; + m_single_instance_checker = std::make_unique(boost::nowide::widen(name), boost::nowide::widen(path)); +} + +bool GUI_App::OnInit() +{ + try { + return on_init_inner(); + } catch (const std::exception&) { + generic_exception_handle(); + return false; + } +} + +bool GUI_App::on_init_inner() +{ + // Set initialization of image handlers before any UI actions - See GH issue #7469 + wxInitAllImageHandlers(); + +#if defined(_WIN32) && ! defined(_WIN64) + // Win32 32bit build. + if (wxPlatformInfo::Get().GetArchName().substr(0, 2) == "64") { + RichMessageDialog dlg(nullptr, + _L("You are running a 32 bit build of PrusaSlicer on 64-bit Windows." + "\n32 bit build of PrusaSlicer will likely not be able to utilize all the RAM available in the system." + "\nPlease download and install a 64 bit build of PrusaSlicer from https://www.prusa3d.cz/prusaslicer/." + "\nDo you wish to continue?"), + "PrusaSlicer", wxICON_QUESTION | wxYES_NO); + if (dlg.ShowModal() != wxID_YES) + return false; + } +#endif // _WIN64 + + // Forcing back menu icons under gtk2 and gtk3. Solution is based on: + // https://docs.gtk.org/gtk3/class.Settings.html + // see also https://docs.wxwidgets.org/3.0/classwx_menu_item.html#a2b5d6bcb820b992b1e4709facbf6d4fb + // TODO: Find workaround for GTK4 +#if defined(__WXGTK20__) || defined(__WXGTK3__) + g_object_set (gtk_settings_get_default (), "gtk-menu-images", TRUE, NULL); +#endif + + // Verify resources path + const wxString resources_dir = from_u8(Slic3r::resources_dir()); + wxCHECK_MSG(wxDirExists(resources_dir), false, + wxString::Format("Resources path does not exist or is not a directory: %s", resources_dir)); + +#ifdef __linux__ + if (! check_old_linux_datadir(GetAppName())) { + std::cerr << "Quitting, user chose to move their data to new location." << std::endl; + return false; + } +#endif + + // Enable this to get the default Win32 COMCTRL32 behavior of static boxes. +// wxSystemOptions::SetOption("msw.staticbox.optimized-paint", 0); + // Enable this to disable Windows Vista themes for all wxNotebooks. The themes seem to lead to terrible + // performance when working on high resolution multi-display setups. +// wxSystemOptions::SetOption("msw.notebook.themed-background", 0); + +// Slic3r::debugf "wxWidgets version %s, Wx version %s\n", wxVERSION_STRING, wxVERSION; + + // !!! Initialization of UI settings as a language, application color mode, fonts... have to be done before first UI action. + // Like here, before the show InfoDialog in check_older_app_config() + + // If load_language() fails, the application closes. + load_language(wxString(), true); +#ifdef _MSW_DARK_MODE + bool init_dark_color_mode = app_config->get("dark_color_mode") == "1"; + bool init_sys_menu_enabled = app_config->get("sys_menu_enabled") == "1"; + NppDarkMode::InitDarkMode(init_dark_color_mode, init_sys_menu_enabled); +#endif + // initialize label colors and fonts + init_label_colours(); + init_fonts(); + + std::string older_data_dir_path; + if (m_app_conf_exists) { + if (app_config->orig_version().valid() && app_config->orig_version() < *Semver::parse(SLIC3R_VERSION)) + // Only copying configuration if it was saved with a newer slicer than the one currently running. + older_data_dir_path = check_older_app_config(app_config->orig_version(), true); + } else { + // No AppConfig exists, fresh install. Always try to copy from an alternate location, don't make backup of the current configuration. + older_data_dir_path = check_older_app_config(Semver(), false); + } + +#ifdef _MSW_DARK_MODE + // app_config can be updated in check_older_app_config(), so check if dark_color_mode and sys_menu_enabled was changed + if (bool new_dark_color_mode = app_config->get("dark_color_mode") == "1"; + init_dark_color_mode != new_dark_color_mode) { + NppDarkMode::SetDarkMode(new_dark_color_mode); + init_label_colours(); + update_label_colours_from_appconfig(); + } + if (bool new_sys_menu_enabled = app_config->get("sys_menu_enabled") == "1"; + init_sys_menu_enabled != new_sys_menu_enabled) + NppDarkMode::SetSystemMenuForApp(new_sys_menu_enabled); +#endif + + if (is_editor()) { + std::string msg = Http::tls_global_init(); + std::string ssl_cert_store = app_config->get("tls_accepted_cert_store_location"); + bool ssl_accept = app_config->get("tls_cert_store_accepted") == "yes" && ssl_cert_store == Http::tls_system_cert_store(); + + if (!msg.empty() && !ssl_accept) { + RichMessageDialog + dlg(nullptr, + wxString::Format(_L("%s\nDo you want to continue?"), msg), + "PrusaSlicer", wxICON_QUESTION | wxYES_NO); + dlg.ShowCheckBox(_L("Remember my choice")); + if (dlg.ShowModal() != wxID_YES) return false; + + app_config->set("tls_cert_store_accepted", + dlg.IsCheckBoxChecked() ? "yes" : "no"); + app_config->set("tls_accepted_cert_store_location", + dlg.IsCheckBoxChecked() ? Http::tls_system_cert_store() : ""); + } + } + + SplashScreen* scrn = nullptr; + if (app_config->get("show_splash_screen") == "1") { + // make a bitmap with dark grey banner on the left side + wxBitmap bmp = SplashScreen::MakeBitmap(wxBitmap(from_u8(var(is_editor() ? "splashscreen.jpg" : "splashscreen-gcodepreview.jpg")), wxBITMAP_TYPE_JPEG)); + + // Detect position (display) to show the splash screen + // Now this position is equal to the mainframe position + wxPoint splashscreen_pos = wxDefaultPosition; + bool default_splashscreen_pos = true; + if (app_config->has("window_mainframe") && app_config->get("restore_win_position") == "1") { + auto metrics = WindowMetrics::deserialize(app_config->get("window_mainframe")); + default_splashscreen_pos = metrics == boost::none; + if (!default_splashscreen_pos) + splashscreen_pos = metrics->get_rect().GetPosition(); + } + + if (!default_splashscreen_pos) { + // workaround for crash related to the positioning of the window on secondary monitor + get_app_config()->set("restore_win_position", "crashed_at_splashscreen_pos"); + get_app_config()->save(); + } + + // create splash screen with updated bmp + scrn = new SplashScreen(bmp.IsOk() ? bmp : get_bmp_bundle("PrusaSlicer", 400)->GetPreferredBitmapSizeAtScale(1.0), + wxSPLASH_CENTRE_ON_SCREEN | wxSPLASH_TIMEOUT, 4000, splashscreen_pos); + + if (!default_splashscreen_pos) + // revert "restore_win_position" value if application wasn't crashed + get_app_config()->set("restore_win_position", "1"); +#ifndef __linux__ + wxYield(); +#endif + scrn->SetText(_L("Loading configuration")+ dots); + } + + preset_bundle = new PresetBundle(); + + // just checking for existence of Slic3r::data_dir is not enough : it may be an empty directory + // supplied as argument to --datadir; in that case we should still run the wizard + preset_bundle->setup_directories(); + + if (! older_data_dir_path.empty()) { + preset_bundle->import_newer_configs(older_data_dir_path); + app_config->save(); + } + + if (is_editor()) { +#ifdef __WXMSW__ + if (app_config->get("associate_3mf") == "1") + associate_3mf_files(); + if (app_config->get("associate_stl") == "1") + associate_stl_files(); +#endif // __WXMSW__ + + preset_updater = new PresetUpdater(); + Bind(EVT_SLIC3R_VERSION_ONLINE, &GUI_App::on_version_read, this); + Bind(EVT_SLIC3R_EXPERIMENTAL_VERSION_ONLINE, [this](const wxCommandEvent& evt) { + if (this->plater_ != nullptr && app_config->get("notify_release") == "all") { + std::string evt_string = into_u8(evt.GetString()); + if (*Semver::parse(SLIC3R_VERSION) < *Semver::parse(evt_string)) { + auto notif_type = (evt_string.find("beta") != std::string::npos ? NotificationType::NewBetaAvailable : NotificationType::NewAlphaAvailable); + this->plater_->get_notification_manager()->push_notification( notif_type + , NotificationManager::NotificationLevel::ImportantNotificationLevel + , Slic3r::format(_u8L("New prerelease version %1% is available."), evt_string) + , _u8L("See Releases page.") + , [](wxEvtHandler* evnthndlr) {wxGetApp().open_browser_with_warning_dialog("https://github.com/prusa3d/PrusaSlicer/releases"); return true; } + ); + } + } + }); + Bind(EVT_SLIC3R_APP_DOWNLOAD_PROGRESS, [this](const wxCommandEvent& evt) { + //lm:This does not force a render. The progress bar only updateswhen the mouse is moved. + if (this->plater_ != nullptr) + this->plater_->get_notification_manager()->set_download_progress_percentage((float)std::stoi(into_u8(evt.GetString())) / 100.f ); + }); + + Bind(EVT_SLIC3R_APP_DOWNLOAD_FAILED, [this](const wxCommandEvent& evt) { + if (this->plater_ != nullptr) + this->plater_->get_notification_manager()->close_notification_of_type(NotificationType::AppDownload); + if(!evt.GetString().IsEmpty()) + show_error(nullptr, evt.GetString()); + }); + + Bind(EVT_SLIC3R_APP_OPEN_FAILED, [](const wxCommandEvent& evt) { + show_error(nullptr, evt.GetString()); + }); + } + else { +#ifdef __WXMSW__ + if (app_config->get("associate_gcode") == "1") + associate_gcode_files(); +#endif // __WXMSW__ + } + + // Suppress the '- default -' presets. + preset_bundle->set_default_suppressed(app_config->get("no_defaults") == "1"); + try { + // Enable all substitutions (in both user and system profiles), but log the substitutions in user profiles only. + // If there are substitutions in system profiles, then a "reconfigure" event shall be triggered, which will force + // installation of a compatible system preset, thus nullifying the system preset substitutions. + init_params->preset_substitutions = preset_bundle->load_presets(*app_config, ForwardCompatibilitySubstitutionRule::EnableSystemSilent); + } catch (const std::exception &ex) { + show_error(nullptr, ex.what()); + } + +#ifdef WIN32 +#if !wxVERSION_EQUAL_OR_GREATER_THAN(3,1,3) + register_win32_dpi_event(); +#endif // !wxVERSION_EQUAL_OR_GREATER_THAN + register_win32_device_notification_event(); +#endif // WIN32 + + // Let the libslic3r know the callback, which will translate messages on demand. + Slic3r::I18N::set_translate_callback(libslic3r_translate_callback); + + // application frame + if (scrn && is_editor()) + scrn->SetText(_L("Preparing settings tabs") + dots); + + mainframe = new MainFrame(); + // hide settings tabs after first Layout + if (is_editor()) + mainframe->select_tab(size_t(0)); + + sidebar().obj_list()->init_objects(); // propagate model objects to object list +// update_mode(); // !!! do that later + SetTopWindow(mainframe); + + plater_->init_notification_manager(); + + m_printhost_job_queue.reset(new PrintHostJobQueue(mainframe->printhost_queue_dlg())); + + if (is_gcode_viewer()) { + mainframe->update_layout(); + if (plater_ != nullptr) + // ensure the selected technology is ptFFF + plater_->set_printer_technology(ptFFF); + } + else + load_current_presets(); + + // Save the active profiles as a "saved into project". + update_saved_preset_from_current_preset(); + + if (plater_ != nullptr) { + // Save the names of active presets and project specific config into ProjectDirtyStateManager. + plater_->reset_project_dirty_initial_presets(); + // Update Project dirty state, update application title bar. + plater_->update_project_dirty_from_presets(); + } + + mainframe->Show(true); + + obj_list()->set_min_height(); + + update_mode(); // update view mode after fix of the object_list size + +#ifdef __APPLE__ + other_instance_message_handler()->bring_instance_forward(); +#endif //__APPLE__ + + Bind(wxEVT_IDLE, [this](wxIdleEvent& event) + { + if (! plater_) + return; + + this->obj_manipul()->update_if_dirty(); + + // An ugly solution to GH #5537 in which GUI_App::init_opengl (normally called from events wxEVT_PAINT + // and wxEVT_SET_FOCUS before GUI_App::post_init is called) wasn't called before GUI_App::post_init and OpenGL wasn't initialized. +#ifdef __linux__ + if (! m_post_initialized && m_opengl_initialized) { +#else + if (! m_post_initialized) { +#endif + m_post_initialized = true; +#ifdef WIN32 + this->mainframe->register_win32_callbacks(); +#endif + this->post_init(); + } + + if (m_post_initialized && app_config->dirty() && app_config->get("autosave") == "1") + app_config->save(); + }); + + m_initialized = true; + + if (const std::string& crash_reason = app_config->get("restore_win_position"); + boost::starts_with(crash_reason,"crashed")) + { + wxString preferences_item = _L("Restore window position on start"); + InfoDialog dialog(nullptr, + _L("PrusaSlicer started after a crash"), + format_wxstr(_L("PrusaSlicer crashed last time when attempting to set window position.\n" + "We are sorry for the inconvenience, it unfortunately happens with certain multiple-monitor setups.\n" + "More precise reason for the crash: \"%1%\".\n" + "For more information see our GitHub issue tracker: \"%2%\" and \"%3%\"\n\n" + "To avoid this problem, consider disabling \"%4%\" in \"Preferences\". " + "Otherwise, the application will most likely crash again next time."), + "" + from_u8(crash_reason) + "", + "#2939", + "#5573", + "" + preferences_item + ""), + true, wxYES_NO); + + dialog.SetButtonLabel(wxID_YES, format_wxstr(_L("Disable \"%1%\""), preferences_item)); + dialog.SetButtonLabel(wxID_NO, format_wxstr(_L("Leave \"%1%\" enabled") , preferences_item)); + + auto answer = dialog.ShowModal(); + if (answer == wxID_YES) + app_config->set("restore_win_position", "0"); + else if (answer == wxID_NO) + app_config->set("restore_win_position", "1"); + app_config->save(); + } + + return true; +} + +unsigned GUI_App::get_colour_approx_luma(const wxColour &colour) +{ + double r = colour.Red(); + double g = colour.Green(); + double b = colour.Blue(); + + return std::round(std::sqrt( + r * r * .241 + + g * g * .691 + + b * b * .068 + )); +} + +bool GUI_App::dark_mode() +{ +#if __APPLE__ + // The check for dark mode returns false positive on 10.12 and 10.13, + // which allowed setting dark menu bar and dock area, which is + // is detected as dark mode. We must run on at least 10.14 where the + // proper dark mode was first introduced. + return wxPlatformInfo::Get().CheckOSVersion(10, 14) && mac_dark_mode(); +#else + return wxGetApp().app_config->get("dark_color_mode") == "1" ? true : check_dark_mode(); + //const unsigned luma = get_colour_approx_luma(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); + //return luma < 128; +#endif +} + +const wxColour GUI_App::get_label_default_clr_system() +{ + return dark_mode() ? wxColour(115, 220, 103) : wxColour(26, 132, 57); +} + +const wxColour GUI_App::get_label_default_clr_modified() +{ + return dark_mode() ? wxColour(253, 111, 40) : wxColour(252, 77, 1); +} + +void GUI_App::init_label_colours() +{ + m_color_label_modified = get_label_default_clr_modified(); + m_color_label_sys = get_label_default_clr_system(); + + bool is_dark_mode = dark_mode(); +#ifdef _WIN32 + m_color_label_default = is_dark_mode ? wxColour(250, 250, 250): wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT); + m_color_highlight_label_default = is_dark_mode ? wxColour(230, 230, 230): wxSystemSettings::GetColour(/*wxSYS_COLOUR_HIGHLIGHTTEXT*/wxSYS_COLOUR_WINDOWTEXT); + m_color_highlight_default = is_dark_mode ? wxColour(78, 78, 78) : wxSystemSettings::GetColour(wxSYS_COLOUR_3DLIGHT); + m_color_hovered_btn_label = is_dark_mode ? wxColour(253, 111, 40) : wxColour(252, 77, 1); + m_color_default_btn_label = is_dark_mode ? wxColour(255, 181, 100): wxColour(203, 61, 0); + m_color_selected_btn_bg = is_dark_mode ? wxColour(95, 73, 62) : wxColour(228, 220, 216); +#else + m_color_label_default = wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT); +#endif + m_color_window_default = is_dark_mode ? wxColour(43, 43, 43) : wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW); +} + +void GUI_App::update_label_colours_from_appconfig() +{ + if (app_config->has("label_clr_sys")) { + auto str = app_config->get("label_clr_sys"); + if (str != "") + m_color_label_sys = wxColour(str); + } + + if (app_config->has("label_clr_modified")) { + auto str = app_config->get("label_clr_modified"); + if (str != "") + m_color_label_modified = wxColour(str); + } +} + +void GUI_App::update_label_colours() +{ + for (Tab* tab : tabs_list) + tab->update_label_colours(); +} + +#ifdef _WIN32 +static bool is_focused(HWND hWnd) +{ + HWND hFocusedWnd = ::GetFocus(); + return hFocusedWnd && hWnd == hFocusedWnd; +} + +static bool is_default(wxWindow* win) +{ + wxTopLevelWindow* tlw = find_toplevel_parent(win); + if (!tlw) + return false; + + return win == tlw->GetDefaultItem(); +} +#endif + +void GUI_App::UpdateDarkUI(wxWindow* window, bool highlited/* = false*/, bool just_font/* = false*/) +{ +#ifdef _WIN32 + bool is_focused_button = false; + bool is_default_button = false; + if (wxButton* btn = dynamic_cast(window)) { + if (!(btn->GetWindowStyle() & wxNO_BORDER)) { + btn->SetWindowStyle(btn->GetWindowStyle() | wxNO_BORDER); + highlited = true; + } + // button marking + { + auto mark_button = [this, btn, highlited](const bool mark) { + if (btn->GetLabel().IsEmpty()) + btn->SetBackgroundColour(mark ? m_color_selected_btn_bg : highlited ? m_color_highlight_default : m_color_window_default); + else + btn->SetForegroundColour(mark ? m_color_hovered_btn_label : (is_default(btn) ? m_color_default_btn_label : m_color_label_default)); + btn->Refresh(); + btn->Update(); + }; + + // hovering + btn->Bind(wxEVT_ENTER_WINDOW, [mark_button](wxMouseEvent& event) { mark_button(true); event.Skip(); }); + btn->Bind(wxEVT_LEAVE_WINDOW, [mark_button, btn](wxMouseEvent& event) { mark_button(is_focused(btn->GetHWND())); event.Skip(); }); + // focusing + btn->Bind(wxEVT_SET_FOCUS, [mark_button](wxFocusEvent& event) { mark_button(true); event.Skip(); }); + btn->Bind(wxEVT_KILL_FOCUS, [mark_button](wxFocusEvent& event) { mark_button(false); event.Skip(); }); + + is_focused_button = is_focused(btn->GetHWND()); + is_default_button = is_default(btn); + if (is_focused_button || is_default_button) + mark_button(is_focused_button); + } + } + else if (wxTextCtrl* text = dynamic_cast(window)) { + if (text->GetBorder() != wxBORDER_SIMPLE) + text->SetWindowStyle(text->GetWindowStyle() | wxBORDER_SIMPLE); + } + else if (wxCheckListBox* list = dynamic_cast(window)) { + list->SetWindowStyle(list->GetWindowStyle() | wxBORDER_SIMPLE); + list->SetBackgroundColour(highlited ? m_color_highlight_default : m_color_window_default); + for (size_t i = 0; i < list->GetCount(); i++) + if (wxOwnerDrawn* item = list->GetItem(i)) { + item->SetBackgroundColour(highlited ? m_color_highlight_default : m_color_window_default); + item->SetTextColour(m_color_label_default); + } + return; + } + else if (dynamic_cast(window)) + window->SetWindowStyle(window->GetWindowStyle() | wxBORDER_SIMPLE); + + if (!just_font) + window->SetBackgroundColour(highlited ? m_color_highlight_default : m_color_window_default); + if (!is_focused_button && !is_default_button) + window->SetForegroundColour(m_color_label_default); +#endif +} + +// recursive function for scaling fonts for all controls in Window +#ifdef _WIN32 +static void update_dark_children_ui(wxWindow* window, bool just_buttons_update = false) +{ + bool is_btn = dynamic_cast(window) != nullptr; + if (!(just_buttons_update && !is_btn)) + wxGetApp().UpdateDarkUI(window, is_btn); + + auto children = window->GetChildren(); + for (auto child : children) { + update_dark_children_ui(child); + } +} +#endif + +// Note: Don't use this function for Dialog contains ScalableButtons +void GUI_App::UpdateDlgDarkUI(wxDialog* dlg, bool just_buttons_update/* = false*/) +{ +#ifdef _WIN32 + update_dark_children_ui(dlg, just_buttons_update); +#endif +} +void GUI_App::UpdateDVCDarkUI(wxDataViewCtrl* dvc, bool highlited/* = false*/) +{ +#ifdef _WIN32 + UpdateDarkUI(dvc, highlited ? dark_mode() : false); +#ifdef _MSW_DARK_MODE + dvc->RefreshHeaderDarkMode(&m_normal_font); +#endif //_MSW_DARK_MODE + if (dvc->HasFlag(wxDV_ROW_LINES)) + dvc->SetAlternateRowColour(m_color_highlight_default); + if (dvc->GetBorder() != wxBORDER_SIMPLE) + dvc->SetWindowStyle(dvc->GetWindowStyle() | wxBORDER_SIMPLE); +#endif +} + +void GUI_App::UpdateAllStaticTextDarkUI(wxWindow* parent) +{ +#ifdef _WIN32 + wxGetApp().UpdateDarkUI(parent); + + auto children = parent->GetChildren(); + for (auto child : children) { + if (dynamic_cast(child)) + child->SetForegroundColour(m_color_label_default); + } +#endif +} + +void GUI_App::init_fonts() +{ + m_small_font = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT); + m_bold_font = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT).Bold(); + m_normal_font = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT); + +#ifdef __WXMAC__ + m_small_font.SetPointSize(11); + m_bold_font.SetPointSize(13); +#endif /*__WXMAC__*/ + + // wxSYS_OEM_FIXED_FONT and wxSYS_ANSI_FIXED_FONT use the same as + // DEFAULT in wxGtk. Use the TELETYPE family as a work-around + m_code_font = wxFont(wxFontInfo().Family(wxFONTFAMILY_TELETYPE)); + m_code_font.SetPointSize(m_normal_font.GetPointSize()); +} + +void GUI_App::update_fonts(const MainFrame *main_frame) +{ + /* Only normal and bold fonts are used for an application rescale, + * because of under MSW small and normal fonts are the same. + * To avoid same rescaling twice, just fill this values + * from rescaled MainFrame + */ + if (main_frame == nullptr) + main_frame = this->mainframe; + m_normal_font = main_frame->normal_font(); + m_small_font = m_normal_font; + m_bold_font = main_frame->normal_font().Bold(); + m_link_font = m_bold_font.Underlined(); + m_em_unit = main_frame->em_unit(); + m_code_font.SetPointSize(m_normal_font.GetPointSize()); +} + +void GUI_App::set_label_clr_modified(const wxColour& clr) +{ + if (m_color_label_modified == clr) + return; + m_color_label_modified = clr; + const std::string str = encode_color(ColorRGB(clr.Red(), clr.Green(), clr.Blue())); + app_config->set("label_clr_modified", str); + app_config->save(); +} + +void GUI_App::set_label_clr_sys(const wxColour& clr) +{ + if (m_color_label_sys == clr) + return; + m_color_label_sys = clr; + const std::string str = encode_color(ColorRGB(clr.Red(), clr.Green(), clr.Blue())); + app_config->set("label_clr_sys", str); + app_config->save(); +} + +bool GUI_App::tabs_as_menu() const +{ + return app_config->get("tabs_as_menu") == "1"; // || dark_mode(); +} + +wxSize GUI_App::get_min_size() const +{ + return wxSize(76*m_em_unit, 49 * m_em_unit); +} + +float GUI_App::toolbar_icon_scale(const bool is_limited/* = false*/) const +{ +#ifdef __APPLE__ + const float icon_sc = 1.0f; // for Retina display will be used its own scale +#else + const float icon_sc = m_em_unit*0.1f; +#endif // __APPLE__ + + const std::string& use_val = app_config->get("use_custom_toolbar_size"); + const std::string& val = app_config->get("custom_toolbar_size"); + const std::string& auto_val = app_config->get("auto_toolbar_size"); + + if (val.empty() || auto_val.empty() || use_val.empty()) + return icon_sc; + + int int_val = use_val == "0" ? 100 : atoi(val.c_str()); + // correct value in respect to auto_toolbar_size + int_val = std::min(atoi(auto_val.c_str()), int_val); + + if (is_limited && int_val < 50) + int_val = 50; + + return 0.01f * int_val * icon_sc; +} + +void GUI_App::set_auto_toolbar_icon_scale(float scale) const +{ +#ifdef __APPLE__ + const float icon_sc = 1.0f; // for Retina display will be used its own scale +#else + const float icon_sc = m_em_unit * 0.1f; +#endif // __APPLE__ + + long int_val = std::min(int(std::lround(scale / icon_sc * 100)), 100); + std::string val = std::to_string(int_val); + + app_config->set("auto_toolbar_size", val); +} + +// check user printer_presets for the containing information about "Print Host upload" +void GUI_App::check_printer_presets() +{ + std::vector preset_names = PhysicalPrinter::presets_with_print_host_information(preset_bundle->printers); + if (preset_names.empty()) + return; + + wxString msg_text = _L("You have the following presets with saved options for \"Print Host upload\"") + ":"; + for (const std::string& preset_name : preset_names) + msg_text += "\n \"" + from_u8(preset_name) + "\","; + msg_text.RemoveLast(); + msg_text += "\n\n" + _L("But since this version of PrusaSlicer we don't show this information in Printer Settings anymore.\n" + "Settings will be available in physical printers settings.") + "\n\n" + + _L("By default new Printer devices will be named as \"Printer N\" during its creation.\n" + "Note: This name can be changed later from the physical printers settings"); + + //wxMessageDialog(nullptr, msg_text, _L("Information"), wxOK | wxICON_INFORMATION).ShowModal(); + MessageDialog(nullptr, msg_text, _L("Information"), wxOK | wxICON_INFORMATION).ShowModal(); + + preset_bundle->physical_printers.load_printers_from_presets(preset_bundle->printers); +} + +void GUI_App::recreate_GUI(const wxString& msg_name) +{ + m_is_recreating_gui = true; + + mainframe->shutdown(); + + wxProgressDialog dlg(msg_name, msg_name, 100, nullptr, wxPD_AUTO_HIDE); + dlg.Pulse(); + dlg.Update(10, _L("Recreating") + dots); + + MainFrame *old_main_frame = mainframe; + mainframe = new MainFrame(); + if (is_editor()) + // hide settings tabs after first Layout + mainframe->select_tab(size_t(0)); + // Propagate model objects to object list. + sidebar().obj_list()->init_objects(); + SetTopWindow(mainframe); + + dlg.Update(30, _L("Recreating") + dots); + old_main_frame->Destroy(); + + dlg.Update(80, _L("Loading of current presets") + dots); + m_printhost_job_queue.reset(new PrintHostJobQueue(mainframe->printhost_queue_dlg())); + load_current_presets(); + mainframe->Show(true); + + dlg.Update(90, _L("Loading of a mode view") + dots); + + obj_list()->set_min_height(); + update_mode(); + + // #ys_FIXME_delete_after_testing Do we still need this ? +// CallAfter([]() { +// // Run the config wizard, don't offer the "reset user profile" checkbox. +// config_wizard_startup(true); +// }); + + m_is_recreating_gui = false; +} + +void GUI_App::system_info() +{ + SysInfoDialog dlg; + dlg.ShowModal(); +} + +void GUI_App::keyboard_shortcuts() +{ + KBShortcutsDialog dlg; + dlg.ShowModal(); +} + +// static method accepting a wxWindow object as first parameter +bool GUI_App::catch_error(std::function cb, + // wxMessageDialog* message_dialog, + const std::string& err /*= ""*/) +{ + if (!err.empty()) { + if (cb) + cb(); + // if (message_dialog) + // message_dialog->(err, "Error", wxOK | wxICON_ERROR); + show_error(/*this*/nullptr, err); + return true; + } + return false; +} + +// static method accepting a wxWindow object as first parameter +void fatal_error(wxWindow* parent) +{ + show_error(parent, ""); + // exit 1; // #ys_FIXME +} + +#ifdef _WIN32 + +#ifdef _MSW_DARK_MODE +static void update_scrolls(wxWindow* window) +{ + wxWindowList::compatibility_iterator node = window->GetChildren().GetFirst(); + while (node) + { + wxWindow* win = node->GetData(); + if (dynamic_cast(win) || + dynamic_cast(win) || + dynamic_cast(win)) + NppDarkMode::SetDarkExplorerTheme(win->GetHWND()); + + update_scrolls(win); + node = node->GetNext(); + } +} +#endif //_MSW_DARK_MODE + + +#ifdef _MSW_DARK_MODE +void GUI_App::force_menu_update() +{ + NppDarkMode::SetSystemMenuForApp(app_config->get("sys_menu_enabled") == "1"); +} +#endif //_MSW_DARK_MODE + +void GUI_App::force_colors_update() +{ +#ifdef _MSW_DARK_MODE + NppDarkMode::SetDarkMode(app_config->get("dark_color_mode") == "1"); + if (WXHWND wxHWND = wxToolTip::GetToolTipCtrl()) + NppDarkMode::SetDarkExplorerTheme((HWND)wxHWND); + NppDarkMode::SetDarkTitleBar(mainframe->GetHWND()); + NppDarkMode::SetDarkTitleBar(mainframe->m_settings_dialog.GetHWND()); +#endif //_MSW_DARK_MODE + m_force_colors_update = true; +} +#endif //_WIN32 + +// Called after the Preferences dialog is closed and the program settings are saved. +// Update the UI based on the current preferences. +void GUI_App::update_ui_from_settings() +{ + update_label_colours(); +#ifdef _WIN32 + // Upadte UI colors before Update UI from settings + if (m_force_colors_update) { + m_force_colors_update = false; + mainframe->force_color_changed(); + mainframe->diff_dialog.force_color_changed(); + mainframe->preferences_dialog->force_color_changed(); + mainframe->printhost_queue_dlg()->force_color_changed(); +#ifdef _MSW_DARK_MODE + update_scrolls(mainframe); + if (mainframe->is_dlg_layout()) { + // update for tabs bar + UpdateDarkUI(&mainframe->m_settings_dialog); + mainframe->m_settings_dialog.Fit(); + mainframe->m_settings_dialog.Refresh(); + // update scrollbars + update_scrolls(&mainframe->m_settings_dialog); + } +#endif //_MSW_DARK_MODE + } +#endif + mainframe->update_ui_from_settings(); +} + +void GUI_App::persist_window_geometry(wxTopLevelWindow *window, bool default_maximized) +{ + const std::string name = into_u8(window->GetName()); + + window->Bind(wxEVT_CLOSE_WINDOW, [=](wxCloseEvent &event) { + window_pos_save(window, name); + event.Skip(); + }); + + window_pos_restore(window, name, default_maximized); + + on_window_geometry(window, [=]() { + window_pos_sanitize(window); + }); +} + +void GUI_App::load_project(wxWindow *parent, wxString& input_file) const +{ + input_file.Clear(); + wxFileDialog dialog(parent ? parent : GetTopWindow(), + _L("Choose one file (3MF/AMF):"), + app_config->get_last_dir(), "", + file_wildcards(FT_PROJECT), wxFD_OPEN | wxFD_FILE_MUST_EXIST); + + if (dialog.ShowModal() == wxID_OK) + input_file = dialog.GetPath(); +} + +void GUI_App::import_model(wxWindow *parent, wxArrayString& input_files) const +{ + input_files.Clear(); + wxFileDialog dialog(parent ? parent : GetTopWindow(), + _L("Choose one or more files (STL/OBJ/AMF/3MF/PRUSA):"), + from_u8(app_config->get_last_dir()), "", + file_wildcards(FT_MODEL), wxFD_OPEN | wxFD_MULTIPLE | wxFD_FILE_MUST_EXIST); + + if (dialog.ShowModal() == wxID_OK) + dialog.GetPaths(input_files); +} + +void GUI_App::load_gcode(wxWindow* parent, wxString& input_file) const +{ + input_file.Clear(); + wxFileDialog dialog(parent ? parent : GetTopWindow(), + _L("Choose one file (GCODE/.GCO/.G/.ngc/NGC):"), + app_config->get_last_dir(), "", + file_wildcards(FT_GCODE), wxFD_OPEN | wxFD_FILE_MUST_EXIST); + + if (dialog.ShowModal() == wxID_OK) + input_file = dialog.GetPath(); +} + +bool GUI_App::switch_language() +{ + if (select_language()) { + recreate_GUI(_L("Changing of an application language") + dots); + return true; + } else { + return false; + } +} + +#ifdef __linux__ +static const wxLanguageInfo* linux_get_existing_locale_language(const wxLanguageInfo* language, + const wxLanguageInfo* system_language) +{ + constexpr size_t max_len = 50; + char path[max_len] = ""; + std::vector locales; + const std::string lang_prefix = into_u8(language->CanonicalName.BeforeFirst('_')); + + // Call locale -a so we can parse the output to get the list of available locales + // We expect lines such as "en_US.utf8". Pick ones starting with the language code + // we are switching to. Lines with different formatting will be removed later. + FILE* fp = popen("locale -a", "r"); + if (fp != NULL) { + while (fgets(path, max_len, fp) != NULL) { + std::string line(path); + line = line.substr(0, line.find('\n')); + if (boost::starts_with(line, lang_prefix)) + locales.push_back(line); + } + pclose(fp); + } + + // locales now contain all candidates for this language. + // Sort them so ones containing anything about UTF-8 are at the end. + std::sort(locales.begin(), locales.end(), [](const std::string& a, const std::string& b) + { + auto has_utf8 = [](const std::string & s) { + auto S = boost::to_upper_copy(s); + return S.find("UTF8") != std::string::npos || S.find("UTF-8") != std::string::npos; + }; + return ! has_utf8(a) && has_utf8(b); + }); + + // Remove the suffix behind a dot, if there is one. + for (std::string& s : locales) + s = s.substr(0, s.find(".")); + + // We just hope that dear Linux "locale -a" returns country codes + // in ISO 3166-1 alpha-2 code (two letter) format. + // https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes + // To be sure, remove anything not looking as expected + // (any number of lowercase letters, underscore, two uppercase letters). + locales.erase(std::remove_if(locales.begin(), + locales.end(), + [](const std::string& s) { + return ! std::regex_match(s, + std::regex("^[a-z]+_[A-Z]{2}$")); + }), + locales.end()); + + // Is there a candidate matching a country code of a system language? Move it to the end, + // while maintaining the order of matches, so that the best match ends up at the very end. + std::string system_country = "_" + into_u8(system_language->CanonicalName.AfterFirst('_')).substr(0, 2); + int cnt = locales.size(); + for (int i=0; iempty()) { + const std::string &locale = *it; + const wxLanguageInfo* lang = wxLocale::FindLanguageInfo(from_u8(locale)); + if (wxLocale::IsAvailable(lang->Language)) + return lang; + } + return language; +} +#endif + +int GUI_App::GetSingleChoiceIndex(const wxString& message, + const wxString& caption, + const wxArrayString& choices, + int initialSelection) +{ +#ifdef _WIN32 + wxSingleChoiceDialog dialog(nullptr, message, caption, choices); + wxGetApp().UpdateDlgDarkUI(&dialog); + + dialog.SetSelection(initialSelection); + return dialog.ShowModal() == wxID_OK ? dialog.GetSelection() : -1; +#else + return wxGetSingleChoiceIndex(message, caption, choices, initialSelection); +#endif +} + +// select language from the list of installed languages +bool GUI_App::select_language() +{ + wxArrayString translations = wxTranslations::Get()->GetAvailableTranslations(SLIC3R_APP_KEY); + std::vector language_infos; + language_infos.emplace_back(wxLocale::GetLanguageInfo(wxLANGUAGE_ENGLISH)); + for (size_t i = 0; i < translations.GetCount(); ++ i) { + const wxLanguageInfo *langinfo = wxLocale::FindLanguageInfo(translations[i]); + if (langinfo != nullptr) + language_infos.emplace_back(langinfo); + } + sort_remove_duplicates(language_infos); + std::sort(language_infos.begin(), language_infos.end(), [](const wxLanguageInfo* l, const wxLanguageInfo* r) { return l->Description < r->Description; }); + + wxArrayString names; + names.Alloc(language_infos.size()); + + // Some valid language should be selected since the application start up. + const wxLanguage current_language = wxLanguage(m_wxLocale->GetLanguage()); + int init_selection = -1; + int init_selection_alt = -1; + int init_selection_default = -1; + for (size_t i = 0; i < language_infos.size(); ++ i) { + if (wxLanguage(language_infos[i]->Language) == current_language) + // The dictionary matches the active language and country. + init_selection = i; + else if ((language_infos[i]->CanonicalName.BeforeFirst('_') == m_wxLocale->GetCanonicalName().BeforeFirst('_')) || + // if the active language is Slovak, mark the Czech language as active. + (language_infos[i]->CanonicalName.BeforeFirst('_') == "cs" && m_wxLocale->GetCanonicalName().BeforeFirst('_') == "sk")) + // The dictionary matches the active language, it does not necessarily match the country. + init_selection_alt = i; + if (language_infos[i]->CanonicalName.BeforeFirst('_') == "en") + // This will be the default selection if the active language does not match any dictionary. + init_selection_default = i; + names.Add(language_infos[i]->Description); + } + if (init_selection == -1) + // This is the dictionary matching the active language. + init_selection = init_selection_alt; + if (init_selection != -1) + // This is the language to highlight in the choice dialog initially. + init_selection_default = init_selection; + + const long index = GetSingleChoiceIndex(_L("Select the language"), _L("Language"), names, init_selection_default); + // Try to load a new language. + if (index != -1 && (init_selection == -1 || init_selection != index)) { + const wxLanguageInfo *new_language_info = language_infos[index]; + if (this->load_language(new_language_info->CanonicalName, false)) { + // Save language at application config. + // Which language to save as the selected dictionary language? + // 1) Hopefully the language set to wxTranslations by this->load_language(), but that API is weird and we don't want to rely on its + // stability in the future: + // wxTranslations::Get()->GetBestTranslation(SLIC3R_APP_KEY, wxLANGUAGE_ENGLISH); + // 2) Current locale language may not match the dictionary name, see GH issue #3901 + // m_wxLocale->GetCanonicalName() + // 3) new_language_info->CanonicalName is a safe bet. It points to a valid dictionary name. + app_config->set("translation_language", new_language_info->CanonicalName.ToUTF8().data()); + app_config->save(); + return true; + } + } + + return false; +} + +// Load gettext translation files and activate them at the start of the application, +// based on the "translation_language" key stored in the application config. +bool GUI_App::load_language(wxString language, bool initial) +{ + if (initial) { + // There is a static list of lookup path prefixes in wxWidgets. Add ours. + wxFileTranslationsLoader::AddCatalogLookupPathPrefix(from_u8(localization_dir())); + // Get the active language from PrusaSlicer.ini, or empty string if the key does not exist. + language = app_config->get("translation_language"); + if (! language.empty()) + BOOST_LOG_TRIVIAL(trace) << boost::format("translation_language provided by PrusaSlicer.ini: %1%") % language; + + // Get the system language. + { + const wxLanguage lang_system = wxLanguage(wxLocale::GetSystemLanguage()); + if (lang_system != wxLANGUAGE_UNKNOWN) { + m_language_info_system = wxLocale::GetLanguageInfo(lang_system); + BOOST_LOG_TRIVIAL(trace) << boost::format("System language detected (user locales and such): %1%") % m_language_info_system->CanonicalName.ToUTF8().data(); + } + } + { + // Allocating a temporary locale will switch the default wxTranslations to its internal wxTranslations instance. + wxLocale temp_locale; +#ifdef __WXOSX__ + // ysFIXME - temporary workaround till it isn't fixed in wxWidgets: + // Use English as an initial language, because of under OSX it try to load "inappropriate" language for wxLANGUAGE_DEFAULT. + // For example in our case it's trying to load "en_CZ" and as a result PrusaSlicer catch warning message. + // But wxWidgets guys work on it. + temp_locale.Init(wxLANGUAGE_ENGLISH); +#else + temp_locale.Init(); +#endif // __WXOSX__ + // Set the current translation's language to default, otherwise GetBestTranslation() may not work (see the wxWidgets source code). + wxTranslations::Get()->SetLanguage(wxLANGUAGE_DEFAULT); + // Let the wxFileTranslationsLoader enumerate all translation dictionaries for PrusaSlicer + // and try to match them with the system specific "preferred languages". + // There seems to be a support for that on Windows and OSX, while on Linuxes the code just returns wxLocale::GetSystemLanguage(). + // The last parameter gets added to the list of detected dictionaries. This is a workaround + // for not having the English dictionary. Let's hope wxWidgets of various versions process this call the same way. + wxString best_language = wxTranslations::Get()->GetBestTranslation(SLIC3R_APP_KEY, wxLANGUAGE_ENGLISH); + if (! best_language.IsEmpty()) { + m_language_info_best = wxLocale::FindLanguageInfo(best_language); + BOOST_LOG_TRIVIAL(trace) << boost::format("Best translation language detected (may be different from user locales): %1%") % m_language_info_best->CanonicalName.ToUTF8().data(); + } + #ifdef __linux__ + wxString lc_all; + if (wxGetEnv("LC_ALL", &lc_all) && ! lc_all.IsEmpty()) { + // Best language returned by wxWidgets on Linux apparently does not respect LC_ALL. + // Disregard the "best" suggestion in case LC_ALL is provided. + m_language_info_best = nullptr; + } + #endif + } + } + + const wxLanguageInfo *language_info = language.empty() ? nullptr : wxLocale::FindLanguageInfo(language); + if (! language.empty() && (language_info == nullptr || language_info->CanonicalName.empty())) { + // Fix for wxWidgets issue, where the FindLanguageInfo() returns locales with undefined ANSII code (wxLANGUAGE_KONKANI or wxLANGUAGE_MANIPURI). + language_info = nullptr; + BOOST_LOG_TRIVIAL(error) << boost::format("Language code \"%1%\" is not supported") % language.ToUTF8().data(); + } + + if (language_info != nullptr && language_info->LayoutDirection == wxLayout_RightToLeft) { + BOOST_LOG_TRIVIAL(trace) << boost::format("The following language code requires right to left layout, which is not supported by PrusaSlicer: %1%") % language_info->CanonicalName.ToUTF8().data(); + language_info = nullptr; + } + + if (language_info == nullptr) { + // PrusaSlicer does not support the Right to Left languages yet. + if (m_language_info_system != nullptr && m_language_info_system->LayoutDirection != wxLayout_RightToLeft) + language_info = m_language_info_system; + if (m_language_info_best != nullptr && m_language_info_best->LayoutDirection != wxLayout_RightToLeft) + language_info = m_language_info_best; + if (language_info == nullptr) + language_info = wxLocale::GetLanguageInfo(wxLANGUAGE_ENGLISH_US); + } + + BOOST_LOG_TRIVIAL(trace) << boost::format("Switching wxLocales to %1%") % language_info->CanonicalName.ToUTF8().data(); + + // Alternate language code. + wxLanguage language_dict = wxLanguage(language_info->Language); + if (language_info->CanonicalName.BeforeFirst('_') == "sk") { + // Slovaks understand Czech well. Give them the Czech translation. + language_dict = wxLANGUAGE_CZECH; + BOOST_LOG_TRIVIAL(trace) << "Using Czech dictionaries for Slovak language"; + } + + // Select language for locales. This language may be different from the language of the dictionary. + if (language_info == m_language_info_best || language_info == m_language_info_system) { + // The current language matches user's default profile exactly. That's great. + } else if (m_language_info_best != nullptr && language_info->CanonicalName.BeforeFirst('_') == m_language_info_best->CanonicalName.BeforeFirst('_')) { + // Use whatever the operating system recommends, if it the language code of the dictionary matches the recommended language. + // This allows a Swiss guy to use a German dictionary without forcing him to German locales. + language_info = m_language_info_best; + } else if (m_language_info_system != nullptr && language_info->CanonicalName.BeforeFirst('_') == m_language_info_system->CanonicalName.BeforeFirst('_')) + language_info = m_language_info_system; + +#ifdef __linux__ + // If we can't find this locale , try to use different one for the language + // instead of just reporting that it is impossible to switch. + if (! wxLocale::IsAvailable(language_info->Language)) { + std::string original_lang = into_u8(language_info->CanonicalName); + language_info = linux_get_existing_locale_language(language_info, m_language_info_system); + BOOST_LOG_TRIVIAL(trace) << boost::format("Can't switch language to %1% (missing locales). Using %2% instead.") + % original_lang % language_info->CanonicalName.ToUTF8().data(); + } +#endif + + if (! wxLocale::IsAvailable(language_info->Language)) { + // Loading the language dictionary failed. + wxString message = "Switching PrusaSlicer to language " + language_info->CanonicalName + " failed."; +#if !defined(_WIN32) && !defined(__APPLE__) + // likely some linux system + message += "\nYou may need to reconfigure the missing locales, likely by running the \"locale-gen\" and \"dpkg-reconfigure locales\" commands.\n"; +#endif + if (initial) + message + "\n\nApplication will close."; + wxMessageBox(message, "PrusaSlicer - Switching language failed", wxOK | wxICON_ERROR); + if (initial) + std::exit(EXIT_FAILURE); + else + return false; + } + + // Release the old locales, create new locales. + //FIXME wxWidgets cause havoc if the current locale is deleted. We just forget it causing memory leaks for now. + m_wxLocale.release(); + m_wxLocale = Slic3r::make_unique(); + m_wxLocale->Init(language_info->Language); + // Override language at the active wxTranslations class (which is stored in the active m_wxLocale) + // to load possibly different dictionary, for example, load Czech dictionary for Slovak language. + wxTranslations::Get()->SetLanguage(language_dict); + m_wxLocale->AddCatalog(SLIC3R_APP_KEY); + m_imgui->set_language(into_u8(language_info->CanonicalName)); + //FIXME This is a temporary workaround, the correct solution is to switch to "C" locale during file import / export only. + //wxSetlocale(LC_NUMERIC, "C"); + Preset::update_suffix_modified((" (" + _L("modified") + ")").ToUTF8().data()); + return true; +} + +Tab* GUI_App::get_tab(Preset::Type type) +{ + for (Tab* tab: tabs_list) + if (tab->type() == type) + return tab->completed() ? tab : nullptr; // To avoid actions with no-completed Tab + return nullptr; +} + +ConfigOptionMode GUI_App::get_mode() +{ + if (!app_config->has("view_mode")) + return comSimple; + + const auto mode = app_config->get("view_mode"); + return mode == "expert" ? comExpert : + mode == "simple" ? comSimple : comAdvanced; +} + +void GUI_App::save_mode(const /*ConfigOptionMode*/int mode) +{ + const std::string mode_str = mode == comExpert ? "expert" : + mode == comSimple ? "simple" : "advanced"; + app_config->set("view_mode", mode_str); + app_config->save(); + update_mode(); +} + +// Update view mode according to selected menu +void GUI_App::update_mode() +{ + sidebar().update_mode(); + +#ifdef _WIN32 //_MSW_DARK_MODE + if (!wxGetApp().tabs_as_menu()) + dynamic_cast(mainframe->m_tabpanel)->UpdateMode(); +#endif + + for (auto tab : tabs_list) + tab->update_mode(); + + plater()->update_menus(); + plater()->canvas3D()->update_gizmos_on_off_state(); +} + +void GUI_App::add_config_menu(wxMenuBar *menu) +{ + auto local_menu = new wxMenu(); + wxWindowID config_id_base = wxWindow::NewControlId(int(ConfigMenuCnt)); + + const auto config_wizard_name = _(ConfigWizard::name(true)); + const auto config_wizard_tooltip = from_u8((boost::format(_utf8(L("Run %s"))) % config_wizard_name).str()); + // Cmd+, is standard on OS X - what about other operating systems? + if (is_editor()) { + local_menu->Append(config_id_base + ConfigMenuWizard, config_wizard_name + dots, config_wizard_tooltip); + local_menu->Append(config_id_base + ConfigMenuSnapshots, _L("&Configuration Snapshots") + dots, _L("Inspect / activate configuration snapshots")); + local_menu->Append(config_id_base + ConfigMenuTakeSnapshot, _L("Take Configuration &Snapshot"), _L("Capture a configuration snapshot")); + local_menu->Append(config_id_base + ConfigMenuUpdateConf, _L("Check for Configuration Updates"), _L("Check for configuration updates")); + local_menu->Append(config_id_base + ConfigMenuUpdateApp, _L("Check for Application Updates"), _L("Check for new version of application")); +#if defined(__linux__) && defined(SLIC3R_DESKTOP_INTEGRATION) + //if (DesktopIntegrationDialog::integration_possible()) + local_menu->Append(config_id_base + ConfigMenuDesktopIntegration, _L("Desktop Integration"), _L("Desktop Integration")); +#endif //(__linux__) && defined(SLIC3R_DESKTOP_INTEGRATION) + local_menu->AppendSeparator(); + } + local_menu->Append(config_id_base + ConfigMenuPreferences, _L("&Preferences") + dots + +#ifdef __APPLE__ + "\tCtrl+,", +#else + "\tCtrl+P", +#endif + _L("Application preferences")); + wxMenu* mode_menu = nullptr; + if (is_editor()) { + local_menu->AppendSeparator(); + mode_menu = new wxMenu(); + mode_menu->AppendRadioItem(config_id_base + ConfigMenuModeSimple, _L("Simple"), _L("Simple View Mode")); +// mode_menu->AppendRadioItem(config_id_base + ConfigMenuModeAdvanced, _L("Advanced"), _L("Advanced View Mode")); + mode_menu->AppendRadioItem(config_id_base + ConfigMenuModeAdvanced, _CTX(L_CONTEXT("Advanced", "Mode"), "Mode"), _L("Advanced View Mode")); + mode_menu->AppendRadioItem(config_id_base + ConfigMenuModeExpert, _L("Expert"), _L("Expert View Mode")); + Bind(wxEVT_UPDATE_UI, [this](wxUpdateUIEvent& evt) { if (get_mode() == comSimple) evt.Check(true); }, config_id_base + ConfigMenuModeSimple); + Bind(wxEVT_UPDATE_UI, [this](wxUpdateUIEvent& evt) { if (get_mode() == comAdvanced) evt.Check(true); }, config_id_base + ConfigMenuModeAdvanced); + Bind(wxEVT_UPDATE_UI, [this](wxUpdateUIEvent& evt) { if (get_mode() == comExpert) evt.Check(true); }, config_id_base + ConfigMenuModeExpert); + + local_menu->AppendSubMenu(mode_menu, _L("Mode"), wxString::Format(_L("%s View Mode"), SLIC3R_APP_NAME)); + } + local_menu->AppendSeparator(); + local_menu->Append(config_id_base + ConfigMenuLanguage, _L("&Language")); + if (is_editor()) { + local_menu->AppendSeparator(); + local_menu->Append(config_id_base + ConfigMenuFlashFirmware, _L("Flash Printer &Firmware"), _L("Upload a firmware image into an Arduino based printer")); + // TODO: for when we're able to flash dictionaries + // local_menu->Append(config_id_base + FirmwareMenuDict, _L("Flash Language File"), _L("Upload a language dictionary file into a Prusa printer")); + } + + local_menu->Bind(wxEVT_MENU, [this, config_id_base](wxEvent &event) { + switch (event.GetId() - config_id_base) { + case ConfigMenuWizard: + run_wizard(ConfigWizard::RR_USER); + break; + case ConfigMenuUpdateConf: + check_updates(true); + break; + case ConfigMenuUpdateApp: + app_version_check(true); + break; +#ifdef __linux__ + case ConfigMenuDesktopIntegration: + show_desktop_integration_dialog(); + break; +#endif + case ConfigMenuTakeSnapshot: + // Take a configuration snapshot. + if (wxString action_name = _L("Taking a configuration snapshot"); + check_and_save_current_preset_changes(action_name, _L("Some presets are modified and the unsaved changes will not be captured by the configuration snapshot."), false, true)) { + wxTextEntryDialog dlg(nullptr, action_name, _L("Snapshot name")); + UpdateDlgDarkUI(&dlg); + + // set current normal font for dialog children, + // because of just dlg.SetFont(normal_font()) has no result; + for (auto child : dlg.GetChildren()) + child->SetFont(normal_font()); + + if (dlg.ShowModal() == wxID_OK) + if (const Config::Snapshot *snapshot = Config::take_config_snapshot_report_error( + *app_config, Config::Snapshot::SNAPSHOT_USER, dlg.GetValue().ToUTF8().data()); + snapshot != nullptr) + app_config->set("on_snapshot", snapshot->id); + } + break; + case ConfigMenuSnapshots: + if (check_and_save_current_preset_changes(_L("Loading a configuration snapshot"), "", false)) { + std::string on_snapshot; + if (Config::SnapshotDB::singleton().is_on_snapshot(*app_config)) + on_snapshot = app_config->get("on_snapshot"); + ConfigSnapshotDialog dlg(Slic3r::GUI::Config::SnapshotDB::singleton(), on_snapshot); + dlg.ShowModal(); + if (!dlg.snapshot_to_activate().empty()) { + if (! Config::SnapshotDB::singleton().is_on_snapshot(*app_config) && + ! Config::take_config_snapshot_cancel_on_error(*app_config, Config::Snapshot::SNAPSHOT_BEFORE_ROLLBACK, "", + GUI::format(_L("Continue to activate a configuration snapshot %1%?"), dlg.snapshot_to_activate()))) + break; + try { + app_config->set("on_snapshot", Config::SnapshotDB::singleton().restore_snapshot(dlg.snapshot_to_activate(), *app_config).id); + // Enable substitutions, log both user and system substitutions. There should not be any substitutions performed when loading system + // presets because compatibility of profiles shall be verified using the min_slic3r_version keys in config index, but users + // are known to be creative and mess with the config files in various ways. + if (PresetsConfigSubstitutions all_substitutions = preset_bundle->load_presets(*app_config, ForwardCompatibilitySubstitutionRule::Enable); + ! all_substitutions.empty()) + show_substitutions_info(all_substitutions); + + // Load the currently selected preset into the GUI, update the preset selection box. + load_current_presets(); + } catch (std::exception &ex) { + GUI::show_error(nullptr, _L("Failed to activate configuration snapshot.") + "\n" + into_u8(ex.what())); + } + } + } + break; + case ConfigMenuPreferences: + { + open_preferences(); + break; + } + case ConfigMenuLanguage: + { + /* Before change application language, let's check unsaved changes on 3D-Scene + * and draw user's attention to the application restarting after a language change + */ + { + // the dialog needs to be destroyed before the call to switch_language() + // or sometimes the application crashes into wxDialogBase() destructor + // so we put it into an inner scope + wxString title = is_editor() ? wxString(SLIC3R_APP_NAME) : wxString(GCODEVIEWER_APP_NAME); + title += " - " + _L("Language selection"); + //wxMessageDialog dialog(nullptr, + MessageDialog dialog(nullptr, + _L("Switching the language will trigger application restart.\n" + "You will lose content of the plater.") + "\n\n" + + _L("Do you want to proceed?"), + title, + wxICON_QUESTION | wxOK | wxCANCEL); + if (dialog.ShowModal() == wxID_CANCEL) + return; + } + + switch_language(); + break; + } + case ConfigMenuFlashFirmware: + FirmwareDialog::run(mainframe); + break; + default: + break; + } + }); + + using std::placeholders::_1; + + if (mode_menu != nullptr) { + auto modfn = [this](int mode, wxCommandEvent&) { if (get_mode() != mode) save_mode(mode); }; + mode_menu->Bind(wxEVT_MENU, std::bind(modfn, comSimple, _1), config_id_base + ConfigMenuModeSimple); + mode_menu->Bind(wxEVT_MENU, std::bind(modfn, comAdvanced, _1), config_id_base + ConfigMenuModeAdvanced); + mode_menu->Bind(wxEVT_MENU, std::bind(modfn, comExpert, _1), config_id_base + ConfigMenuModeExpert); + } + + menu->Append(local_menu, _L("&Configuration")); +} + +void GUI_App::open_preferences(const std::string& highlight_option /*= std::string()*/, const std::string& tab_name/*= std::string()*/) +{ + mainframe->preferences_dialog->show(highlight_option, tab_name); + + if (mainframe->preferences_dialog->recreate_GUI()) + recreate_GUI(_L("Restart application") + dots); + +#if ENABLE_GCODE_LINES_ID_IN_H_SLIDER + if (dlg.seq_top_layer_only_changed() || dlg.seq_seq_top_gcode_indices_changed()) +#else + if (mainframe->preferences_dialog->seq_top_layer_only_changed()) +#endif // ENABLE_GCODE_LINES_ID_IN_H_SLIDER + this->plater_->refresh_print(); + +#ifdef _WIN32 + if (is_editor()) { + if (app_config->get("associate_3mf") == "1") + associate_3mf_files(); + if (app_config->get("associate_stl") == "1") + associate_stl_files(); + } + else { + if (app_config->get("associate_gcode") == "1") + associate_gcode_files(); + } +#endif // _WIN32 + + if (mainframe->preferences_dialog->settings_layout_changed()) { + // hide full main_sizer for mainFrame + mainframe->GetSizer()->Show(false); + mainframe->update_layout(); + mainframe->select_tab(size_t(0)); + } +} + +bool GUI_App::has_unsaved_preset_changes() const +{ + PrinterTechnology printer_technology = preset_bundle->printers.get_edited_preset().printer_technology(); + for (const Tab* const tab : tabs_list) { + if (tab->supports_printer_technology(printer_technology) && tab->saved_preset_is_dirty()) + return true; + } + return false; +} + +bool GUI_App::has_current_preset_changes() const +{ + PrinterTechnology printer_technology = preset_bundle->printers.get_edited_preset().printer_technology(); + for (const Tab* const tab : tabs_list) { + if (tab->supports_printer_technology(printer_technology) && tab->current_preset_is_dirty()) + return true; + } + return false; +} + +void GUI_App::update_saved_preset_from_current_preset() +{ + PrinterTechnology printer_technology = preset_bundle->printers.get_edited_preset().printer_technology(); + for (Tab* tab : tabs_list) { + if (tab->supports_printer_technology(printer_technology)) + tab->update_saved_preset_from_current_preset(); + } +} + +std::vector GUI_App::get_active_preset_collections() const +{ + std::vector ret; + PrinterTechnology printer_technology = preset_bundle->printers.get_edited_preset().printer_technology(); + for (const Tab* tab : tabs_list) + if (tab->supports_printer_technology(printer_technology)) + ret.push_back(tab->get_presets()); + return ret; +} + +// To notify the user whether he is aware that some preset changes will be lost, +// UnsavedChangesDialog: "Discard / Save / Cancel" +// This is called when: +// - Close Application & Current project isn't saved +// - Load Project & Current project isn't saved +// - Undo / Redo with change of print technologie +// - Loading snapshot +// - Loading config_file/bundle +// UnsavedChangesDialog: "Don't save / Save / Cancel" +// This is called when: +// - Exporting config_bundle +// - Taking snapshot +bool GUI_App::check_and_save_current_preset_changes(const wxString& caption, const wxString& header, bool remember_choice/* = true*/, bool dont_save_insted_of_discard/* = false*/) +{ + if (has_current_preset_changes()) { + const std::string app_config_key = remember_choice ? "default_action_on_close_application" : ""; + int act_buttons = UnsavedChangesDialog::ActionButtons::SAVE; + if (dont_save_insted_of_discard) + act_buttons |= UnsavedChangesDialog::ActionButtons::DONT_SAVE; + UnsavedChangesDialog dlg(caption, header, app_config_key, act_buttons); + std::string act = app_config_key.empty() ? "none" : wxGetApp().app_config->get(app_config_key); + if (act == "none" && dlg.ShowModal() == wxID_CANCEL) + return false; + + if (dlg.save_preset()) // save selected changes + { + for (const std::pair& nt : dlg.get_names_and_types()) + preset_bundle->save_changes_for_preset(nt.first, nt.second, dlg.get_unselected_options(nt.second)); + + load_current_presets(false); + + // if we saved changes to the new presets, we should to + // synchronize config.ini with the current selections. + preset_bundle->export_selections(*app_config); + + MessageDialog(nullptr, _L_PLURAL("The preset modifications are successfully saved", + "The presets modifications are successfully saved", dlg.get_names_and_types().size())).ShowModal(); + } + } + + return true; +} + +void GUI_App::apply_keeped_preset_modifications() +{ + PrinterTechnology printer_technology = preset_bundle->printers.get_edited_preset().printer_technology(); + for (Tab* tab : tabs_list) { + if (tab->supports_printer_technology(printer_technology)) + tab->apply_config_from_cache(); + } + load_current_presets(false); +} + +// This is called when creating new project or load another project +// OR close ConfigWizard +// to ask the user what should we do with unsaved changes for presets. +// New Project => Current project is saved => UnsavedChangesDialog: "Keep / Discard / Cancel" +// => Current project isn't saved => UnsavedChangesDialog: "Keep / Discard / Save / Cancel" +// Close ConfigWizard => Current project is saved => UnsavedChangesDialog: "Keep / Discard / Save / Cancel" +// Note: no_nullptr postponed_apply_of_keeped_changes indicates that thie function is called after ConfigWizard is closed +bool GUI_App::check_and_keep_current_preset_changes(const wxString& caption, const wxString& header, int action_buttons, bool* postponed_apply_of_keeped_changes/* = nullptr*/) +{ + if (has_current_preset_changes()) { + bool is_called_from_configwizard = postponed_apply_of_keeped_changes != nullptr; + + const std::string app_config_key = is_called_from_configwizard ? "" : "default_action_on_new_project"; + UnsavedChangesDialog dlg(caption, header, app_config_key, action_buttons); + std::string act = app_config_key.empty() ? "none" : wxGetApp().app_config->get(app_config_key); + if (act == "none" && dlg.ShowModal() == wxID_CANCEL) + return false; + + auto reset_modifications = [this, is_called_from_configwizard]() { + if (is_called_from_configwizard) + return; // no need to discared changes. It will be done fromConfigWizard closing + + PrinterTechnology printer_technology = preset_bundle->printers.get_edited_preset().printer_technology(); + for (const Tab* const tab : tabs_list) { + if (tab->supports_printer_technology(printer_technology) && tab->current_preset_is_dirty()) + tab->m_presets->discard_current_changes(); + } + load_current_presets(false); + }; + + if (dlg.discard()) + reset_modifications(); + else // save selected changes + { + const auto& preset_names_and_types = dlg.get_names_and_types(); + if (dlg.save_preset()) { + for (const std::pair& nt : preset_names_and_types) + preset_bundle->save_changes_for_preset(nt.first, nt.second, dlg.get_unselected_options(nt.second)); + + // if we saved changes to the new presets, we should to + // synchronize config.ini with the current selections. + preset_bundle->export_selections(*app_config); + + wxString text = _L_PLURAL("The preset modifications are successfully saved", + "The presets modifications are successfully saved", preset_names_and_types.size()); + if (!is_called_from_configwizard) + text += "\n\n" + _L("For new project all modifications will be reseted"); + + MessageDialog(nullptr, text).ShowModal(); + reset_modifications(); + } + else if (dlg.transfer_changes() && (dlg.has_unselected_options() || is_called_from_configwizard)) { + // execute this part of code only if not all modifications are keeping to the new project + // OR this function is called when ConfigWizard is closed and "Keep modifications" is selected + for (const std::pair& nt : preset_names_and_types) { + Preset::Type type = nt.second; + Tab* tab = get_tab(type); + std::vector selected_options = dlg.get_selected_options(type); + if (type == Preset::TYPE_PRINTER) { + auto it = std::find(selected_options.begin(), selected_options.end(), "extruders_count"); + if (it != selected_options.end()) { + // erase "extruders_count" option from the list + selected_options.erase(it); + // cache the extruders count + static_cast(tab)->cache_extruder_cnt(); + } + } + tab->cache_config_diff(selected_options); + if (!is_called_from_configwizard) + tab->m_presets->discard_current_changes(); + } + if (is_called_from_configwizard) + *postponed_apply_of_keeped_changes = true; + else + apply_keeped_preset_modifications(); + } + } + } + + return true; +} + +bool GUI_App::can_load_project() +{ + int saved_project = plater()->save_project_if_dirty(_L("Loading a new project while the current project is modified.")); + if (saved_project == wxID_CANCEL || + (plater()->is_project_dirty() && saved_project == wxID_NO && + !check_and_save_current_preset_changes(_L("Project is loading"), _L("Opening new project while some presets are unsaved.")))) + return false; + return true; +} + +bool GUI_App::check_print_host_queue() +{ + wxString dirty; + std::vector> jobs; + // Get ongoing jobs from dialog + mainframe->m_printhost_queue_dlg->get_active_jobs(jobs); + if (jobs.empty()) + return true; + // Show dialog + wxString job_string = wxString(); + for (const auto& job : jobs) { + job_string += format_wxstr(" %1% : %2% \n", job.first, job.second); + } + wxString message; + message += _(L("The uploads are still ongoing")) + ":\n\n" + job_string +"\n" + _(L("Stop them and continue anyway?")); + //wxMessageDialog dialog(mainframe, + MessageDialog dialog(mainframe, + message, + wxString(SLIC3R_APP_NAME) + " - " + _(L("Ongoing uploads")), + wxICON_QUESTION | wxYES_NO | wxNO_DEFAULT); + if (dialog.ShowModal() == wxID_YES) + return true; + + // TODO: If already shown, bring forward + mainframe->m_printhost_queue_dlg->Show(); + return false; +} + +bool GUI_App::checked_tab(Tab* tab) +{ + bool ret = true; + if (find(tabs_list.begin(), tabs_list.end(), tab) == tabs_list.end()) + ret = false; + return ret; +} + +// Update UI / Tabs to reflect changes in the currently loaded presets +void GUI_App::load_current_presets(bool check_printer_presets_ /*= true*/) +{ + // check printer_presets for the containing information about "Print Host upload" + // and create physical printer from it, if any exists + if (check_printer_presets_) + check_printer_presets(); + + PrinterTechnology printer_technology = preset_bundle->printers.get_edited_preset().printer_technology(); + this->plater()->set_printer_technology(printer_technology); + for (Tab *tab : tabs_list) + if (tab->supports_printer_technology(printer_technology)) { + if (tab->type() == Preset::TYPE_PRINTER) { + static_cast(tab)->update_pages(); + // Mark the plater to update print bed by tab->load_current_preset() from Plater::on_config_change(). + this->plater()->force_print_bed_update(); + } + tab->load_current_preset(); + } +} + +bool GUI_App::OnExceptionInMainLoop() +{ + generic_exception_handle(); + return false; +} + +#ifdef __APPLE__ +// This callback is called from wxEntry()->wxApp::CallOnInit()->NSApplication run +// that is, before GUI_App::OnInit(), so we have a chance to switch GUI_App +// to a G-code viewer. +void GUI_App::OSXStoreOpenFiles(const wxArrayString &fileNames) +{ + size_t num_gcodes = 0; + for (const wxString &filename : fileNames) + if (is_gcode_file(into_u8(filename))) + ++ num_gcodes; + if (fileNames.size() == num_gcodes) { + // Opening PrusaSlicer by drag & dropping a G-Code onto PrusaSlicer icon in Finder, + // just G-codes were passed. Switch to G-code viewer mode. + m_app_mode = EAppMode::GCodeViewer; + unlock_lockfile(get_instance_hash_string() + ".lock", data_dir() + "/cache/"); + if(app_config != nullptr) + delete app_config; + app_config = nullptr; + init_app_config(); + } + wxApp::OSXStoreOpenFiles(fileNames); +} +// wxWidgets override to get an event on open files. +void GUI_App::MacOpenFiles(const wxArrayString &fileNames) +{ + std::vector files; + std::vector gcode_files; + std::vector non_gcode_files; + for (const auto& filename : fileNames) { + if (is_gcode_file(into_u8(filename))) + gcode_files.emplace_back(filename); + else { + files.emplace_back(into_u8(filename)); + non_gcode_files.emplace_back(filename); + } + } + if (m_app_mode == EAppMode::GCodeViewer) { + // Running in G-code viewer. + // Load the first G-code into the G-code viewer. + // Or if no G-codes, send other files to slicer. + if (! gcode_files.empty()) { + if (m_post_initialized) + this->plater()->load_gcode(gcode_files.front()); + else + this->init_params->input_files = { into_u8(gcode_files.front()) }; + } + if (!non_gcode_files.empty()) + start_new_slicer(non_gcode_files, true); + } else { + if (! files.empty()) { + if (m_post_initialized) { + wxArrayString input_files; + for (size_t i = 0; i < non_gcode_files.size(); ++i) + input_files.push_back(non_gcode_files[i]); + this->plater()->load_files(input_files); + } else { + for (const auto &f : non_gcode_files) + this->init_params->input_files.emplace_back(into_u8(f)); + } + } + for (const wxString &filename : gcode_files) + start_new_gcodeviewer(&filename); + } +} +#endif /* __APPLE */ + +Sidebar& GUI_App::sidebar() +{ + return plater_->sidebar(); +} + +ObjectManipulation* GUI_App::obj_manipul() +{ + // If this method is called before plater_ has been initialized, return nullptr (to avoid a crash) + return (plater_ != nullptr) ? sidebar().obj_manipul() : nullptr; +} + +ObjectSettings* GUI_App::obj_settings() +{ + return sidebar().obj_settings(); +} + +ObjectList* GUI_App::obj_list() +{ + return sidebar().obj_list(); +} + +ObjectLayers* GUI_App::obj_layers() +{ + return sidebar().obj_layers(); +} + +Plater* GUI_App::plater() +{ + return plater_; +} + +const Plater* GUI_App::plater() const +{ + return plater_; +} + +Model& GUI_App::model() +{ + return plater_->model(); +} +wxBookCtrlBase* GUI_App::tab_panel() const +{ + return mainframe->m_tabpanel; +} + +NotificationManager * GUI_App::notification_manager() +{ + return plater_->get_notification_manager(); +} + +// extruders count from selected printer preset +int GUI_App::extruders_cnt() const +{ + const Preset& preset = preset_bundle->printers.get_selected_preset(); + return preset.printer_technology() == ptSLA ? 1 : + preset.config.option("nozzle_diameter")->values.size(); +} + +// extruders count from edited printer preset +int GUI_App::extruders_edited_cnt() const +{ + const Preset& preset = preset_bundle->printers.get_edited_preset(); + return preset.printer_technology() == ptSLA ? 1 : + preset.config.option("nozzle_diameter")->values.size(); +} + +wxString GUI_App::current_language_code_safe() const +{ + // Translate the language code to a code, for which Prusa Research maintains translations. + const std::map mapping { + { "cs", "cs_CZ", }, + { "sk", "cs_CZ", }, + { "de", "de_DE", }, + { "es", "es_ES", }, + { "fr", "fr_FR", }, + { "it", "it_IT", }, + { "ja", "ja_JP", }, + { "ko", "ko_KR", }, + { "pl", "pl_PL", }, + { "uk", "uk_UA", }, + { "zh", "zh_CN", }, + { "ru", "ru_RU", }, + }; + wxString language_code = this->current_language_code().BeforeFirst('_'); + auto it = mapping.find(language_code); + if (it != mapping.end()) + language_code = it->second; + else + language_code = "en_US"; + return language_code; +} + +void GUI_App::open_web_page_localized(const std::string &http_address) +{ + open_browser_with_warning_dialog(http_address + "&lng=" + this->current_language_code_safe(), nullptr, false); +} + +// If we are switching from the FFF-preset to the SLA, we should to control the printed objects if they have a part(s). +// Because of we can't to print the multi-part objects with SLA technology. +bool GUI_App::may_switch_to_SLA_preset(const wxString& caption) +{ + if (model_has_multi_part_objects(model())) { + show_info(nullptr, + _L("It's impossible to print multi-part object(s) with SLA technology.") + "\n\n" + + _L("Please check your object list before preset changing."), + caption); + return false; + } + return true; +} + +bool GUI_App::run_wizard(ConfigWizard::RunReason reason, ConfigWizard::StartPage start_page) +{ + wxCHECK_MSG(mainframe != nullptr, false, "Internal error: Main frame not created / null"); + + if (reason == ConfigWizard::RR_USER) { + if (preset_updater->config_update(app_config->orig_version(), PresetUpdater::UpdateParams::FORCED_BEFORE_WIZARD) == PresetUpdater::R_ALL_CANCELED) + return false; + } + + auto wizard = new ConfigWizard(mainframe); + const bool res = wizard->run(reason, start_page); + + if (res) { + load_current_presets(); + + // #ysFIXME - delete after testing: This part of code looks redundant. All checks are inside ConfigWizard::priv::apply_config() + if (preset_bundle->printers.get_edited_preset().printer_technology() == ptSLA) + may_switch_to_SLA_preset(_L("Configuration is editing from ConfigWizard")); + } + + return res; +} + +void GUI_App::show_desktop_integration_dialog() +{ +#ifdef __linux__ + //wxCHECK_MSG(mainframe != nullptr, false, "Internal error: Main frame not created / null"); + DesktopIntegrationDialog dialog(mainframe); + dialog.ShowModal(); +#endif //__linux__ +} + +#if ENABLE_THUMBNAIL_GENERATOR_DEBUG +void GUI_App::gcode_thumbnails_debug() +{ + const std::string BEGIN_MASK = "; thumbnail begin"; + const std::string END_MASK = "; thumbnail end"; + std::string gcode_line; + bool reading_image = false; + unsigned int width = 0; + unsigned int height = 0; + + wxFileDialog dialog(GetTopWindow(), _L("Select a gcode file:"), "", "", "G-code files (*.gcode)|*.gcode;*.GCODE;", wxFD_OPEN | wxFD_FILE_MUST_EXIST); + if (dialog.ShowModal() != wxID_OK) + return; + + std::string in_filename = into_u8(dialog.GetPath()); + std::string out_path = boost::filesystem::path(in_filename).remove_filename().append(L"thumbnail").string(); + + boost::nowide::ifstream in_file(in_filename.c_str()); + std::vector rows; + std::string row; + if (in_file.good()) + { + while (std::getline(in_file, gcode_line)) + { + if (in_file.good()) + { + if (boost::starts_with(gcode_line, BEGIN_MASK)) + { + reading_image = true; + gcode_line = gcode_line.substr(BEGIN_MASK.length() + 1); + std::string::size_type x_pos = gcode_line.find('x'); + std::string width_str = gcode_line.substr(0, x_pos); + width = (unsigned int)::atoi(width_str.c_str()); + std::string height_str = gcode_line.substr(x_pos + 1); + height = (unsigned int)::atoi(height_str.c_str()); + row.clear(); + } + else if (reading_image && boost::starts_with(gcode_line, END_MASK)) + { + std::string out_filename = out_path + std::to_string(width) + "x" + std::to_string(height) + ".png"; + boost::nowide::ofstream out_file(out_filename.c_str(), std::ios::binary); + if (out_file.good()) + { + std::string decoded; + decoded.resize(boost::beast::detail::base64::decoded_size(row.size())); + decoded.resize(boost::beast::detail::base64::decode((void*)&decoded[0], row.data(), row.size()).first); + + out_file.write(decoded.c_str(), decoded.size()); + out_file.close(); + } + + reading_image = false; + width = 0; + height = 0; + rows.clear(); + } + else if (reading_image) + row += gcode_line.substr(2); + } + } + + in_file.close(); + } +} +#endif // ENABLE_THUMBNAIL_GENERATOR_DEBUG + +void GUI_App::window_pos_save(wxTopLevelWindow* window, const std::string &name) +{ + if (name.empty()) { return; } + const auto config_key = (boost::format("window_%1%") % name).str(); + + WindowMetrics metrics = WindowMetrics::from_window(window); + app_config->set(config_key, metrics.serialize()); + app_config->save(); +} + +void GUI_App::window_pos_restore(wxTopLevelWindow* window, const std::string &name, bool default_maximized) +{ + if (name.empty()) { return; } + const auto config_key = (boost::format("window_%1%") % name).str(); + + if (! app_config->has(config_key)) { + window->Maximize(default_maximized); + return; + } + + auto metrics = WindowMetrics::deserialize(app_config->get(config_key)); + if (! metrics) { + window->Maximize(default_maximized); + return; + } + + const wxRect& rect = metrics->get_rect(); + + if (app_config->get("restore_win_position") == "1") { + // workaround for crash related to the positioning of the window on secondary monitor + app_config->set("restore_win_position", (boost::format("crashed_at_%1%_pos") % name).str()); + app_config->save(); + window->SetPosition(rect.GetPosition()); + + // workaround for crash related to the positioning of the window on secondary monitor + app_config->set("restore_win_position", (boost::format("crashed_at_%1%_size") % name).str()); + app_config->save(); + window->SetSize(rect.GetSize()); + + // revert "restore_win_position" value if application wasn't crashed + app_config->set("restore_win_position", "1"); + app_config->save(); + } + else + window->CenterOnScreen(); + + window->Maximize(metrics->get_maximized()); +} + +void GUI_App::window_pos_sanitize(wxTopLevelWindow* window) +{ + /*unsigned*/int display_idx = wxDisplay::GetFromWindow(window); + wxRect display; + if (display_idx == wxNOT_FOUND) { + display = wxDisplay(0u).GetClientArea(); + window->Move(display.GetTopLeft()); + } else { + display = wxDisplay(display_idx).GetClientArea(); + } + + auto metrics = WindowMetrics::from_window(window); + metrics.sanitize_for_display(display); + if (window->GetScreenRect() != metrics.get_rect()) { + window->SetSize(metrics.get_rect()); + } +} + +bool GUI_App::config_wizard_startup() +{ + if (!m_app_conf_exists || preset_bundle->printers.only_default_printers()) { + run_wizard(ConfigWizard::RR_DATA_EMPTY); + return true; + } else if (get_app_config()->legacy_datadir()) { + // Looks like user has legacy pre-vendorbundle data directory, + // explain what this is and run the wizard + + MsgDataLegacy dlg; + dlg.ShowModal(); + + run_wizard(ConfigWizard::RR_DATA_LEGACY); + return true; + } + return false; +} + +bool GUI_App::check_updates(const bool verbose) +{ + PresetUpdater::UpdateResult updater_result; + try { + updater_result = preset_updater->config_update(app_config->orig_version(), verbose ? PresetUpdater::UpdateParams::SHOW_TEXT_BOX : PresetUpdater::UpdateParams::SHOW_NOTIFICATION); + if (updater_result == PresetUpdater::R_INCOMPAT_EXIT) { + mainframe->Close(); + // Applicaiton is closing. + return false; + } + else if (updater_result == PresetUpdater::R_INCOMPAT_CONFIGURED) { + m_app_conf_exists = true; + } + else if (verbose && updater_result == PresetUpdater::R_NOOP) { + MsgNoUpdates dlg; + dlg.ShowModal(); + } + } + catch (const std::exception & ex) { + show_error(nullptr, ex.what()); + } + // Applicaiton will continue. + return true; +} + +bool GUI_App::open_browser_with_warning_dialog(const wxString& url, wxWindow* parent/* = nullptr*/, bool force_remember_choice /*= true*/, int flags/* = 0*/) +{ + bool launch = true; + + // warning dialog containes a "Remember my choice" checkbox + std::string option_key = "suppress_hyperlinks"; + if (force_remember_choice || app_config->get(option_key).empty()) { + if (app_config->get(option_key).empty()) { + RichMessageDialog dialog(parent, _L("Open hyperlink in default browser?"), _L("PrusaSlicer: Open hyperlink"), wxICON_QUESTION | wxYES_NO); + dialog.ShowCheckBox(_L("Remember my choice")); + auto answer = dialog.ShowModal(); + launch = answer == wxID_YES; + if (dialog.IsCheckBoxChecked()) { + wxString preferences_item = _L("Suppress to open hyperlink in browser"); + wxString msg = + _L("PrusaSlicer will remember your choice.") + "\n\n" + + _L("You will not be asked about it again on hyperlinks hovering.") + "\n\n" + + format_wxstr(_L("Visit \"Preferences\" and check \"%1%\"\nto changes your choice."), preferences_item); + + MessageDialog msg_dlg(parent, msg, _L("PrusaSlicer: Don't ask me again"), wxOK | wxCANCEL | wxICON_INFORMATION); + if (msg_dlg.ShowModal() == wxID_CANCEL) + return false; + app_config->set(option_key, answer == wxID_NO ? "1" : "0"); + } + } + if (launch) + launch = app_config->get(option_key) != "1"; + } + // warning dialog doesn't containe a "Remember my choice" checkbox + // and will be shown only when "Suppress to open hyperlink in browser" is ON. + else if (app_config->get(option_key) == "1") { + MessageDialog dialog(parent, _L("Open hyperlink in default browser?"), _L("PrusaSlicer: Open hyperlink"), wxICON_QUESTION | wxYES_NO); + launch = dialog.ShowModal() == wxID_YES; + } + + return launch && wxLaunchDefaultBrowser(url, flags); +} + +// static method accepting a wxWindow object as first parameter +// void warning_catcher{ +// my($self, $message_dialog) = @_; +// return sub{ +// my $message = shift; +// return if $message = ~/ GLUquadricObjPtr | Attempt to free unreferenced scalar / ; +// my @params = ($message, 'Warning', wxOK | wxICON_WARNING); +// $message_dialog +// ? $message_dialog->(@params) +// : Wx::MessageDialog->new($self, @params)->ShowModal; +// }; +// } + +// Do we need this function??? +// void GUI_App::notify(message) { +// auto frame = GetTopWindow(); +// // try harder to attract user attention on OS X +// if (!frame->IsActive()) +// frame->RequestUserAttention(defined(__WXOSX__/*&Wx::wxMAC */)? wxUSER_ATTENTION_ERROR : wxUSER_ATTENTION_INFO); +// +// // There used to be notifier using a Growl application for OSX, but Growl is dead. +// // The notifier also supported the Linux X D - bus notifications, but that support was broken. +// //TODO use wxNotificationMessage ? +// } + + +#ifdef __WXMSW__ +void GUI_App::associate_3mf_files() +{ + associate_file_type(L".3mf", L"Prusa.Slicer.1", L"PrusaSlicer", true); +} + +void GUI_App::associate_stl_files() +{ + associate_file_type(L".stl", L"Prusa.Slicer.1", L"PrusaSlicer", true); +} + +void GUI_App::associate_gcode_files() +{ + associate_file_type(L".gcode", L"PrusaSlicer.GCodeViewer.1", L"PrusaSlicerGCodeViewer", true); +} +#endif // __WXMSW__ + +void GUI_App::on_version_read(wxCommandEvent& evt) +{ + app_config->set("version_online", into_u8(evt.GetString())); + app_config->save(); + std::string opt = app_config->get("notify_release"); + if (this->plater_ == nullptr || (opt != "all" && opt != "release")) { + return; + } + if (*Semver::parse(SLIC3R_VERSION) >= *Semver::parse(into_u8(evt.GetString()))) { + return; + } + // notification + /* + this->plater_->get_notification_manager()->push_notification(NotificationType::NewAppAvailable + , NotificationManager::NotificationLevel::ImportantNotificationLevel + , Slic3r::format(_u8L("New release version %1% is available."), evt.GetString()) + , _u8L("See Download page.") + , [](wxEvtHandler* evnthndlr) {wxGetApp().open_web_page_localized("https://www.prusa3d.com/slicerweb"); return true; } + ); + */ + // updater + // read triggered_by_user that was set when calling GUI_App::app_version_check + app_updater(m_app_updater->get_triggered_by_user()); +} + +void GUI_App::app_updater(bool from_user) +{ + DownloadAppData app_data = m_app_updater->get_app_data(); + + if (from_user && (!app_data.version || *app_data.version <= *Semver::parse(SLIC3R_VERSION))) + { + BOOST_LOG_TRIVIAL(info) << "There is no newer version online."; + MsgNoAppUpdates no_update_dialog; + no_update_dialog.ShowModal(); + return; + + } + + assert(!app_data.url.empty()); + assert(!app_data.target_path.empty()); + + // dialog with new version info + AppUpdateAvailableDialog dialog(*Semver::parse(SLIC3R_VERSION), *app_data.version); + auto dialog_result = dialog.ShowModal(); + // checkbox "do not show again" + if (dialog.disable_version_check()) { + app_config->set("notify_release", "none"); + } + // Doesn't wish to update + if (dialog_result != wxID_OK) { + return; + } + // dialog with new version download (installer or app dependent on system) including path selection + AppUpdateDownloadDialog dwnld_dlg(*app_data.version, app_data.target_path); + dialog_result = dwnld_dlg.ShowModal(); + // Doesn't wish to download + if (dialog_result != wxID_OK) { + return; + } + app_data.target_path =dwnld_dlg.get_download_path(); + + // start download + this->plater_->get_notification_manager()->push_download_progress_notification(_utf8("Download"), std::bind(&AppUpdater::cancel_callback, this->m_app_updater.get())); + app_data.start_after = dwnld_dlg.run_after_download(); + m_app_updater->set_app_data(std::move(app_data)); + m_app_updater->sync_download(); +} + +void GUI_App::app_version_check(bool from_user) +{ + if (from_user) { + if (m_app_updater->get_download_ongoing()) { + MessageDialog msgdlg(nullptr, _L("Download of new version is already ongoing. Do you wish to continue?"), _L("Notice"), wxYES_NO); + if (msgdlg.ShowModal() != wxID_YES) + return; + } + } + std::string version_check_url = app_config->version_check_url(); + m_app_updater->sync_version(version_check_url, from_user); +} + +} // GUI +} //Slic3r diff --git a/src/slic3r/GUI/GUI_Factories.cpp b/src/slic3r/GUI/GUI_Factories.cpp index d5e7260af..6f240b27d 100644 --- a/src/slic3r/GUI/GUI_Factories.cpp +++ b/src/slic3r/GUI/GUI_Factories.cpp @@ -142,14 +142,13 @@ std::map SettingsFactory::CATEGORY_ICON = { L("Hollowing") , "hollowing" } }; -wxBitmap SettingsFactory::get_category_bitmap(const std::string& category_name, bool menu_bmp /*= true*/) +wxBitmapBundle* SettingsFactory::get_category_bitmap(const std::string& category_name) { if (CATEGORY_ICON.find(category_name) == CATEGORY_ICON.end()) - return wxNullBitmap; - return menu_bmp ? create_menu_bitmap(CATEGORY_ICON.at(category_name)) : create_scaled_bitmap(CATEGORY_ICON.at(category_name)); + return get_bmp_bundle("empty"); + return get_bmp_bundle(CATEGORY_ICON.at(category_name)); } - //------------------------------------- // MenuFactory //------------------------------------- @@ -430,12 +429,12 @@ static void create_freq_settings_popupmenu(wxMenu* menu, const bool is_object_se #endif } -std::vector MenuFactory::get_volume_bitmaps() +std::vector MenuFactory::get_volume_bitmaps() { - std::vector volume_bmps; + std::vector volume_bmps; volume_bmps.reserve(ADD_VOLUME_MENU_ITEMS.size()); for (auto item : ADD_VOLUME_MENU_ITEMS) - volume_bmps.push_back(create_menu_bitmap(item.second)); + volume_bmps.push_back(get_bmp_bundle(item.second)); return volume_bmps; } @@ -620,7 +619,7 @@ wxMenuItem* MenuFactory::append_menu_item_settings(wxMenu* menu_) // Add full settings list auto menu_item = new wxMenuItem(menu, wxID_ANY, menu_name); - menu_item->SetBitmap(create_menu_bitmap("cog")); + menu_item->SetBitmap(*get_bmp_bundle("cog")); menu_item->SetSubMenu(create_settings_popupmenu(menu, is_object_settings, item)); return menu->Append(menu_item); @@ -765,7 +764,7 @@ void MenuFactory::append_menu_item_change_extruder(wxMenu* menu) return; } - std::vector icons = get_extruder_color_icons(true); + std::vector icons = get_extruder_color_icons(true); wxMenu* extruder_selection_menu = new wxMenu(); const wxString& name = sels.Count() == 1 ? names[0] : names[1]; @@ -784,7 +783,7 @@ void MenuFactory::append_menu_item_change_extruder(wxMenu* menu) (is_active_extruder ? " (" + _L("active") + ")" : ""); append_menu_item(extruder_selection_menu, wxID_ANY, item_name, "", - [i](wxCommandEvent&) { obj_list()->set_extruder_for_selected_items(i); }, *icons[icon_idx], menu, + [i](wxCommandEvent&) { obj_list()->set_extruder_for_selected_items(i); }, icons[icon_idx], menu, [is_active_extruder]() { return !is_active_extruder; }, m_parent); } @@ -1144,12 +1143,6 @@ void MenuFactory::update_default_menu() create_default_menu(); } -void MenuFactory::msw_rescale() -{ - for (MenuWithSeparators* menu : { &m_object_menu, &m_sla_object_menu, &m_part_menu, &m_default_menu }) - msw_rescale_menu(dynamic_cast(menu)); -} - #ifdef _WIN32 // For this class is used code from stackoverflow: // https://stackoverflow.com/questions/257288/is-it-possible-to-write-a-template-to-check-for-a-functions-existence @@ -1179,7 +1172,7 @@ static void update_menu_item_def_colors(T* item) void MenuFactory::sys_color_changed() { for (MenuWithSeparators* menu : { &m_object_menu, &m_sla_object_menu, &m_part_menu, &m_default_menu }) { - msw_rescale_menu(dynamic_cast(menu));// msw_rescale_menu updates just icons, so use it + sys_color_changed_menu(dynamic_cast(menu));// msw_rescale_menu updates just icons, so use it #ifdef _WIN32 // but under MSW we have to update item's bachground color for (wxMenuItem* item : menu->GetMenuItems()) @@ -1192,14 +1185,17 @@ void MenuFactory::sys_color_changed(wxMenuBar* menubar) { for (size_t id = 0; id < menubar->GetMenuCount(); id++) { wxMenu* menu = menubar->GetMenu(id); - msw_rescale_menu(menu); + sys_color_changed_menu(menu); +#ifndef __linux__ + menu->SetupBitmaps(); #ifdef _WIN32 // but under MSW we have to update item's bachground color for (wxMenuItem* item : menu->GetMenuItems()) update_menu_item_def_colors(item); +#endif #endif } - menubar->Refresh(); +// menubar->Refresh(); } diff --git a/src/slic3r/GUI/GUI_Factories.hpp b/src/slic3r/GUI/GUI_Factories.hpp index 0c478a97b..bbbc00d42 100644 --- a/src/slic3r/GUI/GUI_Factories.hpp +++ b/src/slic3r/GUI/GUI_Factories.hpp @@ -25,7 +25,7 @@ struct SettingsFactory typedef std::map> Bundle; static std::map CATEGORY_ICON; - static wxBitmap get_category_bitmap(const std::string& category_name, bool menu_bmp = true); + static wxBitmapBundle* get_category_bitmap(const std::string& category_name); static Bundle get_bundle(const DynamicPrintConfig* config, bool is_object_settings); static std::vector get_options(bool is_part); }; @@ -34,7 +34,7 @@ class MenuFactory { public: static const std::vector> ADD_VOLUME_MENU_ITEMS; - static std::vector get_volume_bitmaps(); + static std::vector get_volume_bitmaps(); MenuFactory(); ~MenuFactory() = default; @@ -43,7 +43,6 @@ public: void update(); void update_object_menu(); void update_default_menu(); - void msw_rescale(); void sys_color_changed(); static void sys_color_changed(wxMenuBar* menu_bar); diff --git a/src/slic3r/GUI/GUI_Geometry.cpp b/src/slic3r/GUI/GUI_Geometry.cpp new file mode 100644 index 000000000..b0ed0e04f --- /dev/null +++ b/src/slic3r/GUI/GUI_Geometry.cpp @@ -0,0 +1,9 @@ +#include "libslic3r/libslic3r.h" +#include "GUI_Geometry.hpp" + +namespace Slic3r { +namespace GUI { + + +} // namespace Slic3r +} // namespace GUI diff --git a/src/slic3r/GUI/GUI_Geometry.hpp b/src/slic3r/GUI/GUI_Geometry.hpp new file mode 100644 index 000000000..0d6cf7f4b --- /dev/null +++ b/src/slic3r/GUI/GUI_Geometry.hpp @@ -0,0 +1,81 @@ +#ifndef slic3r_GUI_Geometry_hpp_ +#define slic3r_GUI_Geometry_hpp_ + +namespace Slic3r { +namespace GUI { + +#if ENABLE_WORLD_COORDINATE +enum class ECoordinatesType : unsigned char +{ + World, + Instance, + Local +}; + +class TransformationType +{ +public: + enum Enum { + // Transforming in a world coordinate system + World = 0, + // Transforming in a instance coordinate system + Instance = 1, + // Transforming in a local coordinate system + Local = 2, + // Absolute transformations, allowed in local coordinate system only. + Absolute = 0, + // Relative transformations, allowed in both local and world coordinate system. + Relative = 4, + // For group selection, the transformation is performed as if the group made a single solid body. + Joint = 0, + // For group selection, the transformation is performed on each object independently. + Independent = 8, + + World_Relative_Joint = World | Relative | Joint, + World_Relative_Independent = World | Relative | Independent, + Instance_Absolute_Joint = Instance | Absolute | Joint, + Instance_Absolute_Independent = Instance | Absolute | Independent, + Instance_Relative_Joint = Instance | Relative | Joint, + Instance_Relative_Independent = Instance | Relative | Independent, + Local_Absolute_Joint = Local | Absolute | Joint, + Local_Absolute_Independent = Local | Absolute | Independent, + Local_Relative_Joint = Local | Relative | Joint, + Local_Relative_Independent = Local | Relative | Independent, + }; + + TransformationType() : m_value(World) {} + TransformationType(Enum value) : m_value(value) {} + TransformationType& operator=(Enum value) { m_value = value; return *this; } + + Enum operator()() const { return m_value; } + bool has(Enum v) const { return ((unsigned int)m_value & (unsigned int)v) != 0; } + + void set_world() { this->remove(Instance); this->remove(Local); } + void set_instance() { this->remove(Local); this->add(Instance); } + void set_local() { this->remove(Instance); this->add(Local); } + void set_absolute() { this->remove(Relative); } + void set_relative() { this->add(Relative); } + void set_joint() { this->remove(Independent); } + void set_independent() { this->add(Independent); } + + bool world() const { return !this->has(Instance) && !this->has(Local); } + bool instance() const { return this->has(Instance); } + bool local() const { return this->has(Local); } + bool absolute() const { return !this->has(Relative); } + bool relative() const { return this->has(Relative); } + bool joint() const { return !this->has(Independent); } + bool independent() const { return this->has(Independent); } + +private: + void add(Enum v) { m_value = Enum((unsigned int)m_value | (unsigned int)v); } + void remove(Enum v) { m_value = Enum((unsigned int)m_value & (~(unsigned int)v)); } + + Enum m_value; +}; + +#endif // ENABLE_WORLD_COORDINATE + +} // namespace Slic3r +} // namespace GUI + +#endif // slic3r_GUI_Geometry_hpp_ diff --git a/src/slic3r/GUI/GUI_ObjectLayers.cpp b/src/slic3r/GUI/GUI_ObjectLayers.cpp index b7ff8e48f..437a526af 100644 --- a/src/slic3r/GUI/GUI_ObjectLayers.cpp +++ b/src/slic3r/GUI/GUI_ObjectLayers.cpp @@ -234,47 +234,47 @@ void ObjectLayers::UpdateAndShow(const bool show) void ObjectLayers::msw_rescale() { - m_bmp_delete.msw_rescale(); - m_bmp_add.msw_rescale(); + //m_bmp_delete.msw_rescale(); + //m_bmp_add.msw_rescale(); - m_grid_sizer->SetHGap(wxGetApp().em_unit()); + //m_grid_sizer->SetHGap(wxGetApp().em_unit()); - // rescale edit-boxes - const int cells_cnt = m_grid_sizer->GetCols() * m_grid_sizer->GetEffectiveRowsCount(); - for (int i = 0; i < cells_cnt; ++i) { - const wxSizerItem* item = m_grid_sizer->GetItem(i); - if (item->IsWindow()) { - LayerRangeEditor* editor = dynamic_cast(item->GetWindow()); - if (editor != nullptr) - editor->msw_rescale(); - } - else if (item->IsSizer()) // case when we have editor with buttons - { - wxSizerItem* e_item = item->GetSizer()->GetItem(size_t(0)); // editor - if (e_item->IsWindow()) { - LayerRangeEditor* editor = dynamic_cast(e_item->GetWindow()); - if (editor != nullptr) - editor->msw_rescale(); - } + //// rescale edit-boxes + //const int cells_cnt = m_grid_sizer->GetCols() * m_grid_sizer->GetEffectiveRowsCount(); + //for (int i = 0; i < cells_cnt; ++i) { + // const wxSizerItem* item = m_grid_sizer->GetItem(i); + // if (item->IsWindow()) { + // LayerRangeEditor* editor = dynamic_cast(item->GetWindow()); + // if (editor != nullptr) + // editor->msw_rescale(); + // } + // else if (item->IsSizer()) // case when we have editor with buttons + // { + // wxSizerItem* e_item = item->GetSizer()->GetItem(size_t(0)); // editor + // if (e_item->IsWindow()) { + // LayerRangeEditor* editor = dynamic_cast(e_item->GetWindow()); + // if (editor != nullptr) + // editor->msw_rescale(); + // } - if (item->GetSizer()->GetItemCount() > 2) // if there are Add/Del buttons - for (size_t btn : {2, 3}) { // del_btn, add_btn - wxSizerItem* b_item = item->GetSizer()->GetItem(btn); - if (b_item->IsWindow()) { - auto button = dynamic_cast(b_item->GetWindow()); - if (button != nullptr) - button->msw_rescale(); - } - } - } - } + // if (item->GetSizer()->GetItemCount() > 2) // if there are Add/Del buttons + // for (size_t btn : {2, 3}) { // del_btn, add_btn + // wxSizerItem* b_item = item->GetSizer()->GetItem(btn); + // if (b_item->IsWindow()) { + // auto button = dynamic_cast(b_item->GetWindow()); + // if (button != nullptr) + // button->msw_rescale(); + // } + // } + // } + //} m_grid_sizer->Layout(); } void ObjectLayers::sys_color_changed() { - m_bmp_delete.msw_rescale(); - m_bmp_add.msw_rescale(); + m_bmp_delete.sys_color_changed(); + m_bmp_add.sys_color_changed(); // rescale edit-boxes const int cells_cnt = m_grid_sizer->GetCols() * m_grid_sizer->GetEffectiveRowsCount(); @@ -286,7 +286,7 @@ void ObjectLayers::sys_color_changed() if (b_item->IsWindow()) { auto button = dynamic_cast(b_item->GetWindow()); if (button != nullptr) - button->msw_rescale(); + button->sys_color_changed(); } } } diff --git a/src/slic3r/GUI/GUI_ObjectList.cpp b/src/slic3r/GUI/GUI_ObjectList.cpp index 7feb40dd5..2c1ee7a6c 100644 --- a/src/slic3r/GUI/GUI_ObjectList.cpp +++ b/src/slic3r/GUI/GUI_ObjectList.cpp @@ -973,12 +973,11 @@ void ObjectList::extruder_editing() if (!item || !(m_objects_model->GetItemType(item) & (itVolume | itObject))) return; - const int column_width = GetColumn(colExtruder)->GetWidth() + wxSystemSettings::GetMetric(wxSYS_VSCROLL_X) + 5; - - wxPoint pos = this->get_mouse_position_in_control(); - wxSize size = wxSize(column_width, -1); - pos.x = GetColumn(colName)->GetWidth() + GetColumn(colPrint)->GetWidth() + 5; - pos.y -= GetTextExtent("m").y; + wxRect rect = this->GetItemRect(item, GetColumn(colExtruder)); + wxPoint pos = rect.GetPosition(); + pos.y -= 4; + wxSize size = rect.GetSize(); + size.SetWidth(size.GetWidth() + 8); apply_extruder_selector(&m_extruder_editor, this, L("default"), pos, size); @@ -1541,9 +1540,13 @@ void ObjectList::load_modifier(const wxArrayString& input_files, ModelObject& mo const BoundingBoxf3 instance_bb = model_object.instance_bounding_box(instance_idx); // First (any) GLVolume of the selected instance. They all share the same instance matrix. - const GLVolume* v = selection.get_volume(*selection.get_volume_idxs().begin()); + const GLVolume* v = selection.get_first_volume(); const Geometry::Transformation inst_transform = v->get_instance_transformation(); +#if ENABLE_WORLD_COORDINATE + const Transform3d inv_inst_transform = inst_transform.get_matrix_no_offset().inverse(); +#else const Transform3d inv_inst_transform = inst_transform.get_matrix(true).inverse(); +#endif // ENABLE_WORLD_COORDINATE const Vec3d instance_offset = v->get_instance_offset(); for (size_t i = 0; i < input_files.size(); ++i) { @@ -1591,9 +1594,15 @@ void ObjectList::load_modifier(const wxArrayString& input_files, ModelObject& mo new_volume->source.mesh_offset = model.objects.front()->volumes.front()->source.mesh_offset; if (from_galery) { +#if ENABLE_WORLD_COORDINATE + // Transform the new modifier to be aligned with the print bed. + new_volume->set_transformation(v->get_instance_transformation().get_matrix_no_offset().inverse()); + const BoundingBoxf3 mesh_bb = new_volume->mesh().bounding_box(); +#else // Transform the new modifier to be aligned with the print bed. const BoundingBoxf3 mesh_bb = new_volume->mesh().bounding_box(); new_volume->set_transformation(Geometry::Transformation::volume_to_bed_transformation(inst_transform, mesh_bb)); +#endif // ENABLE_WORLD_COORDINATE // Set the modifier position. // Translate the new modifier to be pickable: move to the left front corner of the instance's bounding box, lift to print bed. const Vec3d offset = Vec3d(instance_bb.max.x(), instance_bb.min.y(), instance_bb.min.z()) + 0.5 * mesh_bb.size() - instance_offset; @@ -1661,17 +1670,27 @@ void ObjectList::load_generic_subobject(const std::string& type_name, const Mode ModelVolume *new_volume = model_object.add_volume(std::move(mesh), type); // First (any) GLVolume of the selected instance. They all share the same instance matrix. - const GLVolume* v = selection.get_volume(*selection.get_volume_idxs().begin()); + const GLVolume* v = selection.get_first_volume(); +#if ENABLE_WORLD_COORDINATE // Transform the new modifier to be aligned with the print bed. - const BoundingBoxf3 mesh_bb = new_volume->mesh().bounding_box(); + new_volume->set_transformation(v->get_instance_transformation().get_matrix_no_offset().inverse()); + const BoundingBoxf3 mesh_bb = new_volume->mesh().bounding_box(); +#else + // Transform the new modifier to be aligned with the print bed. + const BoundingBoxf3 mesh_bb = new_volume->mesh().bounding_box(); new_volume->set_transformation(Geometry::Transformation::volume_to_bed_transformation(v->get_instance_transformation(), mesh_bb)); +#endif // ENABLE_WORLD_COORDINATE // Set the modifier position. auto offset = (type_name == "Slab") ? // Slab: Lift to print bed Vec3d(0., 0., 0.5 * mesh_bb.size().z() + instance_bb.min.z() - v->get_instance_offset().z()) : // Translate the new modifier to be pickable: move to the left front corner of the instance's bounding box, lift to print bed. Vec3d(instance_bb.max.x(), instance_bb.min.y(), instance_bb.min.z()) + 0.5 * mesh_bb.size() - v->get_instance_offset(); +#if ENABLE_WORLD_COORDINATE + new_volume->set_offset(v->get_instance_transformation().get_matrix_no_offset().inverse() * offset); +#else new_volume->set_offset(v->get_instance_transformation().get_matrix(true).inverse() * offset); +#endif // ENABLE_WORLD_COORDINATE const wxString name = _L("Generic") + "-" + _(type_name); new_volume->name = into_u8(name); @@ -2444,6 +2463,21 @@ bool ObjectList::can_merge_to_single_object() const return (*m_objects)[obj_idx]->volumes.size() > 1; } +wxPoint ObjectList::get_mouse_position_in_control() const +{ + wxPoint pt = wxGetMousePosition() - this->GetScreenPosition(); + +#ifdef __APPLE__ + // Workaround for OSX. From wxWidgets 3.1.6 Hittest doesn't respect to the header of wxDataViewCtrl + if (wxDataViewItem top_item = this->GetTopItem(); top_item.IsOk()) { + auto rect = this->GetItemRect(top_item, this->GetColumn(0)); + pt.y -= rect.y; + } +#endif // __APPLE__ + + return pt; +} + // NO_PARAMETERS function call means that changed object index will be determine from Selection() void ObjectList::changed_object(const int obj_idx/* = -1*/) const { @@ -2627,8 +2661,14 @@ void ObjectList::part_selection_changed() Sidebar& panel = wxGetApp().sidebar(); panel.Freeze(); +#if ENABLE_WORLD_COORDINATE + const ManipulationEditor* const editor = wxGetApp().obj_manipul()->get_focused_editor(); + const std::string opt_key = (editor != nullptr) ? editor->get_full_opt_name() : ""; + wxGetApp().plater()->canvas3D()->handle_sidebar_focus_event(opt_key, !opt_key.empty()); +#else wxGetApp().plater()->canvas3D()->handle_sidebar_focus_event("", false); - wxGetApp().plater()->canvas3D()->enable_moving(enable_manipulation); +#endif // ENABLE_WORLD_COORDINATE + wxGetApp().plater()->canvas3D()->enable_moving(enable_manipulation); // ysFIXME wxGetApp().obj_manipul() ->UpdateAndShow(update_and_show_manipulations); wxGetApp().obj_settings()->UpdateAndShow(update_and_show_settings); wxGetApp().obj_layers() ->UpdateAndShow(update_and_show_layers); @@ -3373,8 +3413,12 @@ void ObjectList::update_selections() return; sels.Add(m_objects_model->GetItemById(selection.get_object_idx())); } +#if ENABLE_WORLD_COORDINATE + else if (selection.is_single_volume_or_modifier()) { +#else else if (selection.is_single_volume() || selection.is_any_modifier()) { - const auto gl_vol = selection.get_volume(*selection.get_volume_idxs().begin()); +#endif // ENABLE_WORLD_COORDINATE + const auto gl_vol = selection.get_first_volume(); if (m_objects_model->GetVolumeIdByItem(m_objects_model->GetParent(item)) == gl_vol->volume_idx()) return; } @@ -4388,9 +4432,6 @@ void ObjectList::msw_rescale() GetColumn(colExtruder)->SetWidth( 8 * em); GetColumn(colEditing )->SetWidth( 3 * em); - // rescale/update existing items with bitmaps - m_objects_model->Rescale(); - Layout(); } @@ -4398,7 +4439,10 @@ void ObjectList::sys_color_changed() { wxGetApp().UpdateDVCDarkUI(this, true); - msw_rescale(); + // update existing items with bitmaps + m_objects_model->UpdateBitmaps(); + + Layout(); } void ObjectList::ItemValueChanged(wxDataViewEvent &event) diff --git a/src/slic3r/GUI/GUI_ObjectList.hpp b/src/slic3r/GUI/GUI_ObjectList.hpp index 25614581b..79f8e74c6 100644 --- a/src/slic3r/GUI/GUI_ObjectList.hpp +++ b/src/slic3r/GUI/GUI_ObjectList.hpp @@ -280,7 +280,7 @@ public: bool can_merge_to_multipart_object() const; bool can_merge_to_single_object() const; - wxPoint get_mouse_position_in_control() const { return wxGetMousePosition() - this->GetScreenPosition(); } + wxPoint get_mouse_position_in_control() const; wxBoxSizer* get_sizer() {return m_sizer;} int get_selected_obj_idx() const; ModelConfig& get_item_config(const wxDataViewItem& item) const; diff --git a/src/slic3r/GUI/GUI_ObjectManipulation.cpp b/src/slic3r/GUI/GUI_ObjectManipulation.cpp index 089e89dfe..b3f71687b 100644 --- a/src/slic3r/GUI/GUI_ObjectManipulation.cpp +++ b/src/slic3r/GUI/GUI_ObjectManipulation.cpp @@ -52,10 +52,17 @@ static choice_ctrl* create_word_local_combo(wxWindow *parent) temp->SetFont(Slic3r::GUI::wxGetApp().normal_font()); if (!wxOSX) temp->SetBackgroundStyle(wxBG_STYLE_PAINT); +#if ENABLE_WORLD_COORDINATE + temp->Append(ObjectManipulation::coordinate_type_str(ECoordinatesType::World)); + temp->Append(ObjectManipulation::coordinate_type_str(ECoordinatesType::Instance)); + temp->Append(ObjectManipulation::coordinate_type_str(ECoordinatesType::Local)); + temp->Select((int)ECoordinatesType::World); +#else temp->Append(_L("World coordinates")); temp->Append(_L("Local coordinates")); temp->SetSelection(0); temp->SetValue(temp->GetString(0)); +#endif // ENABLE_WORLD_COORDINATE temp->SetToolTip(_L("Select coordinate space, in which the transformation will be performed.")); return temp; @@ -81,8 +88,14 @@ void msw_rescale_word_local_combo(choice_ctrl* combo) // Set rescaled size combo->SetSize(size); +#if ENABLE_WORLD_COORDINATE + combo->Append(ObjectManipulation::coordinate_type_str(ECoordinatesType::World)); + combo->Append(ObjectManipulation::coordinate_type_str(ECoordinatesType::Instance)); + combo->Append(ObjectManipulation::coordinate_type_str(ECoordinatesType::Local)); +#else combo->Append(_L("World coordinates")); combo->Append(_L("Local coordinates")); +#endif // ENABLE_WORLD_COORDINATE combo->SetValue(selection); #else @@ -101,6 +114,7 @@ static void set_font_and_background_style(wxWindow* win, const wxFont& font) static const wxString axes_color_text[] = { "#990000", "#009900", "#000099" }; static const wxString axes_color_back[] = { "#f5dcdc", "#dcf5dc", "#dcdcf5" }; + ObjectManipulation::ObjectManipulation(wxWindow* parent) : OG_Settings(parent, true) { @@ -112,7 +126,7 @@ ObjectManipulation::ObjectManipulation(wxWindow* parent) : // Load bitmaps to be used for the mirroring buttons: m_mirror_bitmap_on = ScalableBitmap(parent, "mirroring_on"); m_mirror_bitmap_off = ScalableBitmap(parent, "mirroring_off"); - m_mirror_bitmap_hidden = ScalableBitmap(parent, "mirroring_transparent.png"); + m_mirror_bitmap_hidden = ScalableBitmap(parent, "mirroring_transparent"); const int border = wxOSX ? 0 : 4; const int em = wxGetApp().em_unit(); @@ -157,8 +171,12 @@ ObjectManipulation::ObjectManipulation(wxWindow* parent) : // Add world local combobox m_word_local_combo = create_word_local_combo(parent); m_word_local_combo->Bind(wxEVT_COMBOBOX, ([this](wxCommandEvent& evt) { +#if ENABLE_WORLD_COORDINATE + this->set_coordinates_type(evt.GetString()); +#else this->set_world_coordinates(evt.GetSelection() != 1); - }), m_word_local_combo->GetId()); +#endif // ENABLE_WORLD_COORDINATE + }), m_word_local_combo->GetId()); // Small trick to correct layouting in different view_mode : // Show empty string of a same height as a m_word_local_combo, when m_word_local_combo is hidden @@ -264,8 +282,12 @@ ObjectManipulation::ObjectManipulation(wxWindow* parent) : GLCanvas3D* canvas = wxGetApp().plater()->canvas3D(); Selection& selection = canvas->get_selection(); +#if ENABLE_WORLD_COORDINATE + if (selection.is_single_volume_or_modifier()) { +#else if (selection.is_single_volume() || selection.is_single_modifier()) { - GLVolume* volume = const_cast(selection.get_volume(*selection.get_volume_idxs().begin())); +#endif // ENABLE_WORLD_COORDINATE + GLVolume* volume = const_cast(selection.get_first_volume()); volume->set_volume_mirror(axis, -volume->get_volume_mirror(axis)); } else if (selection.is_single_full_instance()) { @@ -278,7 +300,7 @@ ObjectManipulation::ObjectManipulation(wxWindow* parent) : return; // Update mirroring at the GLVolumes. - selection.synchronize_unselected_instances(Selection::SYNC_ROTATION_GENERAL); + selection.synchronize_unselected_instances(Selection::SyncRotationType::GENERAL); selection.synchronize_unselected_volumes(); // Copy mirroring values from GLVolumes into Model (ModelInstance / ModelVolume), trigger background processing. canvas->do_mirror(L("Set Mirror")); @@ -327,28 +349,60 @@ ObjectManipulation::ObjectManipulation(wxWindow* parent) : GLCanvas3D* canvas = wxGetApp().plater()->canvas3D(); Selection& selection = canvas->get_selection(); - if (selection.is_single_volume() || selection.is_single_modifier()) { - const GLVolume* volume = selection.get_volume(*selection.get_volume_idxs().begin()); +#if ENABLE_WORLD_COORDINATE + if (selection.is_single_volume_or_modifier()) { + const GLVolume* volume = selection.get_first_volume(); + const double min_z = get_volume_min_z(*volume); + if (!is_world_coordinates()) { + const Vec3d diff = m_cache.position - volume->get_instance_transformation().get_matrix_no_offset().inverse() * (min_z * Vec3d::UnitZ()); - const Geometry::Transformation& instance_trafo = volume->get_instance_transformation(); - const Vec3d diff = m_cache.position - instance_trafo.get_matrix(true).inverse() * Vec3d(0., 0., get_volume_min_z(*volume)); + Plater::TakeSnapshot snapshot(wxGetApp().plater(), _L("Drop to bed")); + change_position_value(0, diff.x()); + change_position_value(1, diff.y()); + change_position_value(2, diff.z()); + } + else { + Plater::TakeSnapshot snapshot(wxGetApp().plater(), _L("Drop to bed")); + change_position_value(2, m_cache.position.z() - min_z); + } +#else + if (selection.is_single_volume() || selection.is_single_modifier()) { + const GLVolume* volume = selection.get_first_volume(); + const Vec3d diff = m_cache.position - volume->get_instance_transformation().get_matrix(true).inverse() * (get_volume_min_z(*volume) * Vec3d::UnitZ()); Plater::TakeSnapshot snapshot(wxGetApp().plater(), _L("Drop to bed")); change_position_value(0, diff.x()); change_position_value(1, diff.y()); change_position_value(2, diff.z()); +#endif // ENABLE_WORLD_COORDINATE } else if (selection.is_single_full_instance()) { +#if ENABLE_WORLD_COORDINATE + const double min_z = selection.get_scaled_instance_bounding_box().min.z(); + if (!is_world_coordinates()) { + const GLVolume* volume = selection.get_first_volume(); + const Vec3d diff = m_cache.position - volume->get_instance_transformation().get_matrix_no_offset().inverse() * (min_z * Vec3d::UnitZ()); + + Plater::TakeSnapshot snapshot(wxGetApp().plater(), _L("Drop to bed")); + change_position_value(0, diff.x()); + change_position_value(1, diff.y()); + change_position_value(2, diff.z()); + } + else { +#else const ModelObjectPtrs& objects = wxGetApp().model().objects; const int idx = selection.get_object_idx(); if (0 <= idx && idx < static_cast(objects.size())) { const ModelObject* mo = wxGetApp().model().objects[idx]; const double min_z = mo->bounding_box().min.z(); if (std::abs(min_z) > SINKING_Z_THRESHOLD) { +#endif // ENABLE_WORLD_COORDINATE Plater::TakeSnapshot snapshot(wxGetApp().plater(), _L("Drop to bed")); change_position_value(2, m_cache.position.z() - min_z); } +#if !ENABLE_WORLD_COORDINATE } +#endif // !ENABLE_WORLD_COORDINATE } }); editors_grid_sizer->Add(m_drop_to_bed_button); @@ -365,21 +419,22 @@ ObjectManipulation::ObjectManipulation(wxWindow* parent) : GLCanvas3D* canvas = wxGetApp().plater()->canvas3D(); Selection& selection = canvas->get_selection(); - if (selection.is_single_volume() || selection.is_single_modifier()) { - GLVolume* volume = const_cast(selection.get_volume(*selection.get_volume_idxs().begin())); - volume->set_volume_rotation(Vec3d::Zero()); - } +#if ENABLE_WORLD_COORDINATE + if (selection.is_single_volume_or_modifier()) +#else + if (selection.is_single_volume() || selection.is_single_modifier()) +#endif // ENABLE_WORLD_COORDINATE + const_cast(selection.get_first_volume())->set_volume_rotation(Vec3d::Zero()); else if (selection.is_single_full_instance()) { for (unsigned int idx : selection.get_volume_idxs()) { - GLVolume* volume = const_cast(selection.get_volume(idx)); - volume->set_instance_rotation(Vec3d::Zero()); + const_cast(selection.get_volume(idx))->set_instance_rotation(Vec3d::Zero()); } } else return; // Update rotation at the GLVolumes. - selection.synchronize_unselected_instances(Selection::SYNC_ROTATION_GENERAL); + selection.synchronize_unselected_instances(Selection::SyncRotationType::GENERAL); selection.synchronize_unselected_volumes(); // Copy rotation values from GLVolumes into Model (ModelInstance / ModelVolume), trigger background processing. canvas->do_rotate(L("Reset Rotation")); @@ -397,11 +452,29 @@ ObjectManipulation::ObjectManipulation(wxWindow* parent) : m_reset_scale_button = new ScalableButton(parent, wxID_ANY, ScalableBitmap(parent, "undo")); m_reset_scale_button->SetToolTip(_L("Reset scale")); m_reset_scale_button->Bind(wxEVT_BUTTON, [this](wxCommandEvent& e) { +#if ENABLE_WORLD_COORDINATE + GLCanvas3D* canvas = wxGetApp().plater()->canvas3D(); + Selection& selection = canvas->get_selection(); + if (selection.is_single_volume_or_modifier()) + const_cast(selection.get_first_volume())->set_volume_scaling_factor(Vec3d::Ones()); + else if (selection.is_single_full_instance()) { + for (unsigned int idx : selection.get_volume_idxs()) { + const_cast(selection.get_volume(idx))->set_instance_scaling_factor(Vec3d::Ones()); + } + } + else + return; + + canvas->do_scale(L("Reset scale")); + + UpdateAndShow(true); +#else Plater::TakeSnapshot snapshot(wxGetApp().plater(), _L("Reset scale")); change_scale_value(0, 100.); change_scale_value(1, 100.); change_scale_value(2, 100.); - }); +#endif // ENABLE_WORLD_COORDINATE + }); editors_grid_sizer->Add(m_reset_scale_button); for (size_t axis_idx = 0; axis_idx < sizeof(axes); axis_idx++) @@ -411,6 +484,25 @@ ObjectManipulation::ObjectManipulation(wxWindow* parent) : m_main_grid_sizer->Add(editors_grid_sizer, 1, wxEXPAND); +#if ENABLE_WORLD_COORDINATE + m_skew_label = new wxStaticText(parent, wxID_ANY, _L("Skew")); + m_main_grid_sizer->Add(m_skew_label, 1, wxEXPAND); + + m_reset_skew_button = new ScalableButton(parent, wxID_ANY, ScalableBitmap(parent, "undo")); + m_reset_skew_button->SetToolTip(_L("Reset skew")); + m_reset_skew_button->Bind(wxEVT_BUTTON, [this](wxCommandEvent& e) { + GLCanvas3D* canvas = wxGetApp().plater()->canvas3D(); + Selection& selection = canvas->get_selection(); + if (selection.is_single_full_instance() || selection.is_single_volume_or_modifier()) { + selection.setup_cache(); + selection.reset_skew(); + canvas->do_reset_skew(L("Reset skew")); + UpdateAndShow(true); + } + }); + m_main_grid_sizer->Add(m_reset_skew_button); +#endif // ENABLE_WORLD_COORDINATE + m_check_inch = new wxCheckBox(parent, wxID_ANY, _L("Inches")); m_check_inch->SetFont(wxGetApp().normal_font()); @@ -444,8 +536,27 @@ void ObjectManipulation::Show(const bool show) if (show) { // Show the "World Coordinates" / "Local Coordintes" Combo in Advanced / Expert mode only. - bool show_world_local_combo = wxGetApp().plater()->canvas3D()->get_selection().is_single_full_instance() && wxGetApp().get_mode() != comSimple; - m_word_local_combo->Show(show_world_local_combo); +#if ENABLE_WORLD_COORDINATE + const Selection& selection = wxGetApp().plater()->canvas3D()->get_selection(); + bool show_world_local_combo = wxGetApp().get_mode() != comSimple && (selection.is_single_full_instance() || selection.is_single_volume_or_modifier()); + if (selection.is_single_volume_or_modifier() && m_word_local_combo->GetCount() < 3) { +#ifdef __linux__ + m_word_local_combo->Insert(coordinate_type_str(ECoordinatesType::Instance), 1); +#else + m_word_local_combo->Insert(coordinate_type_str(ECoordinatesType::Instance), wxNullBitmap, 1); +#endif // __linux__ + m_word_local_combo->Select((int)ECoordinatesType::World); + this->set_coordinates_type(m_word_local_combo->GetString(m_word_local_combo->GetSelection())); + } + else if (selection.is_single_full_instance() && m_word_local_combo->GetCount() > 2) { + m_word_local_combo->Delete(1); + m_word_local_combo->Select((int)ECoordinatesType::World); + this->set_coordinates_type(m_word_local_combo->GetString(m_word_local_combo->GetSelection())); + } +#else + bool show_world_local_combo = wxGetApp().plater()->canvas3D()->get_selection().is_single_full_instance() && wxGetApp().get_mode() != comSimple; +#endif // ENABLE_WORLD_COORDINATE + m_word_local_combo->Show(show_world_local_combo); m_empty_str->Show(!show_world_local_combo); } } @@ -505,8 +616,7 @@ void ObjectManipulation::update_ui_from_settings() } m_check_inch->SetValue(m_imperial_units); - if (m_use_colors != (wxGetApp().app_config->get("color_mapinulation_panel") == "1")) - { + if (m_use_colors != (wxGetApp().app_config->get("color_mapinulation_panel") == "1")) { m_use_colors = wxGetApp().app_config->get("color_mapinulation_panel") == "1"; // update colors for edit-boxes int axis_id = 0; @@ -538,33 +648,49 @@ void ObjectManipulation::update_settings_value(const Selection& selection) m_new_rotate_label_string = L("Rotation"); m_new_scale_label_string = L("Scale factors"); +#if !ENABLE_WORLD_COORDINATE if (wxGetApp().get_mode() == comSimple) m_world_coordinates = true; +#endif // !ENABLE_WORLD_COORDINATE ObjectList* obj_list = wxGetApp().obj_list(); if (selection.is_single_full_instance()) { // all volumes in the selection belongs to the same instance, any of them contains the needed instance data, so we take the first one - const GLVolume* volume = selection.get_volume(*selection.get_volume_idxs().begin()); + const GLVolume* volume = selection.get_first_volume(); +#if !ENABLE_WORLD_COORDINATE m_new_position = volume->get_instance_offset(); // Verify whether the instance rotation is multiples of 90 degrees, so that the scaling in world coordinates is possible. - if (m_world_coordinates && ! m_uniform_scale && + if (m_world_coordinates && ! m_uniform_scale && ! Geometry::is_rotation_ninety_degrees(volume->get_instance_rotation())) { // Manipulating an instance in the world coordinate system, rotation is not multiples of ninety degrees, therefore enforce uniform scaling. m_uniform_scale = true; m_lock_bnt->SetLock(true); } +#endif // !ENABLE_WORLD_COORDINATE +#if ENABLE_WORLD_COORDINATE + if (is_world_coordinates()) { + m_new_position = volume->get_instance_offset(); +#else if (m_world_coordinates) { - m_new_rotate_label_string = L("Rotate"); - m_new_rotation = Vec3d::Zero(); - m_new_size = selection.get_scaled_instance_bounding_box().size(); - m_new_scale = m_new_size.cwiseProduct(selection.get_unscaled_instance_bounding_box().size().cwiseInverse()) * 100.; - } +#endif // ENABLE_WORLD_COORDINATE + m_new_rotate_label_string = L("Rotate"); + m_new_rotation = Vec3d::Zero(); + m_new_size = selection.get_scaled_instance_bounding_box().size(); + m_new_scale = m_new_size.cwiseQuotient(selection.get_unscaled_instance_bounding_box().size()) * 100.0; + } else { - m_new_rotation = volume->get_instance_rotation() * (180. / M_PI); - m_new_size = volume->get_instance_scaling_factor().cwiseProduct(wxGetApp().model().objects[volume->object_idx()]->raw_mesh_bounding_box().size()); - m_new_scale = volume->get_instance_scaling_factor() * 100.; +#if ENABLE_WORLD_COORDINATE + m_new_move_label_string = L("Translate"); + m_new_rotate_label_string = L("Rotate"); + m_new_position = Vec3d::Zero(); + m_new_rotation = Vec3d::Zero(); +#else + m_new_rotation = volume->get_instance_rotation() * (180.0 / M_PI); +#endif // ENABLE_WORLD_COORDINATE + m_new_size = volume->get_instance_scaling_factor().cwiseProduct(wxGetApp().model().objects[volume->object_idx()]->raw_mesh_bounding_box().size()); + m_new_scale = volume->get_instance_scaling_factor() * 100.0; } m_new_enabled = true; @@ -573,19 +699,52 @@ void ObjectManipulation::update_settings_value(const Selection& selection) const BoundingBoxf3& box = selection.get_bounding_box(); m_new_position = box.center(); m_new_rotation = Vec3d::Zero(); - m_new_scale = Vec3d(100., 100., 100.); + m_new_scale = Vec3d(100.0, 100.0, 100.0); m_new_size = box.size(); m_new_rotate_label_string = L("Rotate"); m_new_scale_label_string = L("Scale"); m_new_enabled = true; } +#if ENABLE_WORLD_COORDINATE + else if (selection.is_single_volume_or_modifier()) { +#else else if (selection.is_single_modifier() || selection.is_single_volume()) { +#endif // ENABLE_WORLD_COORDINATE // the selection contains a single volume - const GLVolume* volume = selection.get_volume(*selection.get_volume_idxs().begin()); + const GLVolume* volume = selection.get_first_volume(); +#if ENABLE_WORLD_COORDINATE + if (is_world_coordinates()) { + const Geometry::Transformation trafo(volume->world_matrix()); + + const Vec3d& offset = trafo.get_offset(); + + m_new_position = offset; + m_new_rotate_label_string = L("Rotate"); + m_new_rotation = Vec3d::Zero(); + m_new_size = volume->transformed_convex_hull_bounding_box(trafo.get_matrix()).size(); + m_new_scale = m_new_size.cwiseQuotient(volume->transformed_convex_hull_bounding_box(volume->get_instance_transformation().get_matrix() * volume->get_volume_transformation().get_matrix_no_scaling_factor()).size()) * 100.0; + } + else if (is_local_coordinates()) { + m_new_move_label_string = L("Translate"); + m_new_rotate_label_string = L("Rotate"); + m_new_position = Vec3d::Zero(); + m_new_rotation = Vec3d::Zero(); + m_new_scale = volume->get_volume_scaling_factor() * 100.0; + m_new_size = volume->get_volume_scaling_factor().cwiseProduct(volume->bounding_box().size()); + } + else { +#endif // ENABLE_WORLD_COORDINATE m_new_position = volume->get_volume_offset(); - m_new_rotation = volume->get_volume_rotation() * (180. / M_PI); - m_new_scale = volume->get_volume_scaling_factor() * 100.; - m_new_size = volume->get_instance_scaling_factor().cwiseProduct(volume->get_volume_scaling_factor().cwiseProduct(volume->bounding_box().size())); + m_new_rotate_label_string = L("Rotate"); + m_new_rotation = Vec3d::Zero(); +#if ENABLE_WORLD_COORDINATE + m_new_size = volume->transformed_convex_hull_bounding_box(volume->get_volume_transformation().get_matrix()).size(); + m_new_scale = m_new_size.cwiseQuotient(volume->transformed_convex_hull_bounding_box(volume->get_volume_transformation().get_matrix_no_scaling_factor()).size()) * 100.0; + } +#else + m_new_scale = volume->get_volume_scaling_factor() * 100.0; + m_new_size = volume->get_instance_scaling_factor().cwiseProduct(volume->get_volume_scaling_factor().cwiseProduct(volume->bounding_box().size())); +#endif // ENABLE_WORLD_COORDINATE m_new_enabled = true; } else if (obj_list->multiple_selection() || obj_list->is_selected(itInstanceRoot)) { @@ -651,22 +810,26 @@ void ObjectManipulation::update_if_dirty() update(m_cache.rotation, m_cache.rotation_rounded, meRotation, m_new_rotation); } +#if !ENABLE_WORLD_COORDINATE if (selection.requires_uniform_scale()) { m_lock_bnt->SetLock(true); m_lock_bnt->SetToolTip(_L("You cannot use non-uniform scaling mode for multiple objects/parts selection")); m_lock_bnt->disable(); } else { +#endif // !ENABLE_WORLD_COORDINATE m_lock_bnt->SetLock(m_uniform_scale); m_lock_bnt->SetToolTip(wxEmptyString); m_lock_bnt->enable(); +#if !ENABLE_WORLD_COORDINATE } - { + { int new_selection = m_world_coordinates ? 0 : 1; if (m_word_local_combo->GetSelection() != new_selection) m_word_local_combo->SetSelection(new_selection); } +#endif // !ENABLE_WORLD_COORDINATE if (m_new_enabled) m_og->enable(); @@ -693,29 +856,75 @@ void ObjectManipulation::update_reset_buttons_visibility() bool show_rotation = false; bool show_scale = false; bool show_drop_to_bed = false; +#if ENABLE_WORLD_COORDINATE + bool show_skew = false; + if (selection.is_single_full_instance() || selection.is_single_volume_or_modifier()) { + const double min_z = selection.is_single_full_instance() ? selection.get_scaled_instance_bounding_box().min.z() : + get_volume_min_z(*selection.get_first_volume()); + + show_drop_to_bed = std::abs(min_z) > EPSILON; + const GLVolume* volume = selection.get_first_volume(); + Transform3d rotation = Transform3d::Identity(); + Transform3d scale = Transform3d::Identity(); + Geometry::Transformation skew; +#else if (selection.is_single_full_instance() || selection.is_single_modifier() || selection.is_single_volume()) { - const GLVolume* volume = selection.get_volume(*selection.get_volume_idxs().begin()); + const GLVolume* volume = selection.get_first_volume(); Vec3d rotation; Vec3d scale; - double min_z = 0.; + double min_z = 0.0; +#endif // ENABLE_WORLD_COORDINATE if (selection.is_single_full_instance()) { +#if ENABLE_WORLD_COORDINATE + const Geometry::Transformation& trafo = volume->get_instance_transformation(); + rotation = trafo.get_rotation_matrix(); + scale = trafo.get_scaling_factor_matrix(); + const Selection::IndicesList& idxs = selection.get_volume_idxs(); + for (unsigned int id : idxs) { + const Geometry::Transformation world_trafo(selection.get_volume(id)->world_matrix()); + if (world_trafo.has_skew()) { + skew = world_trafo; + break; + } + } +#else rotation = volume->get_instance_rotation(); scale = volume->get_instance_scaling_factor(); - min_z = wxGetApp().model().objects[volume->composite_id.object_id]->bounding_box().min.z(); + min_z = selection.get_scaled_instance_bounding_box().min.z(); +#endif // ENABLE_WORLD_COORDINATE } else { +#if ENABLE_WORLD_COORDINATE + const Geometry::Transformation& trafo = volume->get_volume_transformation(); + rotation = trafo.get_rotation_matrix(); + scale = trafo.get_scaling_factor_matrix(); + const Geometry::Transformation world_trafo(volume->world_matrix()); + if (world_trafo.has_skew()) + skew = world_trafo; +#else rotation = volume->get_volume_rotation(); scale = volume->get_volume_scaling_factor(); min_z = get_volume_min_z(*volume); +#endif // ENABLE_WORLD_COORDINATE } +#if ENABLE_WORLD_COORDINATE + show_rotation = !rotation.isApprox(Transform3d::Identity()); + show_scale = !scale.isApprox(Transform3d::Identity()); + show_skew = skew.has_skew(); +#else show_rotation = !rotation.isApprox(Vec3d::Zero()); show_scale = !scale.isApprox(Vec3d::Ones()); show_drop_to_bed = std::abs(min_z) > SINKING_Z_THRESHOLD; +#endif // ENABLE_WORLD_COORDINATE } +#if ENABLE_WORLD_COORDINATE + wxGetApp().CallAfter([this, show_rotation, show_scale, show_drop_to_bed, show_skew] { +#else wxGetApp().CallAfter([this, show_rotation, show_scale, show_drop_to_bed] { +#endif // ENABLE_WORLD_COORDINATE // There is a case (under OSX), when this function is called after the Manipulation panel is hidden // So, let check if Manipulation panel is still shown for this moment if (!this->IsShown()) @@ -723,6 +932,10 @@ void ObjectManipulation::update_reset_buttons_visibility() m_reset_rotation_button->Show(show_rotation); m_reset_scale_button->Show(show_scale); m_drop_to_bed_button->Show(show_drop_to_bed); +#if ENABLE_WORLD_COORDINATE + m_reset_skew_button->Show(show_skew); + m_skew_label->Show(show_skew); +#endif // ENABLE_WORLD_COORDINATE // Because of CallAfter we need to layout sidebar after Show/hide of reset buttons one more time Sidebar& panel = wxGetApp().sidebar(); @@ -742,9 +955,17 @@ void ObjectManipulation::update_mirror_buttons_visibility() Selection& selection = canvas->get_selection(); std::array new_states = {mbHidden, mbHidden, mbHidden}; +#if ENABLE_WORLD_COORDINATE + if (is_local_coordinates()) { +#else if (!m_world_coordinates) { +#endif // ENABLE_WORLD_COORDINATE +#if ENABLE_WORLD_COORDINATE + if (selection.is_single_full_instance() || selection.is_single_volume_or_modifier()) { +#else if (selection.is_single_full_instance() || selection.is_single_modifier() || selection.is_single_volume()) { - const GLVolume* volume = selection.get_volume(*selection.get_volume_idxs().begin()); +#endif // ENABLE_WORLD_COORDINATE + const GLVolume* volume = selection.get_first_volume(); Vec3d mirror; if (selection.is_single_full_instance()) @@ -804,10 +1025,23 @@ void ObjectManipulation::update_warning_icon_state(const MeshErrorsInfo& warning m_manifold_warning_bmp = ScalableBitmap(m_parent, warning_icon_name); const wxString& tooltip = warning.tooltip; m_fix_throught_netfab_bitmap->SetBitmap(tooltip.IsEmpty() ? wxNullBitmap : m_manifold_warning_bmp.bmp()); - m_fix_throught_netfab_bitmap->SetMinSize(tooltip.IsEmpty() ? wxSize(0,0) : m_manifold_warning_bmp.bmp().GetSize()); + m_fix_throught_netfab_bitmap->SetMinSize(tooltip.IsEmpty() ? wxSize(0,0) : m_manifold_warning_bmp.GetSize()); m_fix_throught_netfab_bitmap->SetToolTip(tooltip); } +#if ENABLE_WORLD_COORDINATE +wxString ObjectManipulation::coordinate_type_str(ECoordinatesType type) +{ + switch (type) + { + case ECoordinatesType::World: { return _L("World coordinates"); } + case ECoordinatesType::Instance: { return _L("Instance coordinates"); } + case ECoordinatesType::Local: { return _L("Local coordinates"); } + default: { assert(false); return _L("Unknown"); } + } +} +#endif // ENABLE_WORLD_COORDINATE + void ObjectManipulation::reset_settings_value() { m_new_position = Vec3d::Zero(); @@ -831,7 +1065,19 @@ void ObjectManipulation::change_position_value(int axis, double value) auto canvas = wxGetApp().plater()->canvas3D(); Selection& selection = canvas->get_selection(); selection.setup_cache(); +#if ENABLE_WORLD_COORDINATE + TransformationType trafo_type; + trafo_type.set_relative(); + switch (get_coordinates_type()) + { + case ECoordinatesType::Instance: { trafo_type.set_instance(); break; } + case ECoordinatesType::Local: { trafo_type.set_local(); break; } + default: { break; } + } + selection.translate(position - m_cache.position, trafo_type); +#else selection.translate(position - m_cache.position, selection.requires_local_axes()); +#endif // ENABLE_WORLD_COORDINATE canvas->do_move(L("Set Position")); m_cache.position = position; @@ -850,6 +1096,18 @@ void ObjectManipulation::change_rotation_value(int axis, double value) GLCanvas3D* canvas = wxGetApp().plater()->canvas3D(); Selection& selection = canvas->get_selection(); +#if ENABLE_WORLD_COORDINATE + TransformationType transformation_type; + transformation_type.set_relative(); + if (selection.is_single_full_instance()) + transformation_type.set_independent(); + + if (is_local_coordinates()) + transformation_type.set_local(); + + if (is_instance_coordinates()) + transformation_type.set_instance(); +#else TransformationType transformation_type(TransformationType::World_Relative_Joint); if (selection.is_single_full_instance() || selection.requires_local_axes()) transformation_type.set_independent(); @@ -858,6 +1116,7 @@ void ObjectManipulation::change_rotation_value(int axis, double value) // transformation_type.set_absolute(); transformation_type.set_local(); } +#endif // ENABLE_WORLD_COORDINATE selection.setup_cache(); selection.rotate( @@ -903,8 +1162,12 @@ void ObjectManipulation::change_size_value(int axis, double value) const Selection& selection = wxGetApp().plater()->canvas3D()->get_selection(); Vec3d ref_size = m_cache.size; +#if ENABLE_WORLD_COORDINATE + if (selection.is_single_volume_or_modifier()) { +#else if (selection.is_single_volume() || selection.is_single_modifier()) { - const GLVolume* v = selection.get_volume(*selection.get_volume_idxs().begin()); +#endif // ENABLE_WORLD_COORDINATE + const GLVolume* v = selection.get_first_volume(); const Vec3d local_size = size.cwiseQuotient(v->get_instance_scaling_factor()); const Vec3d local_ref_size = v->bounding_box().size().cwiseProduct(v->get_volume_scaling_factor()); const Vec3d local_change = local_size.cwiseQuotient(local_ref_size); @@ -913,11 +1176,19 @@ void ObjectManipulation::change_size_value(int axis, double value) ref_size = Vec3d::Ones(); } else if (selection.is_single_full_instance()) - ref_size = m_world_coordinates ? +#if ENABLE_WORLD_COORDINATE + ref_size = is_world_coordinates() ? +#else + ref_size = m_world_coordinates ? +#endif // ENABLE_WORLD_COORDINATE selection.get_unscaled_instance_bounding_box().size() : - wxGetApp().model().objects[selection.get_volume(*selection.get_volume_idxs().begin())->object_idx()]->raw_mesh_bounding_box().size(); + wxGetApp().model().objects[selection.get_first_volume()->object_idx()]->raw_mesh_bounding_box().size(); +#if ENABLE_WORLD_COORDINATE + this->do_size(axis, size.cwiseQuotient(ref_size)); +#else this->do_scale(axis, size.cwiseQuotient(ref_size)); +#endif // ENABLE_WORLD_COORDINATE m_cache.size = size; m_cache.size_rounded(axis) = DBL_MAX; @@ -927,8 +1198,22 @@ void ObjectManipulation::change_size_value(int axis, double value) void ObjectManipulation::do_scale(int axis, const Vec3d &scale) const { Selection& selection = wxGetApp().plater()->canvas3D()->get_selection(); +#if !ENABLE_WORLD_COORDINATE Vec3d scaling_factor = scale; +#endif // !ENABLE_WORLD_COORDINATE +#if ENABLE_WORLD_COORDINATE + TransformationType transformation_type; + if (is_local_coordinates()) + transformation_type.set_local(); + else if (is_instance_coordinates()) + transformation_type.set_instance(); + + if (!selection.is_single_full_instance() && !selection.is_single_volume_or_modifier()) + transformation_type.set_relative(); + + const Vec3d scaling_factor = m_uniform_scale ? scale(axis) * Vec3d::Ones() : scale; +#else TransformationType transformation_type(TransformationType::World_Relative_Joint); if (selection.is_single_full_instance()) { transformation_type.set_absolute(); @@ -938,12 +1223,31 @@ void ObjectManipulation::do_scale(int axis, const Vec3d &scale) const if (m_uniform_scale || selection.requires_uniform_scale()) scaling_factor = scale(axis) * Vec3d::Ones(); +#endif // ENABLE_WORLD_COORDINATE selection.setup_cache(); selection.scale(scaling_factor, transformation_type); wxGetApp().plater()->canvas3D()->do_scale(L("Set Scale")); } +#if ENABLE_WORLD_COORDINATE +void ObjectManipulation::do_size(int axis, const Vec3d& scale) const +{ + Selection& selection = wxGetApp().plater()->canvas3D()->get_selection(); + + TransformationType transformation_type; + if (is_local_coordinates()) + transformation_type.set_local(); + else if (is_instance_coordinates()) + transformation_type.set_instance(); + + const Vec3d scaling_factor = m_uniform_scale ? scale(axis) * Vec3d::Ones() : scale; + selection.setup_cache(); + selection.scale(scaling_factor, transformation_type); + wxGetApp().plater()->canvas3D()->do_scale(L("Set Size")); +} +#endif // ENABLE_WORLD_COORDINATE + void ObjectManipulation::on_change(const std::string& opt_key, int axis, double new_value) { if (!m_cache.is_valid()) @@ -978,17 +1282,26 @@ void ObjectManipulation::on_change(const std::string& opt_key, int axis, double } } -void ObjectManipulation::set_uniform_scaling(const bool new_value) +void ObjectManipulation::set_uniform_scaling(const bool use_uniform_scale) { - const Selection &selection = wxGetApp().plater()->canvas3D()->get_selection(); - if (selection.is_single_full_instance() && m_world_coordinates && !new_value) { +#if ENABLE_WORLD_COORDINATE + if (!use_uniform_scale) + // Recalculate cached values at this panel, refresh the screen. + this->UpdateAndShow(true); + + m_uniform_scale = use_uniform_scale; + + set_dirty(); +#else + const Selection& selection = wxGetApp().plater()->canvas3D()->get_selection(); + if (selection.is_single_full_instance() && m_world_coordinates && !use_uniform_scale) { // Verify whether the instance rotation is multiples of 90 degrees, so that the scaling in world coordinates is possible. // all volumes in the selection belongs to the same instance, any of them contains the needed instance data, so we take the first one - const GLVolume* volume = selection.get_volume(*selection.get_volume_idxs().begin()); + const GLVolume* volume = selection.get_first_volume(); // Is the angle close to a multiple of 90 degrees? - if (! Geometry::is_rotation_ninety_degrees(volume->get_instance_rotation())) { + if (!Geometry::is_rotation_ninety_degrees(volume->get_instance_rotation())) { // Cannot apply scaling in the world coordinate system. - //wxMessageDialog dlg(GUI::wxGetApp().mainframe, + //wxMessageDialog dlg(GUI::wxGetApp().mainframe, MessageDialog dlg(GUI::wxGetApp().mainframe, _L("The currently manipulated object is tilted (rotation angles are not multiples of 90°).\n" "Non-uniform scaling of tilted objects is only possible in the World coordinate system,\n" @@ -996,7 +1309,7 @@ void ObjectManipulation::set_uniform_scaling(const bool new_value) _L("This operation is irreversible.\n" "Do you want to proceed?"), SLIC3R_APP_NAME, - wxYES_NO | wxCANCEL | wxCANCEL_DEFAULT | wxICON_QUESTION); + wxYES_NO | wxCANCEL | wxCANCEL_DEFAULT | wxICON_QUESTION); if (dlg.ShowModal() != wxID_YES) { // Enforce uniform scaling. m_lock_bnt->SetLock(true); @@ -1010,31 +1323,39 @@ void ObjectManipulation::set_uniform_scaling(const bool new_value) this->UpdateAndShow(true); } } - m_uniform_scale = new_value; + + m_uniform_scale = use_uniform_scale; +#endif // ENABLE_WORLD_COORDINATE } +#if ENABLE_WORLD_COORDINATE +void ObjectManipulation::set_coordinates_type(ECoordinatesType type) +{ + if (wxGetApp().get_mode() == comSimple) + type = ECoordinatesType::World; + + if (m_coordinates_type == type) + return; + + m_coordinates_type = type; + this->UpdateAndShow(true); + GLCanvas3D* canvas = wxGetApp().plater()->canvas3D(); + canvas->get_gizmos_manager().update_data(); + canvas->set_as_dirty(); + canvas->request_extra_frame(); +} +#endif // ENABLE_WORLD_COORDINATE + void ObjectManipulation::msw_rescale() { const int em = wxGetApp().em_unit(); m_item_name->SetMinSize(wxSize(20*em, wxDefaultCoord)); msw_rescale_word_local_combo(m_word_local_combo); m_word_local_combo_sizer->SetMinSize(wxSize(-1, m_word_local_combo->GetBestHeight(-1))); - m_manifold_warning_bmp.msw_rescale(); const wxString& tooltip = m_fix_throught_netfab_bitmap->GetToolTipText(); m_fix_throught_netfab_bitmap->SetBitmap(tooltip.IsEmpty() ? wxNullBitmap : m_manifold_warning_bmp.bmp()); - m_fix_throught_netfab_bitmap->SetMinSize(tooltip.IsEmpty() ? wxSize(0, 0) : m_manifold_warning_bmp.bmp().GetSize()); - - m_mirror_bitmap_on.msw_rescale(); - m_mirror_bitmap_off.msw_rescale(); - m_mirror_bitmap_hidden.msw_rescale(); - m_reset_scale_button->msw_rescale(); - m_reset_rotation_button->msw_rescale(); - m_drop_to_bed_button->msw_rescale(); - m_lock_bnt->msw_rescale(); - - for (int id = 0; id < 3; ++id) - m_mirror_buttons[id].first->msw_rescale(); + m_fix_throught_netfab_bitmap->SetMinSize(tooltip.IsEmpty() ? wxSize(0, 0) : m_manifold_warning_bmp.GetSize()); // rescale label-heights // Text trick to grid sizer layout: @@ -1063,19 +1384,31 @@ void ObjectManipulation::sys_color_changed() for (ManipulationEditor* editor : m_editors) editor->sys_color_changed(this); - // btn...->msw_rescale() updates icon on button, so use it - m_mirror_bitmap_on.msw_rescale(); - m_mirror_bitmap_off.msw_rescale(); - m_mirror_bitmap_hidden.msw_rescale(); - m_reset_scale_button->msw_rescale(); - m_reset_rotation_button->msw_rescale(); - m_drop_to_bed_button->msw_rescale(); - m_lock_bnt->msw_rescale(); + m_mirror_bitmap_on.sys_color_changed(); + m_mirror_bitmap_off.sys_color_changed(); + m_mirror_bitmap_hidden.sys_color_changed(); + m_reset_scale_button->sys_color_changed(); + m_reset_rotation_button->sys_color_changed(); + m_drop_to_bed_button->sys_color_changed(); + m_lock_bnt->sys_color_changed(); for (int id = 0; id < 3; ++id) - m_mirror_buttons[id].first->msw_rescale(); + m_mirror_buttons[id].first->sys_color_changed(); } +#if ENABLE_WORLD_COORDINATE +void ObjectManipulation::set_coordinates_type(const wxString& type_string) +{ + ECoordinatesType type = ECoordinatesType::World; + if (type_string == coordinate_type_str(ECoordinatesType::Instance)) + type = ECoordinatesType::Instance; + else if (type_string == coordinate_type_str(ECoordinatesType::Local)) + type = ECoordinatesType::Local; + + this->set_coordinates_type(type); +} +#endif // ENABLE_WORLD_COORDINATE + static const char axes[] = { 'x', 'y', 'z' }; ManipulationEditor::ManipulationEditor(ObjectManipulation* parent, const std::string& opt_key, @@ -1117,8 +1450,8 @@ ManipulationEditor::ManipulationEditor(ObjectManipulation* parent, parent->set_focused_editor(nullptr); #if ENABLE_OBJECT_MANIPULATOR_FOCUS - // if the widgets exchanging focus are both manipulator fields, call kill_focus - if (dynamic_cast(e.GetEventObject()) != nullptr && dynamic_cast(e.GetWindow()) != nullptr) + // if the widgets loosing focus is a manipulator field, call kill_focus + if (dynamic_cast(e.GetEventObject()) != nullptr) #else if (!m_enter_pressed) #endif // ENABLE_OBJECT_MANIPULATOR_FOCUS diff --git a/src/slic3r/GUI/GUI_ObjectManipulation.hpp b/src/slic3r/GUI/GUI_ObjectManipulation.hpp index 3a2eca2b6..936ac99d8 100644 --- a/src/slic3r/GUI/GUI_ObjectManipulation.hpp +++ b/src/slic3r/GUI/GUI_ObjectManipulation.hpp @@ -5,6 +5,9 @@ #include "GUI_ObjectSettings.hpp" #include "GUI_ObjectList.hpp" +#if ENABLE_WORLD_COORDINATE +#include "GUI_Geometry.hpp" +#endif // ENABLE_WORLD_COORDINATE #include "libslic3r/Point.hpp" #include @@ -57,6 +60,10 @@ public: void set_value(const wxString& new_value); void kill_focus(ObjectManipulation *parent); +#if ENABLE_WORLD_COORDINATE + const std::string& get_full_opt_name() const { return m_full_opt_name; } +#endif // ENABLE_WORLD_COORDINATE + bool has_opt_key(const std::string& key) { return m_opt_key == key; } private: @@ -115,9 +122,12 @@ private: wxStaticText* m_empty_str = nullptr; // Non-owning pointers to the reset buttons, so we can hide and show them. - ScalableButton* m_reset_scale_button = nullptr; - ScalableButton* m_reset_rotation_button = nullptr; - ScalableButton* m_drop_to_bed_button = nullptr; + ScalableButton* m_reset_scale_button{ nullptr }; + ScalableButton* m_reset_rotation_button{ nullptr }; +#if ENABLE_WORLD_COORDINATE + ScalableButton* m_reset_skew_button{ nullptr }; +#endif // ENABLE_WORLD_COORDINATE + ScalableButton* m_drop_to_bed_button{ nullptr }; wxCheckBox* m_check_inch {nullptr}; @@ -146,22 +156,35 @@ private: Vec3d m_new_size; bool m_new_enabled {true}; bool m_uniform_scale {true}; +#if ENABLE_WORLD_COORDINATE + ECoordinatesType m_coordinates_type{ ECoordinatesType::World }; +#else // Does the object manipulation panel work in World or Local coordinates? bool m_world_coordinates = true; +#endif // ENABLE_WORLD_COORDINATE LockButton* m_lock_bnt{ nullptr }; choice_ctrl* m_word_local_combo { nullptr }; ScalableBitmap m_manifold_warning_bmp; wxStaticBitmap* m_fix_throught_netfab_bitmap; +#if ENABLE_WORLD_COORDINATE + // Currently focused editor (nullptr if none) + ManipulationEditor* m_focused_editor{ nullptr }; +#else #ifndef __APPLE__ // Currently focused editor (nullptr if none) ManipulationEditor* m_focused_editor {nullptr}; #endif // __APPLE__ +#endif // ENABLE_WORLD_COORDINATE wxFlexGridSizer* m_main_grid_sizer; wxFlexGridSizer* m_labels_grid_sizer; +#if ENABLE_WORLD_COORDINATE + wxStaticText* m_skew_label{ nullptr }; +#endif // ENABLE_WORLD_COORDINATE + // sizers, used for msw_rescale wxBoxSizer* m_word_local_combo_sizer; std::vector m_rescalable_sizers; @@ -185,11 +208,19 @@ public: // Called from the App to update the UI if dirty. void update_if_dirty(); - void set_uniform_scaling(const bool uniform_scale); + void set_uniform_scaling(const bool use_uniform_scale); bool get_uniform_scaling() const { return m_uniform_scale; } +#if ENABLE_WORLD_COORDINATE + void set_coordinates_type(ECoordinatesType type); + ECoordinatesType get_coordinates_type() const { return m_coordinates_type; } + bool is_world_coordinates() const { return m_coordinates_type == ECoordinatesType::World; } + bool is_instance_coordinates() const { return m_coordinates_type == ECoordinatesType::Instance; } + bool is_local_coordinates() const { return m_coordinates_type == ECoordinatesType::Local; } +#else // Does the object manipulation panel work in World or Local coordinates? void set_world_coordinates(const bool world_coordinates) { m_world_coordinates = world_coordinates; this->UpdateAndShow(true); } bool get_world_coordinates() const { return m_world_coordinates; } +#endif // ENABLE_WORLD_COORDINATE void reset_cache() { m_cache.reset(); } #ifndef __APPLE__ @@ -205,11 +236,23 @@ public: void sys_color_changed(); void on_change(const std::string& opt_key, int axis, double new_value); void set_focused_editor(ManipulationEditor* focused_editor) { +#if ENABLE_WORLD_COORDINATE + m_focused_editor = focused_editor; +#else #ifndef __APPLE__ m_focused_editor = focused_editor; #endif // __APPLE__ +#endif // ENABLE_WORLD_COORDINATE } +#if ENABLE_WORLD_COORDINATE + ManipulationEditor* get_focused_editor() { return m_focused_editor; } +#endif // ENABLE_WORLD_COORDINATE + +#if ENABLE_WORLD_COORDINATE + static wxString coordinate_type_str(ECoordinatesType type); +#endif // ENABLE_WORLD_COORDINATE + private: void reset_settings_value(); void update_settings_value(const Selection& selection); @@ -225,6 +268,11 @@ private: void change_scale_value(int axis, double value); void change_size_value(int axis, double value); void do_scale(int axis, const Vec3d &scale) const; +#if ENABLE_WORLD_COORDINATE + void do_size(int axis, const Vec3d& scale) const; + + void set_coordinates_type(const wxString& type_string); +#endif // ENABLE_WORLD_COORDINATE }; }} diff --git a/src/slic3r/GUI/GUI_ObjectSettings.cpp b/src/slic3r/GUI/GUI_ObjectSettings.cpp index 291013fe9..97eb5f10d 100644 --- a/src/slic3r/GUI/GUI_ObjectSettings.cpp +++ b/src/slic3r/GUI/GUI_ObjectSettings.cpp @@ -99,7 +99,7 @@ bool ObjectSettings::update_settings_list() btn->SetToolTip(_(L("Remove parameter"))); btn->SetBitmapFocus(m_bmp_delete_focus.bmp()); - btn->SetBitmapHover(m_bmp_delete_focus.bmp()); + btn->SetBitmapCurrent(m_bmp_delete_focus.bmp()); btn->Bind(wxEVT_BUTTON, [opt_key, config, this](wxEvent &event) { wxGetApp().plater()->take_snapshot(from_u8((boost::format(_utf8(L("Delete Option %s"))) % opt_key).str())); @@ -133,7 +133,7 @@ bool ObjectSettings::update_settings_list() return; ctrl->SetBitmap_(m_bmp_delete); ctrl->SetBitmapFocus(m_bmp_delete_focus.bmp()); - ctrl->SetBitmapHover(m_bmp_delete_focus.bmp()); + ctrl->SetBitmapCurrent(m_bmp_delete_focus.bmp()); }; const bool is_extruders_cat = cat.first == "Extruders"; @@ -268,15 +268,6 @@ void ObjectSettings::UpdateAndShow(const bool show) OG_Settings::UpdateAndShow(show ? update_settings_list() : false); } -void ObjectSettings::msw_rescale() -{ - m_bmp_delete.msw_rescale(); - m_bmp_delete_focus.msw_rescale(); - - for (auto group : m_og_settings) - group->msw_rescale(); -} - void ObjectSettings::sys_color_changed() { m_og->sys_color_changed(); diff --git a/src/slic3r/GUI/GUI_ObjectSettings.hpp b/src/slic3r/GUI/GUI_ObjectSettings.hpp index e5a6937f1..5d0be1308 100644 --- a/src/slic3r/GUI/GUI_ObjectSettings.hpp +++ b/src/slic3r/GUI/GUI_ObjectSettings.hpp @@ -56,7 +56,6 @@ public: bool add_missed_options(ModelConfig *config_to, const DynamicPrintConfig &config_from); void update_config_values(ModelConfig *config); void UpdateAndShow(const bool show) override; - void msw_rescale(); void sys_color_changed(); }; diff --git a/src/slic3r/GUI/GUI_Preview.cpp b/src/slic3r/GUI/GUI_Preview.cpp index 9f62fa0b8..88bd42a66 100644 --- a/src/slic3r/GUI/GUI_Preview.cpp +++ b/src/slic3r/GUI/GUI_Preview.cpp @@ -983,10 +983,11 @@ void Preview::load_print_as_fff(bool keep_z_range) if (gcode_preview_data_valid) { // Load the real G-code preview. m_canvas->load_gcode_preview(*m_gcode_result, colors); - m_left_sizer->Show(m_bottom_toolbar_panel); m_left_sizer->Layout(); Refresh(); zs = m_canvas->get_gcode_layers_zs(); + if (!zs.empty()) + m_left_sizer->Show(m_bottom_toolbar_panel); m_loaded = true; } else if (wxGetApp().is_editor()) { diff --git a/src/slic3r/GUI/GalleryDialog.cpp b/src/slic3r/GUI/GalleryDialog.cpp index 975b807dc..6be6a94fe 100644 --- a/src/slic3r/GUI/GalleryDialog.cpp +++ b/src/slic3r/GUI/GalleryDialog.cpp @@ -157,8 +157,9 @@ bool GalleryDialog::can_change_thumbnail() void GalleryDialog::on_dpi_changed(const wxRect& suggested_rect) { - const int& em = em_unit(); + update(); + const int& em = em_unit(); msw_buttons_rescale(this, em, { ID_BTN_ADD_CUSTOM_SHAPE, ID_BTN_DEL_CUSTOM_SHAPE, ID_BTN_REPLACE_CUSTOM_PNG, wxID_OK, wxID_CLOSE }); wxSize size = wxSize(50 * em, 35 * em); @@ -169,13 +170,14 @@ void GalleryDialog::on_dpi_changed(const wxRect& suggested_rect) Refresh(); } -static void add_lock(wxImage& image) +static void add_lock(wxImage& image, wxWindow* parent_win) { - int lock_sz = 22; + wxBitmapBundle* bmp_bndl = get_bmp_bundle("lock", 22); #ifdef __APPLE__ - lock_sz /= mac_max_scaling_factor(); + wxBitmap bmp = bmp_bndl->GetBitmap(bmp_bndl->GetDefaultSize() * mac_max_scaling_factor()); +#else + wxBitmap bmp = bmp_bndl->GetBitmapFor(parent_win); #endif - wxBitmap bmp = create_scaled_bitmap("lock", nullptr, lock_sz); wxImage lock_image = bmp.ConvertToImage(); if (!lock_image.IsOk() || lock_image.GetWidth() == 0 || lock_image.GetHeight() == 0) @@ -213,21 +215,28 @@ static void add_lock(wxImage& image) } } -static void add_default_image(wxImageList* img_list, bool is_system) +static void add_default_image(wxImageList* img_list, bool is_system, wxWindow* parent_win) { - int sz = IMG_PX_CNT; + wxBitmapBundle* bmp_bndl = get_bmp_bundle("cog", IMG_PX_CNT); #ifdef __APPLE__ - sz /= mac_max_scaling_factor(); + wxBitmap bmp = bmp_bndl->GetBitmap(bmp_bndl->GetDefaultSize() * mac_max_scaling_factor()); +#else + wxBitmap bmp = bmp_bndl->GetBitmapFor(parent_win); #endif - wxBitmap bmp = create_scaled_bitmap("cog", nullptr, sz, true); + bmp = bmp.ConvertToDisabled(); if (is_system) { wxImage image = bmp.ConvertToImage(); if (image.IsOk() && image.GetWidth() != 0 && image.GetHeight() != 0) { - add_lock(image); + add_lock(image, parent_win); +#ifdef __APPLE__ + bmp = wxBitmap(std::move(image), -1, mac_max_scaling_factor()); +#else bmp = wxBitmap(std::move(image)); +#endif } } + img_list->Add(bmp); }; @@ -344,8 +353,13 @@ void GalleryDialog::load_label_icon_list() // Make an image list containing large icons +#ifdef __APPLE__ + m_image_list = new wxImageList(IMG_PX_CNT, IMG_PX_CNT); + int px_cnt = IMG_PX_CNT * mac_max_scaling_factor(); +#else int px_cnt = (int)(em_unit() * IMG_PX_CNT * 0.1f + 0.5f); m_image_list = new wxImageList(px_cnt, px_cnt); +#endif std::string ext = ".png"; @@ -364,7 +378,7 @@ void GalleryDialog::load_label_icon_list() if (can_generate_thumbnail) generate_thumbnail_from_model(model_name); else { - add_default_image(m_image_list, item.is_system); + add_default_image(m_image_list, item.is_system, this); continue; } } @@ -373,14 +387,18 @@ void GalleryDialog::load_label_icon_list() if (!image.CanRead(from_u8(img_name)) || !image.LoadFile(from_u8(img_name), wxBITMAP_TYPE_PNG) || image.GetWidth() == 0 || image.GetHeight() == 0) { - add_default_image(m_image_list, item.is_system); + add_default_image(m_image_list, item.is_system, this); continue; } image.Rescale(px_cnt, px_cnt, wxIMAGE_QUALITY_BILINEAR); if (item.is_system) - add_lock(image); + add_lock(image, this); +#ifdef __APPLE__ + wxBitmap bmp = wxBitmap(std::move(image), -1, mac_max_scaling_factor()); +#else wxBitmap bmp = wxBitmap(std::move(image)); +#endif m_image_list->Add(bmp); } diff --git a/src/slic3r/GUI/Gizmos/GLGizmoBase.cpp b/src/slic3r/GUI/Gizmos/GLGizmoBase.cpp index 1ad5a6004..6ddb804d4 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoBase.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoBase.cpp @@ -316,8 +316,6 @@ bool GLGizmoBase::use_grabbers(const wxMouseEvent &mouse_event) { m_grabbers[m_hover_id - m_group_id].dragging = true; } - // prevent change of hover_id during dragging - m_parent.set_mouse_as_dragging(); on_start_dragging(); // Let the plater know that the dragging started @@ -329,7 +327,6 @@ bool GLGizmoBase::use_grabbers(const wxMouseEvent &mouse_event) { // when mouse cursor leave window than finish actual dragging operation bool is_leaving = mouse_event.Leaving(); if (mouse_event.Dragging()) { - m_parent.set_mouse_as_dragging(); Point mouse_coord(mouse_event.GetX(), mouse_event.GetY()); auto ray = m_parent.mouse_ray(mouse_coord); UpdateData data(ray, mouse_coord); @@ -339,7 +336,11 @@ bool GLGizmoBase::use_grabbers(const wxMouseEvent &mouse_event) { wxGetApp().obj_manipul()->set_dirty(); m_parent.set_as_dirty(); return true; - } else if (mouse_event.LeftUp() || is_leaving || is_dragging_finished) { + } + else if (mouse_event.LeftUp() || is_leaving || is_dragging_finished) { +#if ENABLE_WORLD_COORDINATE + do_stop_dragging(is_leaving); +#else for (auto &grabber : m_grabbers) grabber.dragging = false; m_dragging = false; @@ -362,12 +363,41 @@ bool GLGizmoBase::use_grabbers(const wxMouseEvent &mouse_event) { m_parent.post_event(SimpleEvent(EVT_GLCANVAS_MOUSE_DRAGGING_FINISHED)); // updates camera target constraints m_parent.refresh_camera_scene_box(); +#endif // ENABLE_WORLD_COORDINATE return true; } } return false; } +#if ENABLE_WORLD_COORDINATE +void GLGizmoBase::do_stop_dragging(bool perform_mouse_cleanup) +{ + for (auto& grabber : m_grabbers) grabber.dragging = false; + m_dragging = false; + + // NOTE: This should be part of GLCanvas3D + // Reset hover_id when leave window + if (perform_mouse_cleanup) m_parent.mouse_up_cleanup(); + + on_stop_dragging(); + + // There is prediction that after draggign, data are changed + // Data are updated twice also by canvas3D::reload_scene. + // Should be fixed. + m_parent.get_gizmos_manager().update_data(); + + wxGetApp().obj_manipul()->set_dirty(); + + // Let the plater know that the dragging finished, so a delayed + // refresh of the scene with the background processing data should + // be performed. + m_parent.post_event(SimpleEvent(EVT_GLCANVAS_MOUSE_DRAGGING_FINISHED)); + // updates camera target constraints + m_parent.refresh_camera_scene_box(); +} +#endif // ENABLE_WORLD_COORDINATE + std::string GLGizmoBase::format(float value, unsigned int decimals) const { return Slic3r::string_printf("%.*f", decimals, value); diff --git a/src/slic3r/GUI/Gizmos/GLGizmoBase.hpp b/src/slic3r/GUI/Gizmos/GLGizmoBase.hpp index f61654183..95763f004 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoBase.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoBase.hpp @@ -89,151 +89,156 @@ protected: static GLModel s_cone; #else GLModel m_cube; -#endif // ENABLE_GIZMO_GRABBER_REFACTOR - }; - -public: - enum EState - { - Off, - On, - Num_States - }; - - struct UpdateData - { - const Linef3& mouse_ray; - const Point& mouse_pos; - - UpdateData(const Linef3& mouse_ray, const Point& mouse_pos) - : mouse_ray(mouse_ray), mouse_pos(mouse_pos) - {} - }; - -protected: - GLCanvas3D& m_parent; - int m_group_id; // TODO: remove only for rotate - EState m_state; - int m_shortcut_key; - std::string m_icon_filename; - unsigned int m_sprite_id; - int m_hover_id; - bool m_dragging; - mutable std::vector m_grabbers; - ImGuiWrapper* m_imgui; - bool m_first_input_window_render; - CommonGizmosDataPool* m_c; -public: - GLGizmoBase(GLCanvas3D& parent, - const std::string& icon_filename, - unsigned int sprite_id); - virtual ~GLGizmoBase() = default; - - bool init() { return on_init(); } - - void load(cereal::BinaryInputArchive& ar) { m_state = On; on_load(ar); } - void save(cereal::BinaryOutputArchive& ar) const { on_save(ar); } - - std::string get_name(bool include_shortcut = true) const; - - EState get_state() const { return m_state; } - void set_state(EState state) { m_state = state; on_set_state(); } - - int get_shortcut_key() const { return m_shortcut_key; } - - const std::string& get_icon_filename() const { return m_icon_filename; } - - bool is_activable() const { return on_is_activable(); } - bool is_selectable() const { return on_is_selectable(); } - CommonGizmosDataID get_requirements() const { return on_get_requirements(); } - virtual bool wants_enter_leave_snapshots() const { return false; } - virtual std::string get_gizmo_entering_text() const { assert(false); return ""; } - virtual std::string get_gizmo_leaving_text() const { assert(false); return ""; } - virtual std::string get_action_snapshot_name() { return _u8L("Gizmo action"); } - void set_common_data_pool(CommonGizmosDataPool* ptr) { m_c = ptr; } - - unsigned int get_sprite_id() const { return m_sprite_id; } - - int get_hover_id() const { return m_hover_id; } - void set_hover_id(int id); - - bool is_dragging() const { return m_dragging; } - - // returns True when Gizmo changed its state - bool update_items_state(); - - void render() { on_render(); } - void render_for_picking() { on_render_for_picking(); } - void render_input_window(float x, float y, float bottom_limit); - - /// - /// Mouse tooltip text - /// - /// Text to be visible in mouse tooltip - virtual std::string get_tooltip() const { return ""; } - - /// - /// Is called when data (Selection) is changed - /// - virtual void data_changed(){}; - - /// - /// Implement when want to process mouse events in gizmo - /// Click, Right click, move, drag, ... - /// - /// Keep information about mouse click - /// Return True when use the information and don't want to propagate it otherwise False. - virtual bool on_mouse(const wxMouseEvent &mouse_event) { return false; } -protected: - virtual bool on_init() = 0; - virtual void on_load(cereal::BinaryInputArchive& ar) {} - virtual void on_save(cereal::BinaryOutputArchive& ar) const {} - virtual std::string on_get_name() const = 0; - virtual void on_set_state() {} - virtual void on_set_hover_id() {} - virtual bool on_is_activable() const { return true; } - virtual bool on_is_selectable() const { return true; } - virtual CommonGizmosDataID on_get_requirements() const { return CommonGizmosDataID(0); } - virtual void on_enable_grabber(unsigned int id) {} - virtual void on_disable_grabber(unsigned int id) {} - - // called inside use_grabbers - virtual void on_start_dragging() {} - virtual void on_stop_dragging() {} - virtual void on_dragging(const UpdateData& data) {} - - virtual void on_render() = 0; - virtual void on_render_for_picking() = 0; - virtual void on_render_input_window(float x, float y, float bottom_limit) {} - - // Returns the picking color for the given id, based on the BASE_ID constant - // No check is made for clashing with other picking color (i.e. GLVolumes) - ColorRGBA picking_color_component(unsigned int id) const; - - void render_grabbers(const BoundingBoxf3& box) const; - void render_grabbers(float size) const; - void render_grabbers_for_picking(const BoundingBoxf3& box) const; - - std::string format(float value, unsigned int decimals) const; - - // Mark gizmo as dirty to Re-Render when idle() - void set_dirty(); - - /// - /// function which - /// Set up m_dragging and call functions - /// on_start_dragging / on_dragging / on_stop_dragging - /// - /// Keep information about mouse click - /// same as on_mouse - bool use_grabbers(const wxMouseEvent &mouse_event); -private: - // Flag for dirty visible state of Gizmo - // When True then need new rendering - bool m_dirty; -}; - -} // namespace GUI -} // namespace Slic3r - -#endif // slic3r_GLGizmoBase_hpp_ +#endif // ENABLE_GIZMO_GRABBER_REFACTOR + }; + +public: + enum EState + { + Off, + On, + Num_States + }; + + struct UpdateData + { + const Linef3& mouse_ray; + const Point& mouse_pos; + + UpdateData(const Linef3& mouse_ray, const Point& mouse_pos) + : mouse_ray(mouse_ray), mouse_pos(mouse_pos) + {} + }; + +protected: + GLCanvas3D& m_parent; + int m_group_id; // TODO: remove only for rotate + EState m_state; + int m_shortcut_key; + std::string m_icon_filename; + unsigned int m_sprite_id; + int m_hover_id; + bool m_dragging; + mutable std::vector m_grabbers; + ImGuiWrapper* m_imgui; + bool m_first_input_window_render; + CommonGizmosDataPool* m_c; +public: + GLGizmoBase(GLCanvas3D& parent, + const std::string& icon_filename, + unsigned int sprite_id); + virtual ~GLGizmoBase() = default; + + bool init() { return on_init(); } + + void load(cereal::BinaryInputArchive& ar) { m_state = On; on_load(ar); } + void save(cereal::BinaryOutputArchive& ar) const { on_save(ar); } + + std::string get_name(bool include_shortcut = true) const; + + EState get_state() const { return m_state; } + void set_state(EState state) { m_state = state; on_set_state(); } + + int get_shortcut_key() const { return m_shortcut_key; } + + const std::string& get_icon_filename() const { return m_icon_filename; } + + bool is_activable() const { return on_is_activable(); } + bool is_selectable() const { return on_is_selectable(); } + CommonGizmosDataID get_requirements() const { return on_get_requirements(); } + virtual bool wants_enter_leave_snapshots() const { return false; } + virtual std::string get_gizmo_entering_text() const { assert(false); return ""; } + virtual std::string get_gizmo_leaving_text() const { assert(false); return ""; } + virtual std::string get_action_snapshot_name() { return _u8L("Gizmo action"); } + void set_common_data_pool(CommonGizmosDataPool* ptr) { m_c = ptr; } + + unsigned int get_sprite_id() const { return m_sprite_id; } + + int get_hover_id() const { return m_hover_id; } + void set_hover_id(int id); + + bool is_dragging() const { return m_dragging; } + + // returns True when Gizmo changed its state + bool update_items_state(); + + void render() { on_render(); } + void render_for_picking() { on_render_for_picking(); } + void render_input_window(float x, float y, float bottom_limit); + + /// + /// Mouse tooltip text + /// + /// Text to be visible in mouse tooltip + virtual std::string get_tooltip() const { return ""; } + + /// + /// Is called when data (Selection) is changed + /// + virtual void data_changed(){}; + + /// + /// Implement when want to process mouse events in gizmo + /// Click, Right click, move, drag, ... + /// + /// Keep information about mouse click + /// Return True when use the information and don't want to propagate it otherwise False. + virtual bool on_mouse(const wxMouseEvent &mouse_event) { return false; } +protected: + virtual bool on_init() = 0; + virtual void on_load(cereal::BinaryInputArchive& ar) {} + virtual void on_save(cereal::BinaryOutputArchive& ar) const {} + virtual std::string on_get_name() const = 0; + virtual void on_set_state() {} + virtual void on_set_hover_id() {} + virtual bool on_is_activable() const { return true; } + virtual bool on_is_selectable() const { return true; } + virtual CommonGizmosDataID on_get_requirements() const { return CommonGizmosDataID(0); } + virtual void on_enable_grabber(unsigned int id) {} + virtual void on_disable_grabber(unsigned int id) {} + + // called inside use_grabbers + virtual void on_start_dragging() {} + virtual void on_stop_dragging() {} + virtual void on_dragging(const UpdateData& data) {} + + virtual void on_render() = 0; + virtual void on_render_for_picking() = 0; + virtual void on_render_input_window(float x, float y, float bottom_limit) {} + + // Returns the picking color for the given id, based on the BASE_ID constant + // No check is made for clashing with other picking color (i.e. GLVolumes) + ColorRGBA picking_color_component(unsigned int id) const; + + void render_grabbers(const BoundingBoxf3& box) const; + void render_grabbers(float size) const; + void render_grabbers_for_picking(const BoundingBoxf3& box) const; + + std::string format(float value, unsigned int decimals) const; + + // Mark gizmo as dirty to Re-Render when idle() + void set_dirty(); + + /// + /// function which + /// Set up m_dragging and call functions + /// on_start_dragging / on_dragging / on_stop_dragging + /// + /// Keep information about mouse click + /// same as on_mouse + bool use_grabbers(const wxMouseEvent &mouse_event); + +#if ENABLE_WORLD_COORDINATE + void do_stop_dragging(bool perform_mouse_cleanup); +#endif // ENABLE_WORLD_COORDINATE + +private: + // Flag for dirty visible state of Gizmo + // When True then need new rendering + bool m_dirty; +}; + +} // namespace GUI +} // namespace Slic3r + +#endif // slic3r_GLGizmoBase_hpp_ diff --git a/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp b/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp index 58eb19a04..350f8cdc0 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp @@ -980,7 +980,7 @@ void GLGizmoCut3D::on_render_input_window(float x, float y, float bottom_limit) // for (Axis axis : {X, Y, Z}) // render_rotation_input(axis); - // m_imgui->text(_L("°")); + // m_imgui->text(_L("°")); // ImGui::Separator(); @@ -1289,7 +1289,7 @@ void GLGizmoCut3D::perform_cut(const Selection& selection) wxCHECK_RET(instance_idx >= 0 && object_idx >= 0, "GLGizmoCut: Invalid object selection"); // m_cut_z is the distance from the bed. Subtract possible SLA elevation. - const GLVolume* first_glvolume = selection.get_volume(*selection.get_volume_idxs().begin()); + const GLVolume* first_glvolume = selection.get_first_volume(); const double object_cut_z = m_plane_center.z() - first_glvolume->get_sla_shift_z(); const Vec3d& instance_offset = wxGetApp().plater()->model().objects[object_idx]->instances[instance_idx]->get_offset(); @@ -1412,6 +1412,9 @@ void GLGizmoCut3D::update_connector_shape() void GLGizmoCut3D::update_model_object() const { +/* const Selection& selection = m_parent.get_selection(); + const GLVolume* first_glvolume = selection.get_first_volume(); + const BoundingBoxf3& box = first_glvolume->transformed_convex_hull_bounding_box();*/ const ModelObjectPtrs& mos = wxGetApp().model().objects; ModelObject* mo = m_c->selection_info()->model_object(); wxGetApp().obj_list()->update_info_items(std::find(mos.begin(), mos.end(), mo) - mos.begin()); diff --git a/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp b/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp index b6da2e63d..aee2785db 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp @@ -341,7 +341,11 @@ void GLGizmoFdmSupports::select_facets_by_angle(float threshold_deg, bool block) ++mesh_id; +#if ENABLE_WORLD_COORDINATE + const Transform3d trafo_matrix = mi->get_matrix_no_offset() * mv->get_matrix_no_offset(); +#else const Transform3d trafo_matrix = mi->get_matrix(true) * mv->get_matrix(true); +#endif // ENABLE_WORLD_COORDINATE Vec3f down = (trafo_matrix.inverse() * (-Vec3d::UnitZ())).cast().normalized(); Vec3f limit = (trafo_matrix.inverse() * Vec3d(std::sin(threshold), 0, -std::cos(threshold))).cast().normalized(); diff --git a/src/slic3r/GUI/Gizmos/GLGizmoFlatten.cpp b/src/slic3r/GUI/Gizmos/GLGizmoFlatten.cpp index aa291f623..f854edb81 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoFlatten.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoFlatten.cpp @@ -1,475 +1,479 @@ -// Include GLGizmoBase.hpp before I18N.hpp as it includes some libigl code, which overrides our localization "L" macro. -#include "GLGizmoFlatten.hpp" -#include "slic3r/GUI/GLCanvas3D.hpp" -#if ENABLE_LEGACY_OPENGL_REMOVAL -#include "slic3r/GUI/GUI_App.hpp" -#endif // ENABLE_LEGACY_OPENGL_REMOVAL -#if ENABLE_GL_SHADERS_ATTRIBUTES -#include "slic3r/GUI/Plater.hpp" -#endif // ENABLE_GL_SHADERS_ATTRIBUTES - -#include "slic3r/GUI/Gizmos/GLGizmosCommon.hpp" - -#include "libslic3r/Geometry/ConvexHull.hpp" -#include "libslic3r/Model.hpp" - -#include - -#include - -namespace Slic3r { -namespace GUI { - -static const Slic3r::ColorRGBA DEFAULT_PLANE_COLOR = { 0.9f, 0.9f, 0.9f, 0.5f }; -static const Slic3r::ColorRGBA DEFAULT_HOVER_PLANE_COLOR = { 0.9f, 0.9f, 0.9f, 0.75f }; - -GLGizmoFlatten::GLGizmoFlatten(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id) - : GLGizmoBase(parent, icon_filename, sprite_id) -{} - -bool GLGizmoFlatten::on_mouse(const wxMouseEvent &mouse_event) -{ - if (mouse_event.Moving()) { - // only for sure - m_mouse_left_down = false; - return false; - } - if (mouse_event.LeftDown()) { - if (m_hover_id != -1) { - m_mouse_left_down = true; - Selection &selection = m_parent.get_selection(); - if (selection.is_single_full_instance()) { - // Rotate the object so the normal points downward: - selection.flattening_rotate(m_planes[m_hover_id].normal); - m_parent.do_rotate(L("Gizmo-Place on Face")); - } - return true; - } - - // fix: prevent restart gizmo when reselect object - // take responsibility for left up - if (m_parent.get_first_hover_volume_idx() >= 0) m_mouse_left_down = true; - - } else if (mouse_event.LeftUp()) { - if (m_mouse_left_down) { - // responsible for mouse left up after selecting plane - m_mouse_left_down = false; - return true; - } - } else if (mouse_event.Leaving()) { - m_mouse_left_down = false; - } - return false; -} - -void GLGizmoFlatten::data_changed() -{ - const Selection & selection = m_parent.get_selection(); - const ModelObject *model_object = nullptr; - if (selection.is_single_full_instance() || - selection.is_from_single_object() ) { - model_object = selection.get_model()->objects[selection.get_object_idx()]; - } - set_flattening_data(model_object); -} - -bool GLGizmoFlatten::on_init() -{ - m_shortcut_key = WXK_CONTROL_F; - return true; -} - -void GLGizmoFlatten::on_set_state() -{ -} - -CommonGizmosDataID GLGizmoFlatten::on_get_requirements() const -{ - return CommonGizmosDataID::SelectionInfo; -} - -std::string GLGizmoFlatten::on_get_name() const -{ - return _u8L("Place on face"); -} - -bool GLGizmoFlatten::on_is_activable() const -{ - // This is assumed in GLCanvas3D::do_rotate, do not change this - // without updating that function too. - return m_parent.get_selection().is_single_full_instance(); -} - -void GLGizmoFlatten::on_render() -{ - const Selection& selection = m_parent.get_selection(); - -#if ENABLE_LEGACY_OPENGL_REMOVAL - GLShaderProgram* shader = wxGetApp().get_shader("flat"); - if (shader == nullptr) - return; - - shader->start_using(); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - - glsafe(::glClear(GL_DEPTH_BUFFER_BIT)); - - glsafe(::glEnable(GL_DEPTH_TEST)); - glsafe(::glEnable(GL_BLEND)); - - if (selection.is_single_full_instance()) { - const Transform3d& m = selection.get_volume(*selection.get_volume_idxs().begin())->get_instance_transformation().get_matrix(); -#if ENABLE_GL_SHADERS_ATTRIBUTES - const Camera& camera = wxGetApp().plater()->get_camera(); - const Transform3d view_model_matrix = camera.get_view_matrix() * - Geometry::assemble_transform(selection.get_volume(*selection.get_volume_idxs().begin())->get_sla_shift_z() * Vec3d::UnitZ()) * m; - - shader->set_uniform("view_model_matrix", view_model_matrix); - shader->set_uniform("projection_matrix", camera.get_projection_matrix()); -#else - glsafe(::glPushMatrix()); - glsafe(::glTranslatef(0.f, 0.f, selection.get_volume(*selection.get_volume_idxs().begin())->get_sla_shift_z())); - glsafe(::glMultMatrixd(m.data())); -#endif // ENABLE_GL_SHADERS_ATTRIBUTES - if (this->is_plane_update_necessary()) - update_planes(); - for (int i = 0; i < (int)m_planes.size(); ++i) { -#if ENABLE_LEGACY_OPENGL_REMOVAL - m_planes[i].vbo.set_color(i == m_hover_id ? DEFAULT_HOVER_PLANE_COLOR : DEFAULT_PLANE_COLOR); - m_planes[i].vbo.render(); -#else - glsafe(::glColor4fv(i == m_hover_id ? DEFAULT_HOVER_PLANE_COLOR.data() : DEFAULT_PLANE_COLOR.data())); - if (m_planes[i].vbo.has_VBOs()) - m_planes[i].vbo.render(); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - } -#if !ENABLE_GL_SHADERS_ATTRIBUTES - glsafe(::glPopMatrix()); -#endif // !ENABLE_GL_SHADERS_ATTRIBUTES - } - - glsafe(::glEnable(GL_CULL_FACE)); - glsafe(::glDisable(GL_BLEND)); - -#if ENABLE_LEGACY_OPENGL_REMOVAL - shader->stop_using(); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL -} - -void GLGizmoFlatten::on_render_for_picking() -{ - const Selection& selection = m_parent.get_selection(); - -#if ENABLE_LEGACY_OPENGL_REMOVAL - GLShaderProgram* shader = wxGetApp().get_shader("flat"); - if (shader == nullptr) - return; - - shader->start_using(); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - - glsafe(::glDisable(GL_DEPTH_TEST)); - glsafe(::glDisable(GL_BLEND)); - - if (selection.is_single_full_instance() && !wxGetKeyState(WXK_CONTROL)) { - const Transform3d& m = selection.get_volume(*selection.get_volume_idxs().begin())->get_instance_transformation().get_matrix(); -#if ENABLE_GL_SHADERS_ATTRIBUTES - const Camera& camera = wxGetApp().plater()->get_camera(); - const Transform3d view_model_matrix = camera.get_view_matrix() * - Geometry::assemble_transform(selection.get_volume(*selection.get_volume_idxs().begin())->get_sla_shift_z() * Vec3d::UnitZ()) * m; - - shader->set_uniform("view_model_matrix", view_model_matrix); - shader->set_uniform("projection_matrix", camera.get_projection_matrix()); -#else - glsafe(::glPushMatrix()); - glsafe(::glTranslatef(0.f, 0.f, selection.get_volume(*selection.get_volume_idxs().begin())->get_sla_shift_z())); - glsafe(::glMultMatrixd(m.data())); -#endif // ENABLE_GL_SHADERS_ATTRIBUTES - if (this->is_plane_update_necessary()) - update_planes(); - for (int i = 0; i < (int)m_planes.size(); ++i) { -#if ENABLE_LEGACY_OPENGL_REMOVAL - m_planes[i].vbo.set_color(picking_color_component(i)); -#else - glsafe(::glColor4fv(picking_color_component(i).data())); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - m_planes[i].vbo.render(); - } -#if !ENABLE_GL_SHADERS_ATTRIBUTES - glsafe(::glPopMatrix()); -#endif // !ENABLE_GL_SHADERS_ATTRIBUTES - } - - glsafe(::glEnable(GL_CULL_FACE)); - -#if ENABLE_LEGACY_OPENGL_REMOVAL - shader->stop_using(); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL -} - -void GLGizmoFlatten::set_flattening_data(const ModelObject* model_object) -{ - if (model_object != m_old_model_object) { - m_planes.clear(); - m_planes_valid = false; - } -} - -void GLGizmoFlatten::update_planes() -{ - const ModelObject* mo = m_c->selection_info()->model_object(); - TriangleMesh ch; - for (const ModelVolume* vol : mo->volumes) { - if (vol->type() != ModelVolumeType::MODEL_PART) - continue; - TriangleMesh vol_ch = vol->get_convex_hull(); - vol_ch.transform(vol->get_matrix()); - ch.merge(vol_ch); - } - ch = ch.convex_hull_3d(); - m_planes.clear(); - const Transform3d& inst_matrix = mo->instances.front()->get_matrix(true); - - // Following constants are used for discarding too small polygons. - const float minimal_area = 5.f; // in square mm (world coordinates) - const float minimal_side = 1.f; // mm - - // Now we'll go through all the facets and append Points of facets sharing the same normal. - // This part is still performed in mesh coordinate system. - const int num_of_facets = ch.facets_count(); - const std::vector face_normals = its_face_normals(ch.its); - const std::vector face_neighbors = its_face_neighbors(ch.its); - std::vector facet_queue(num_of_facets, 0); - std::vector facet_visited(num_of_facets, false); - int facet_queue_cnt = 0; - const stl_normal* normal_ptr = nullptr; - int facet_idx = 0; - while (1) { - // Find next unvisited triangle: - for (; facet_idx < num_of_facets; ++ facet_idx) - if (!facet_visited[facet_idx]) { - facet_queue[facet_queue_cnt ++] = facet_idx; - facet_visited[facet_idx] = true; - normal_ptr = &face_normals[facet_idx]; - m_planes.emplace_back(); - break; - } - if (facet_idx == num_of_facets) - break; // Everything was visited already - - while (facet_queue_cnt > 0) { - int facet_idx = facet_queue[-- facet_queue_cnt]; - const stl_normal& this_normal = face_normals[facet_idx]; - if (std::abs(this_normal(0) - (*normal_ptr)(0)) < 0.001 && std::abs(this_normal(1) - (*normal_ptr)(1)) < 0.001 && std::abs(this_normal(2) - (*normal_ptr)(2)) < 0.001) { - const Vec3i face = ch.its.indices[facet_idx]; - for (int j=0; j<3; ++j) - m_planes.back().vertices.emplace_back(ch.its.vertices[face[j]].cast()); - - facet_visited[facet_idx] = true; - for (int j = 0; j < 3; ++ j) - if (int neighbor_idx = face_neighbors[facet_idx][j]; neighbor_idx >= 0 && ! facet_visited[neighbor_idx]) - facet_queue[facet_queue_cnt ++] = neighbor_idx; - } - } - m_planes.back().normal = normal_ptr->cast(); - - Pointf3s& verts = m_planes.back().vertices; - // Now we'll transform all the points into world coordinates, so that the areas, angles and distances - // make real sense. - verts = transform(verts, inst_matrix); - - // if this is a just a very small triangle, remove it to speed up further calculations (it would be rejected later anyway): - if (verts.size() == 3 && - ((verts[0] - verts[1]).norm() < minimal_side - || (verts[0] - verts[2]).norm() < minimal_side - || (verts[1] - verts[2]).norm() < minimal_side)) - m_planes.pop_back(); - } - - // Let's prepare transformation of the normal vector from mesh to instance coordinates. - Geometry::Transformation t(inst_matrix); - Vec3d scaling = t.get_scaling_factor(); - t.set_scaling_factor(Vec3d(1./scaling(0), 1./scaling(1), 1./scaling(2))); - - // Now we'll go through all the polygons, transform the points into xy plane to process them: - for (unsigned int polygon_id=0; polygon_id < m_planes.size(); ++polygon_id) { - Pointf3s& polygon = m_planes[polygon_id].vertices; - const Vec3d& normal = m_planes[polygon_id].normal; - - // transform the normal according to the instance matrix: - Vec3d normal_transformed = t.get_matrix() * normal; - - // We are going to rotate about z and y to flatten the plane - Eigen::Quaterniond q; - Transform3d m = Transform3d::Identity(); - m.matrix().block(0, 0, 3, 3) = q.setFromTwoVectors(normal_transformed, Vec3d::UnitZ()).toRotationMatrix(); - polygon = transform(polygon, m); - - // Now to remove the inner points. We'll misuse Geometry::convex_hull for that, but since - // it works in fixed point representation, we will rescale the polygon to avoid overflows. - // And yes, it is a nasty thing to do. Whoever has time is free to refactor. - Vec3d bb_size = BoundingBoxf3(polygon).size(); - float sf = std::min(1./bb_size(0), 1./bb_size(1)); - Transform3d tr = Geometry::assemble_transform(Vec3d::Zero(), Vec3d::Zero(), Vec3d(sf, sf, 1.f)); - polygon = transform(polygon, tr); - polygon = Slic3r::Geometry::convex_hull(polygon); - polygon = transform(polygon, tr.inverse()); - - // Calculate area of the polygons and discard ones that are too small - float& area = m_planes[polygon_id].area; - area = 0.f; - for (unsigned int i = 0; i < polygon.size(); i++) // Shoelace formula - area += polygon[i](0)*polygon[i + 1 < polygon.size() ? i + 1 : 0](1) - polygon[i + 1 < polygon.size() ? i + 1 : 0](0)*polygon[i](1); - area = 0.5f * std::abs(area); - - bool discard = false; - if (area < minimal_area) - discard = true; - else { - // We also check the inner angles and discard polygons with angles smaller than the following threshold - const double angle_threshold = ::cos(10.0 * (double)PI / 180.0); - - for (unsigned int i = 0; i < polygon.size(); ++i) { - const Vec3d& prec = polygon[(i == 0) ? polygon.size() - 1 : i - 1]; - const Vec3d& curr = polygon[i]; - const Vec3d& next = polygon[(i == polygon.size() - 1) ? 0 : i + 1]; - - if ((prec - curr).normalized().dot((next - curr).normalized()) > angle_threshold) { - discard = true; - break; - } - } - } - - if (discard) { - m_planes[polygon_id--] = std::move(m_planes.back()); - m_planes.pop_back(); - continue; - } - - // We will shrink the polygon a little bit so it does not touch the object edges: - Vec3d centroid = std::accumulate(polygon.begin(), polygon.end(), Vec3d(0.0, 0.0, 0.0)); - centroid /= (double)polygon.size(); - for (auto& vertex : polygon) - vertex = 0.9f*vertex + 0.1f*centroid; - - // Polygon is now simple and convex, we'll round the corners to make them look nicer. - // The algorithm takes a vertex, calculates middles of respective sides and moves the vertex - // towards their average (controlled by 'aggressivity'). This is repeated k times. - // In next iterations, the neighbours are not always taken at the middle (to increase the - // rounding effect at the corners, where we need it most). - const unsigned int k = 10; // number of iterations - const float aggressivity = 0.2f; // agressivity - const unsigned int N = polygon.size(); - std::vector> neighbours; - if (k != 0) { - Pointf3s points_out(2*k*N); // vector long enough to store the future vertices - for (unsigned int j=0; jvolumes) { - m_volumes_matrices.push_back(vol->get_matrix()); - m_volumes_types.push_back(vol->type()); - } - m_first_instance_scale = mo->instances.front()->get_scaling_factor(); - m_first_instance_mirror = mo->instances.front()->get_mirror(); - m_old_model_object = mo; - - // And finally create respective VBOs. The polygon is convex with - // the vertices in order, so triangulation is trivial. - for (auto& plane : m_planes) { -#if ENABLE_LEGACY_OPENGL_REMOVAL - GLModel::Geometry init_data; - init_data.format = { GLModel::Geometry::EPrimitiveType::TriangleFan, GLModel::Geometry::EVertexLayout::P3N3 }; - init_data.reserve_vertices(plane.vertices.size()); - init_data.reserve_indices(plane.vertices.size()); - // vertices + indices - for (size_t i = 0; i < plane.vertices.size(); ++i) { - init_data.add_vertex((Vec3f)plane.vertices[i].cast(), (Vec3f)plane.normal.cast()); - init_data.add_index((unsigned int)i); - } - plane.vbo.init_from(std::move(init_data)); -#else - plane.vbo.reserve(plane.vertices.size()); - for (const auto& vert : plane.vertices) - plane.vbo.push_geometry(vert, plane.normal); - for (size_t i=1; iselection_info()->model_object(); - if (m_state != On || ! mo || mo->instances.empty()) - return false; - - if (! m_planes_valid || mo != m_old_model_object - || mo->volumes.size() != m_volumes_matrices.size()) - return true; - - // We want to recalculate when the scale changes - some planes could (dis)appear. - if (! mo->instances.front()->get_scaling_factor().isApprox(m_first_instance_scale) - || ! mo->instances.front()->get_mirror().isApprox(m_first_instance_mirror)) - return true; - - for (unsigned int i=0; i < mo->volumes.size(); ++i) - if (! mo->volumes[i]->get_matrix().isApprox(m_volumes_matrices[i]) - || mo->volumes[i]->type() != m_volumes_types[i]) - return true; - - return false; -} - -} // namespace GUI -} // namespace Slic3r +// Include GLGizmoBase.hpp before I18N.hpp as it includes some libigl code, which overrides our localization "L" macro. +#include "GLGizmoFlatten.hpp" +#include "slic3r/GUI/GLCanvas3D.hpp" +#if ENABLE_LEGACY_OPENGL_REMOVAL +#include "slic3r/GUI/GUI_App.hpp" +#endif // ENABLE_LEGACY_OPENGL_REMOVAL +#if ENABLE_GL_SHADERS_ATTRIBUTES +#include "slic3r/GUI/Plater.hpp" +#endif // ENABLE_GL_SHADERS_ATTRIBUTES + +#include "slic3r/GUI/Gizmos/GLGizmosCommon.hpp" + +#include "libslic3r/Geometry/ConvexHull.hpp" +#include "libslic3r/Model.hpp" + +#include + +#include + +namespace Slic3r { +namespace GUI { + +static const Slic3r::ColorRGBA DEFAULT_PLANE_COLOR = { 0.9f, 0.9f, 0.9f, 0.5f }; +static const Slic3r::ColorRGBA DEFAULT_HOVER_PLANE_COLOR = { 0.9f, 0.9f, 0.9f, 0.75f }; + +GLGizmoFlatten::GLGizmoFlatten(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id) + : GLGizmoBase(parent, icon_filename, sprite_id) +{} + +bool GLGizmoFlatten::on_mouse(const wxMouseEvent &mouse_event) +{ + if (mouse_event.Moving()) { + // only for sure + m_mouse_left_down = false; + return false; + } + if (mouse_event.LeftDown()) { + if (m_hover_id != -1) { + m_mouse_left_down = true; + Selection &selection = m_parent.get_selection(); + if (selection.is_single_full_instance()) { + // Rotate the object so the normal points downward: + selection.flattening_rotate(m_planes[m_hover_id].normal); + m_parent.do_rotate(L("Gizmo-Place on Face")); + } + return true; + } + + // fix: prevent restart gizmo when reselect object + // take responsibility for left up + if (m_parent.get_first_hover_volume_idx() >= 0) m_mouse_left_down = true; + + } else if (mouse_event.LeftUp()) { + if (m_mouse_left_down) { + // responsible for mouse left up after selecting plane + m_mouse_left_down = false; + return true; + } + } else if (mouse_event.Leaving()) { + m_mouse_left_down = false; + } + return false; +} + +void GLGizmoFlatten::data_changed() +{ + const Selection & selection = m_parent.get_selection(); + const ModelObject *model_object = nullptr; + if (selection.is_single_full_instance() || + selection.is_from_single_object() ) { + model_object = selection.get_model()->objects[selection.get_object_idx()]; + } + set_flattening_data(model_object); +} + +bool GLGizmoFlatten::on_init() +{ + m_shortcut_key = WXK_CONTROL_F; + return true; +} + +void GLGizmoFlatten::on_set_state() +{ +} + +CommonGizmosDataID GLGizmoFlatten::on_get_requirements() const +{ + return CommonGizmosDataID::SelectionInfo; +} + +std::string GLGizmoFlatten::on_get_name() const +{ + return _u8L("Place on face"); +} + +bool GLGizmoFlatten::on_is_activable() const +{ + // This is assumed in GLCanvas3D::do_rotate, do not change this + // without updating that function too. + return m_parent.get_selection().is_single_full_instance(); +} + +void GLGizmoFlatten::on_render() +{ + const Selection& selection = m_parent.get_selection(); + +#if ENABLE_LEGACY_OPENGL_REMOVAL + GLShaderProgram* shader = wxGetApp().get_shader("flat"); + if (shader == nullptr) + return; + + shader->start_using(); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + + glsafe(::glClear(GL_DEPTH_BUFFER_BIT)); + + glsafe(::glEnable(GL_DEPTH_TEST)); + glsafe(::glEnable(GL_BLEND)); + + if (selection.is_single_full_instance()) { + const Transform3d& m = selection.get_first_volume()->get_instance_transformation().get_matrix(); +#if ENABLE_GL_SHADERS_ATTRIBUTES + const Camera& camera = wxGetApp().plater()->get_camera(); + const Transform3d view_model_matrix = camera.get_view_matrix() * + Geometry::assemble_transform(selection.get_first_volume()->get_sla_shift_z() * Vec3d::UnitZ()) * m; + + shader->set_uniform("view_model_matrix", view_model_matrix); + shader->set_uniform("projection_matrix", camera.get_projection_matrix()); +#else + glsafe(::glPushMatrix()); + glsafe(::glTranslatef(0.f, 0.f, selection.get_first_volume()->get_sla_shift_z())); + glsafe(::glMultMatrixd(m.data())); +#endif // ENABLE_GL_SHADERS_ATTRIBUTES + if (this->is_plane_update_necessary()) + update_planes(); + for (int i = 0; i < (int)m_planes.size(); ++i) { +#if ENABLE_LEGACY_OPENGL_REMOVAL + m_planes[i].vbo.set_color(i == m_hover_id ? DEFAULT_HOVER_PLANE_COLOR : DEFAULT_PLANE_COLOR); + m_planes[i].vbo.render(); +#else + glsafe(::glColor4fv(i == m_hover_id ? DEFAULT_HOVER_PLANE_COLOR.data() : DEFAULT_PLANE_COLOR.data())); + if (m_planes[i].vbo.has_VBOs()) + m_planes[i].vbo.render(); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + } +#if !ENABLE_GL_SHADERS_ATTRIBUTES + glsafe(::glPopMatrix()); +#endif // !ENABLE_GL_SHADERS_ATTRIBUTES + } + + glsafe(::glEnable(GL_CULL_FACE)); + glsafe(::glDisable(GL_BLEND)); + +#if ENABLE_LEGACY_OPENGL_REMOVAL + shader->stop_using(); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL +} + +void GLGizmoFlatten::on_render_for_picking() +{ + const Selection& selection = m_parent.get_selection(); + +#if ENABLE_LEGACY_OPENGL_REMOVAL + GLShaderProgram* shader = wxGetApp().get_shader("flat"); + if (shader == nullptr) + return; + + shader->start_using(); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + + glsafe(::glDisable(GL_DEPTH_TEST)); + glsafe(::glDisable(GL_BLEND)); + + if (selection.is_single_full_instance() && !wxGetKeyState(WXK_CONTROL)) { + const Transform3d& m = selection.get_first_volume()->get_instance_transformation().get_matrix(); +#if ENABLE_GL_SHADERS_ATTRIBUTES + const Camera& camera = wxGetApp().plater()->get_camera(); + const Transform3d view_model_matrix = camera.get_view_matrix() * + Geometry::assemble_transform(selection.get_first_volume()->get_sla_shift_z() * Vec3d::UnitZ()) * m; + + shader->set_uniform("view_model_matrix", view_model_matrix); + shader->set_uniform("projection_matrix", camera.get_projection_matrix()); +#else + glsafe(::glPushMatrix()); + glsafe(::glTranslatef(0.f, 0.f, selection.get_first_volume()->get_sla_shift_z())); + glsafe(::glMultMatrixd(m.data())); +#endif // ENABLE_GL_SHADERS_ATTRIBUTES + if (this->is_plane_update_necessary()) + update_planes(); + for (int i = 0; i < (int)m_planes.size(); ++i) { +#if ENABLE_LEGACY_OPENGL_REMOVAL + m_planes[i].vbo.set_color(picking_color_component(i)); +#else + glsafe(::glColor4fv(picking_color_component(i).data())); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + m_planes[i].vbo.render(); + } +#if !ENABLE_GL_SHADERS_ATTRIBUTES + glsafe(::glPopMatrix()); +#endif // !ENABLE_GL_SHADERS_ATTRIBUTES + } + + glsafe(::glEnable(GL_CULL_FACE)); + +#if ENABLE_LEGACY_OPENGL_REMOVAL + shader->stop_using(); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL +} + +void GLGizmoFlatten::set_flattening_data(const ModelObject* model_object) +{ + if (model_object != m_old_model_object) { + m_planes.clear(); + m_planes_valid = false; + } +} + +void GLGizmoFlatten::update_planes() +{ + const ModelObject* mo = m_c->selection_info()->model_object(); + TriangleMesh ch; + for (const ModelVolume* vol : mo->volumes) { + if (vol->type() != ModelVolumeType::MODEL_PART) + continue; + TriangleMesh vol_ch = vol->get_convex_hull(); + vol_ch.transform(vol->get_matrix()); + ch.merge(vol_ch); + } + ch = ch.convex_hull_3d(); + m_planes.clear(); +#if ENABLE_WORLD_COORDINATE + const Transform3d inst_matrix = mo->instances.front()->get_matrix_no_offset(); +#else + const Transform3d& inst_matrix = mo->instances.front()->get_matrix(true); +#endif // ENABLE_WORLD_COORDINATE + + // Following constants are used for discarding too small polygons. + const float minimal_area = 5.f; // in square mm (world coordinates) + const float minimal_side = 1.f; // mm + + // Now we'll go through all the facets and append Points of facets sharing the same normal. + // This part is still performed in mesh coordinate system. + const int num_of_facets = ch.facets_count(); + const std::vector face_normals = its_face_normals(ch.its); + const std::vector face_neighbors = its_face_neighbors(ch.its); + std::vector facet_queue(num_of_facets, 0); + std::vector facet_visited(num_of_facets, false); + int facet_queue_cnt = 0; + const stl_normal* normal_ptr = nullptr; + int facet_idx = 0; + while (1) { + // Find next unvisited triangle: + for (; facet_idx < num_of_facets; ++ facet_idx) + if (!facet_visited[facet_idx]) { + facet_queue[facet_queue_cnt ++] = facet_idx; + facet_visited[facet_idx] = true; + normal_ptr = &face_normals[facet_idx]; + m_planes.emplace_back(); + break; + } + if (facet_idx == num_of_facets) + break; // Everything was visited already + + while (facet_queue_cnt > 0) { + int facet_idx = facet_queue[-- facet_queue_cnt]; + const stl_normal& this_normal = face_normals[facet_idx]; + if (std::abs(this_normal(0) - (*normal_ptr)(0)) < 0.001 && std::abs(this_normal(1) - (*normal_ptr)(1)) < 0.001 && std::abs(this_normal(2) - (*normal_ptr)(2)) < 0.001) { + const Vec3i face = ch.its.indices[facet_idx]; + for (int j=0; j<3; ++j) + m_planes.back().vertices.emplace_back(ch.its.vertices[face[j]].cast()); + + facet_visited[facet_idx] = true; + for (int j = 0; j < 3; ++ j) + if (int neighbor_idx = face_neighbors[facet_idx][j]; neighbor_idx >= 0 && ! facet_visited[neighbor_idx]) + facet_queue[facet_queue_cnt ++] = neighbor_idx; + } + } + m_planes.back().normal = normal_ptr->cast(); + + Pointf3s& verts = m_planes.back().vertices; + // Now we'll transform all the points into world coordinates, so that the areas, angles and distances + // make real sense. + verts = transform(verts, inst_matrix); + + // if this is a just a very small triangle, remove it to speed up further calculations (it would be rejected later anyway): + if (verts.size() == 3 && + ((verts[0] - verts[1]).norm() < minimal_side + || (verts[0] - verts[2]).norm() < minimal_side + || (verts[1] - verts[2]).norm() < minimal_side)) + m_planes.pop_back(); + } + + // Let's prepare transformation of the normal vector from mesh to instance coordinates. + Geometry::Transformation t(inst_matrix); + Vec3d scaling = t.get_scaling_factor(); + t.set_scaling_factor(Vec3d(1./scaling(0), 1./scaling(1), 1./scaling(2))); + + // Now we'll go through all the polygons, transform the points into xy plane to process them: + for (unsigned int polygon_id=0; polygon_id < m_planes.size(); ++polygon_id) { + Pointf3s& polygon = m_planes[polygon_id].vertices; + const Vec3d& normal = m_planes[polygon_id].normal; + + // transform the normal according to the instance matrix: + Vec3d normal_transformed = t.get_matrix() * normal; + + // We are going to rotate about z and y to flatten the plane + Eigen::Quaterniond q; + Transform3d m = Transform3d::Identity(); + m.matrix().block(0, 0, 3, 3) = q.setFromTwoVectors(normal_transformed, Vec3d::UnitZ()).toRotationMatrix(); + polygon = transform(polygon, m); + + // Now to remove the inner points. We'll misuse Geometry::convex_hull for that, but since + // it works in fixed point representation, we will rescale the polygon to avoid overflows. + // And yes, it is a nasty thing to do. Whoever has time is free to refactor. + Vec3d bb_size = BoundingBoxf3(polygon).size(); + float sf = std::min(1./bb_size(0), 1./bb_size(1)); + Transform3d tr = Geometry::assemble_transform(Vec3d::Zero(), Vec3d::Zero(), Vec3d(sf, sf, 1.f)); + polygon = transform(polygon, tr); + polygon = Slic3r::Geometry::convex_hull(polygon); + polygon = transform(polygon, tr.inverse()); + + // Calculate area of the polygons and discard ones that are too small + float& area = m_planes[polygon_id].area; + area = 0.f; + for (unsigned int i = 0; i < polygon.size(); i++) // Shoelace formula + area += polygon[i](0)*polygon[i + 1 < polygon.size() ? i + 1 : 0](1) - polygon[i + 1 < polygon.size() ? i + 1 : 0](0)*polygon[i](1); + area = 0.5f * std::abs(area); + + bool discard = false; + if (area < minimal_area) + discard = true; + else { + // We also check the inner angles and discard polygons with angles smaller than the following threshold + const double angle_threshold = ::cos(10.0 * (double)PI / 180.0); + + for (unsigned int i = 0; i < polygon.size(); ++i) { + const Vec3d& prec = polygon[(i == 0) ? polygon.size() - 1 : i - 1]; + const Vec3d& curr = polygon[i]; + const Vec3d& next = polygon[(i == polygon.size() - 1) ? 0 : i + 1]; + + if ((prec - curr).normalized().dot((next - curr).normalized()) > angle_threshold) { + discard = true; + break; + } + } + } + + if (discard) { + m_planes[polygon_id--] = std::move(m_planes.back()); + m_planes.pop_back(); + continue; + } + + // We will shrink the polygon a little bit so it does not touch the object edges: + Vec3d centroid = std::accumulate(polygon.begin(), polygon.end(), Vec3d(0.0, 0.0, 0.0)); + centroid /= (double)polygon.size(); + for (auto& vertex : polygon) + vertex = 0.9f*vertex + 0.1f*centroid; + + // Polygon is now simple and convex, we'll round the corners to make them look nicer. + // The algorithm takes a vertex, calculates middles of respective sides and moves the vertex + // towards their average (controlled by 'aggressivity'). This is repeated k times. + // In next iterations, the neighbours are not always taken at the middle (to increase the + // rounding effect at the corners, where we need it most). + const unsigned int k = 10; // number of iterations + const float aggressivity = 0.2f; // agressivity + const unsigned int N = polygon.size(); + std::vector> neighbours; + if (k != 0) { + Pointf3s points_out(2*k*N); // vector long enough to store the future vertices + for (unsigned int j=0; jvolumes) { + m_volumes_matrices.push_back(vol->get_matrix()); + m_volumes_types.push_back(vol->type()); + } + m_first_instance_scale = mo->instances.front()->get_scaling_factor(); + m_first_instance_mirror = mo->instances.front()->get_mirror(); + m_old_model_object = mo; + + // And finally create respective VBOs. The polygon is convex with + // the vertices in order, so triangulation is trivial. + for (auto& plane : m_planes) { +#if ENABLE_LEGACY_OPENGL_REMOVAL + GLModel::Geometry init_data; + init_data.format = { GLModel::Geometry::EPrimitiveType::TriangleFan, GLModel::Geometry::EVertexLayout::P3N3 }; + init_data.reserve_vertices(plane.vertices.size()); + init_data.reserve_indices(plane.vertices.size()); + // vertices + indices + for (size_t i = 0; i < plane.vertices.size(); ++i) { + init_data.add_vertex((Vec3f)plane.vertices[i].cast(), (Vec3f)plane.normal.cast()); + init_data.add_index((unsigned int)i); + } + plane.vbo.init_from(std::move(init_data)); +#else + plane.vbo.reserve(plane.vertices.size()); + for (const auto& vert : plane.vertices) + plane.vbo.push_geometry(vert, plane.normal); + for (size_t i=1; iselection_info()->model_object(); + if (m_state != On || ! mo || mo->instances.empty()) + return false; + + if (! m_planes_valid || mo != m_old_model_object + || mo->volumes.size() != m_volumes_matrices.size()) + return true; + + // We want to recalculate when the scale changes - some planes could (dis)appear. + if (! mo->instances.front()->get_scaling_factor().isApprox(m_first_instance_scale) + || ! mo->instances.front()->get_mirror().isApprox(m_first_instance_mirror)) + return true; + + for (unsigned int i=0; i < mo->volumes.size(); ++i) + if (! mo->volumes[i]->get_matrix().isApprox(m_volumes_matrices[i]) + || mo->volumes[i]->type() != m_volumes_types[i]) + return true; + + return false; +} + +} // namespace GUI +} // namespace Slic3r diff --git a/src/slic3r/GUI/Gizmos/GLGizmoHollow.cpp b/src/slic3r/GUI/Gizmos/GLGizmoHollow.cpp index 27dccefe2..3b3c32b74 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoHollow.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoHollow.cpp @@ -94,7 +94,7 @@ void GLGizmoHollow::on_render_for_picking() { 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(); +// m_z_shift = selection.get_first_volume()->get_sla_shift_z(); //#endif glsafe(::glEnable(GL_DEPTH_TEST)); @@ -117,12 +117,16 @@ void GLGizmoHollow::render_points(const Selection& selection, bool picking) ScopeGuard guard([shader]() { if (shader) shader->stop_using(); }); #endif // ENABLE_LEGACY_OPENGL_REMOVAL - const GLVolume* vol = selection.get_volume(*selection.get_volume_idxs().begin()); + const GLVolume* vol = selection.get_first_volume(); Geometry::Transformation trafo = vol->get_instance_transformation() * vol->get_volume_transformation(); #if ENABLE_GL_SHADERS_ATTRIBUTES +#if ENABLE_WORLD_COORDINATE + const Transform3d instance_scaling_matrix_inverse = vol->get_instance_transformation().get_scaling_factor_matrix().inverse(); +#else const Transform3d instance_scaling_matrix_inverse = vol->get_instance_transformation().get_matrix(true, true, false, true).inverse(); - const Transform3d instance_matrix = Geometry::assemble_transform(m_c->selection_info()->get_sla_shift() * Vec3d::UnitZ()) * trafo.get_matrix(); +#endif // ENABLE_WORLD_COORDINATE + const Transform3d instance_matrix = Geometry::translation_transform(m_c->selection_info()->get_sla_shift() * Vec3d::UnitZ()) * trafo.get_matrix(); const Camera& camera = wxGetApp().plater()->get_camera(); const Transform3d& view_matrix = camera.get_view_matrix(); @@ -238,7 +242,7 @@ bool GLGizmoHollow::unproject_on_mesh(const Vec2d& mouse_pos, std::pairget_camera(); const Selection& selection = m_parent.get_selection(); - const GLVolume* volume = selection.get_volume(*selection.get_volume_idxs().begin()); + const GLVolume* volume = selection.get_first_volume(); Geometry::Transformation trafo = volume->get_instance_transformation() * volume->get_volume_transformation(); trafo.set_offset(trafo.get_offset() + Vec3d(0., 0., m_c->selection_info()->get_sla_shift())); diff --git a/src/slic3r/GUI/Gizmos/GLGizmoMove.cpp b/src/slic3r/GUI/Gizmos/GLGizmoMove.cpp index a82de7241..aff55fd3f 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoMove.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoMove.cpp @@ -2,6 +2,9 @@ #include "GLGizmoMove.hpp" #include "slic3r/GUI/GLCanvas3D.hpp" #include "slic3r/GUI/GUI_App.hpp" +#if ENABLE_WORLD_COORDINATE +#include "slic3r/GUI/GUI_ObjectManipulation.hpp" +#endif // ENABLE_WORLD_COORDINATE #if ENABLE_GL_SHADERS_ATTRIBUTES #include "slic3r/GUI/Plater.hpp" #endif // ENABLE_GL_SHADERS_ATTRIBUTES @@ -22,18 +25,29 @@ GLGizmoMove3D::GLGizmoMove3D(GLCanvas3D& parent, const std::string& icon_filenam std::string GLGizmoMove3D::get_tooltip() const { +#if ENABLE_WORLD_COORDINATE + if (m_hover_id == 0) + return "X: " + format(m_displacement.x(), 2); + else if (m_hover_id == 1) + return "Y: " + format(m_displacement.y(), 2); + else if (m_hover_id == 2) + return "Z: " + format(m_displacement.z(), 2); + else + return ""; +#else const Selection& selection = m_parent.get_selection(); - bool show_position = selection.is_single_full_instance(); + const bool show_position = selection.is_single_full_instance(); const Vec3d& position = selection.get_bounding_box().center(); if (m_hover_id == 0 || m_grabbers[0].dragging) - return "X: " + format(show_position ? position(0) : m_displacement(0), 2); + return "X: " + format(show_position ? position.x() : m_displacement.x(), 2); else if (m_hover_id == 1 || m_grabbers[1].dragging) - return "Y: " + format(show_position ? position(1) : m_displacement(1), 2); + return "Y: " + format(show_position ? position.y() : m_displacement.y(), 2); else if (m_hover_id == 2 || m_grabbers[2].dragging) - return "Z: " + format(show_position ? position(2) : m_displacement(2), 2); + return "Z: " + format(show_position ? position.z() : m_displacement.z(), 2); else return ""; +#endif // ENABLE_WORLD_COORDINATE } bool GLGizmoMove3D::on_mouse(const wxMouseEvent &mouse_event) { @@ -41,9 +55,7 @@ bool GLGizmoMove3D::on_mouse(const wxMouseEvent &mouse_event) { } void GLGizmoMove3D::data_changed() { - const Selection &selection = m_parent.get_selection(); - bool is_wipe_tower = selection.is_wipe_tower(); - m_grabbers[2].enabled = !is_wipe_tower; + m_grabbers[2].enabled = !m_parent.get_selection().is_wipe_tower(); } bool GLGizmoMove3D::on_init() @@ -86,11 +98,29 @@ void GLGizmoMove3D::on_start_dragging() assert(m_hover_id != -1); m_displacement = Vec3d::Zero(); +#if ENABLE_WORLD_COORDINATE + const Selection& selection = m_parent.get_selection(); + const ECoordinatesType coordinates_type = wxGetApp().obj_manipul()->get_coordinates_type(); + if (coordinates_type == ECoordinatesType::World) + m_starting_drag_position = m_center + m_grabbers[m_hover_id].center; + else if (coordinates_type == ECoordinatesType::Local && selection.is_single_volume_or_modifier()) { + const GLVolume& v = *selection.get_first_volume(); + m_starting_drag_position = m_center + v.get_instance_transformation().get_rotation_matrix() * v.get_volume_transformation().get_rotation_matrix() * m_grabbers[m_hover_id].center; + } + else { + const GLVolume& v = *selection.get_first_volume(); + m_starting_drag_position = m_center + v.get_instance_transformation().get_rotation_matrix() * m_grabbers[m_hover_id].center; + } + m_starting_box_center = m_center; + m_starting_box_bottom_center = m_center; + m_starting_box_bottom_center.z() = m_bounding_box.min.z(); +#else const BoundingBoxf3& box = m_parent.get_selection().get_bounding_box(); m_starting_drag_position = m_grabbers[m_hover_id].center; m_starting_box_center = box.center(); m_starting_box_bottom_center = box.center(); - m_starting_box_bottom_center(2) = box.min(2); + m_starting_box_bottom_center.z() = box.min.z(); +#endif // ENABLE_WORLD_COORDINATE } void GLGizmoMove3D::on_stop_dragging() @@ -109,7 +139,19 @@ void GLGizmoMove3D::on_dragging(const UpdateData& data) m_displacement.z() = calc_projection(data); Selection &selection = m_parent.get_selection(); +#if ENABLE_WORLD_COORDINATE + TransformationType trafo_type; + trafo_type.set_relative(); + switch (wxGetApp().obj_manipul()->get_coordinates_type()) + { + case ECoordinatesType::Instance: { trafo_type.set_instance(); break; } + case ECoordinatesType::Local: { trafo_type.set_local(); break; } + default: { break; } + } + selection.translate(m_displacement, trafo_type); +#else selection.translate(m_displacement); +#endif // ENABLE_WORLD_COORDINATE } void GLGizmoMove3D::on_render() @@ -119,11 +161,39 @@ void GLGizmoMove3D::on_render() m_cone.init_from(its_make_cone(1.0, 1.0, double(PI) / 18.0)); #endif // !ENABLE_GIZMO_GRABBER_REFACTOR - const Selection& selection = m_parent.get_selection(); - glsafe(::glClear(GL_DEPTH_BUFFER_BIT)); glsafe(::glEnable(GL_DEPTH_TEST)); +#if ENABLE_WORLD_COORDINATE +#if !ENABLE_GL_SHADERS_ATTRIBUTES + glsafe(::glPushMatrix()); +#endif // !ENABLE_GL_SHADERS_ATTRIBUTES + calc_selection_box_and_center(); +#if ENABLE_GL_SHADERS_ATTRIBUTES + const Transform3d base_matrix = local_transform(m_parent.get_selection()); + for (int i = 0; i < 3; ++i) { + m_grabbers[i].matrix = base_matrix; + } +#else + transform_to_local(m_parent.get_selection()); +#endif // ENABLE_GL_SHADERS_ATTRIBUTES + + const Vec3d zero = Vec3d::Zero(); + const Vec3d half_box_size = 0.5 * m_bounding_box.size(); + + // x axis + m_grabbers[0].center = { half_box_size.x() + Offset, 0.0, 0.0 }; + m_grabbers[0].color = AXES_COLOR[0]; + + // y axis + m_grabbers[1].center = { 0.0, half_box_size.y() + Offset, 0.0 }; + m_grabbers[1].color = AXES_COLOR[1]; + + // z axis + m_grabbers[2].center = { 0.0, 0.0, half_box_size.z() + Offset }; + m_grabbers[2].color = AXES_COLOR[2]; +#else + const Selection& selection = m_parent.get_selection(); const BoundingBoxf3& box = selection.get_bounding_box(); const Vec3d& center = box.center(); @@ -138,14 +208,24 @@ void GLGizmoMove3D::on_render() // z axis m_grabbers[2].center = { center.x(), center.y(), box.max.z() + Offset }; m_grabbers[2].color = AXES_COLOR[2]; +#endif // ENABLE_WORLD_COORDINATE glsafe(::glLineWidth((m_hover_id != -1) ? 2.0f : 1.5f)); #if ENABLE_LEGACY_OPENGL_REMOVAL +#if ENABLE_WORLD_COORDINATE + auto render_grabber_connection = [this, &zero](unsigned int id) { +#else auto render_grabber_connection = [this, ¢er](unsigned int id) { +#endif // ENABLE_WORLD_COORDINATE if (m_grabbers[id].enabled) { +#if ENABLE_WORLD_COORDINATE + if (!m_grabber_connections[id].model.is_initialized() || !m_grabber_connections[id].old_center.isApprox(m_grabbers[id].center)) { + m_grabber_connections[id].old_center = m_grabbers[id].center; +#else if (!m_grabber_connections[id].model.is_initialized() || !m_grabber_connections[id].old_center.isApprox(center)) { m_grabber_connections[id].old_center = center; +#endif // ENABLE_WORLD_COORDINATE m_grabber_connections[id].model.reset(); GLModel::Geometry init_data; @@ -155,7 +235,11 @@ void GLGizmoMove3D::on_render() init_data.reserve_indices(2); // vertices +#if ENABLE_WORLD_COORDINATE + init_data.add_vertex((Vec3f)zero.cast()); +#else init_data.add_vertex((Vec3f)center.cast()); +#endif // ENABLE_WORLD_COORDINATE init_data.add_vertex((Vec3f)m_grabbers[id].center.cast()); // indices @@ -178,7 +262,11 @@ void GLGizmoMove3D::on_render() #if ENABLE_GL_SHADERS_ATTRIBUTES const Camera& camera = wxGetApp().plater()->get_camera(); +#if ENABLE_WORLD_COORDINATE + shader->set_uniform("view_model_matrix", camera.get_view_matrix() * base_matrix); +#else shader->set_uniform("view_model_matrix", camera.get_view_matrix()); +#endif // ENABLE_WORLD_COORDINATE shader->set_uniform("projection_matrix", camera.get_projection_matrix()); #endif // ENABLE_GL_SHADERS_ATTRIBUTES @@ -190,7 +278,11 @@ void GLGizmoMove3D::on_render() if (m_grabbers[i].enabled) { glsafe(::glColor4fv(AXES_COLOR[i].data())); ::glBegin(GL_LINES); +#if ENABLE_WORLD_COORDINATE + ::glVertex3dv(zero.data()); +#else ::glVertex3dv(center.data()); +#endif // ENABLE_WORLD_COORDINATE ::glVertex3dv(m_grabbers[i].center.data()); glsafe(::glEnd()); } @@ -203,6 +295,19 @@ void GLGizmoMove3D::on_render() #endif // ENABLE_LEGACY_OPENGL_REMOVAL // draw grabbers +#if ENABLE_WORLD_COORDINATE + render_grabbers(m_bounding_box); +#if !ENABLE_GIZMO_GRABBER_REFACTOR + for (unsigned int i = 0; i < 3; ++i) { + if (m_grabbers[i].enabled) +#if ENABLE_GL_SHADERS_ATTRIBUTES + render_grabber_extension((Axis)i, base_matrix, m_bounding_box, false); +#else + render_grabber_extension((Axis)i, m_bounding_box, false); +#endif // ENABLE_GL_SHADERS_ATTRIBUTES + } +#endif // !ENABLE_GIZMO_GRABBER_REFACTOR +#else render_grabbers(box); #if !ENABLE_GIZMO_GRABBER_REFACTOR for (unsigned int i = 0; i < 3; ++i) { @@ -210,6 +315,7 @@ void GLGizmoMove3D::on_render() render_grabber_extension((Axis)i, box, false); } #endif // !ENABLE_GIZMO_GRABBER_REFACTOR +#endif // ENABLE_WORLD_COORDINATE } else { // draw axis @@ -220,7 +326,11 @@ void GLGizmoMove3D::on_render() #if ENABLE_GL_SHADERS_ATTRIBUTES const Camera& camera = wxGetApp().plater()->get_camera(); +#if ENABLE_WORLD_COORDINATE + shader->set_uniform("view_model_matrix", camera.get_view_matrix()* base_matrix); +#else shader->set_uniform("view_model_matrix", camera.get_view_matrix()); +#endif // ENABLE_WORLD_COORDINATE shader->set_uniform("projection_matrix", camera.get_projection_matrix()); #endif // ENABLE_GL_SHADERS_ATTRIBUTES @@ -232,7 +342,11 @@ void GLGizmoMove3D::on_render() #else glsafe(::glColor4fv(AXES_COLOR[m_hover_id].data())); ::glBegin(GL_LINES); +#if ENABLE_WORLD_COORDINATE + ::glVertex3dv(zero.data()); +#else ::glVertex3dv(center.data()); +#endif // ENABLE_WORLD_COORDINATE ::glVertex3dv(m_grabbers[m_hover_id].center.data()); glsafe(::glEnd()); @@ -242,20 +356,65 @@ void GLGizmoMove3D::on_render() shader->start_using(); shader->set_uniform("emission_factor", 0.1f); // draw grabber - const float mean_size = (float)((box.size().x() + box.size().y() + box.size().z()) / 3.0); +#if ENABLE_WORLD_COORDINATE + const Vec3d box_size = m_bounding_box.size(); +#else + const Vec3d box_size = box.size(); +#endif // ENABLE_WORLD_COORDINATE + const float mean_size = (float)((box_size.x() + box_size.y() + box_size.z()) / 3.0); m_grabbers[m_hover_id].render(true, mean_size); shader->stop_using(); } #if !ENABLE_GIZMO_GRABBER_REFACTOR +#if ENABLE_WORLD_COORDINATE +#if ENABLE_GL_SHADERS_ATTRIBUTES + render_grabber_extension((Axis)m_hover_id, base_matrix, m_bounding_box, false); +#else + render_grabber_extension((Axis)m_hover_id, m_bounding_box, false); +#endif // ENABLE_GL_SHADERS_ATTRIBUTES +#else render_grabber_extension((Axis)m_hover_id, box, false); +#endif // ENABLE_WORLD_COORDINATE #endif // !ENABLE_GIZMO_GRABBER_REFACTOR } + +#if ENABLE_WORLD_COORDINATE +#if !ENABLE_GL_SHADERS_ATTRIBUTES + glsafe(::glPopMatrix()); +#endif // !ENABLE_GL_SHADERS_ATTRIBUTES +#endif // ENABLE_WORLD_COORDINATE } void GLGizmoMove3D::on_render_for_picking() { glsafe(::glDisable(GL_DEPTH_TEST)); +#if ENABLE_WORLD_COORDINATE +#if ENABLE_GL_SHADERS_ATTRIBUTES + const Transform3d base_matrix = local_transform(m_parent.get_selection()); + for (int i = 0; i < 3; ++i) { + m_grabbers[i].matrix = base_matrix; + } +#else + glsafe(::glPushMatrix()); + transform_to_local(m_parent.get_selection()); +#endif // ENABLE_GL_SHADERS_ATTRIBUTES + render_grabbers_for_picking(m_bounding_box); +#if ENABLE_GL_SHADERS_ATTRIBUTES +#if !ENABLE_GIZMO_GRABBER_REFACTOR + render_grabber_extension(X, base_matrix, m_bounding_box, true); + render_grabber_extension(Y, base_matrix, m_bounding_box, true); + render_grabber_extension(Z, base_matrix, m_bounding_box, true); +#endif // !ENABLE_GIZMO_GRABBER_REFACTOR +#else +#if !ENABLE_GIZMO_GRABBER_REFACTOR + render_grabber_extension(X, m_bounding_box, true); + render_grabber_extension(Y, m_bounding_box, true); + render_grabber_extension(Z, m_bounding_box, true); +#endif // !ENABLE_GIZMO_GRABBER_REFACTOR + glsafe(::glPopMatrix()); +#endif // ENABLE_GL_SHADERS_ATTRIBUTES +#else const BoundingBoxf3& box = m_parent.get_selection().get_bounding_box(); render_grabbers_for_picking(box); #if !ENABLE_GIZMO_GRABBER_REFACTOR @@ -263,23 +422,24 @@ void GLGizmoMove3D::on_render_for_picking() render_grabber_extension(Y, box, true); render_grabber_extension(Z, box, true); #endif // !ENABLE_GIZMO_GRABBER_REFACTOR +#endif // ENABLE_WORLD_COORDINATE } double GLGizmoMove3D::calc_projection(const UpdateData& data) const { double projection = 0.0; - Vec3d starting_vec = m_starting_drag_position - m_starting_box_center; - double len_starting_vec = starting_vec.norm(); + const Vec3d starting_vec = m_starting_drag_position - m_starting_box_center; + const double len_starting_vec = starting_vec.norm(); if (len_starting_vec != 0.0) { - Vec3d mouse_dir = data.mouse_ray.unit_vector(); + const Vec3d mouse_dir = data.mouse_ray.unit_vector(); // finds the intersection of the mouse ray with the plane parallel to the camera viewport and passing throught the starting position // use ray-plane intersection see i.e. https://en.wikipedia.org/wiki/Line%E2%80%93plane_intersection algebric form // in our case plane normal and ray direction are the same (orthogonal view) // when moving to perspective camera the negative z unit axis of the camera needs to be transformed in world space and used as plane normal - Vec3d inters = data.mouse_ray.a + (m_starting_drag_position - data.mouse_ray.a).dot(mouse_dir) / mouse_dir.squaredNorm() * mouse_dir; + const Vec3d inters = data.mouse_ray.a + (m_starting_drag_position - data.mouse_ray.a).dot(mouse_dir) / mouse_dir.squaredNorm() * mouse_dir; // vector from the starting position to the found intersection - Vec3d inters_vec = inters - m_starting_drag_position; + const Vec3d inters_vec = inters - m_starting_drag_position; // finds projection of the vector along the staring direction projection = inters_vec.dot(starting_vec.normalized()); @@ -292,9 +452,14 @@ double GLGizmoMove3D::calc_projection(const UpdateData& data) const } #if !ENABLE_GIZMO_GRABBER_REFACTOR +#if ENABLE_WORLD_COORDINATE && ENABLE_GL_SHADERS_ATTRIBUTES +void GLGizmoMove3D::render_grabber_extension(Axis axis, const Transform3d& base_matrix, const BoundingBoxf3& box, bool picking) +#else void GLGizmoMove3D::render_grabber_extension(Axis axis, const BoundingBoxf3& box, bool picking) +#endif // ENABLE_WORLD_COORDINATE && ENABLE_GL_SHADERS_ATTRIBUTES { - const float mean_size = float((box.size().x() + box.size().y() + box.size().z()) / 3.0); + const Vec3d box_size = box.size(); + const float mean_size = float((box_size.x() + box_size.y() + box_size.z()) / 3.0); const double size = m_dragging ? double(m_grabbers[axis].get_dragging_half_size(mean_size)) : double(m_grabbers[axis].get_half_size(mean_size)); #if ENABLE_LEGACY_OPENGL_REMOVAL @@ -319,7 +484,7 @@ void GLGizmoMove3D::render_grabber_extension(Axis axis, const BoundingBoxf3& box #if ENABLE_GL_SHADERS_ATTRIBUTES const Camera& camera = wxGetApp().plater()->get_camera(); - Transform3d view_model_matrix = camera.get_view_matrix() * Geometry::assemble_transform(m_grabbers[axis].center); + Transform3d view_model_matrix = camera.get_view_matrix() * base_matrix * Geometry::assemble_transform(m_grabbers[axis].center); if (axis == X) view_model_matrix = view_model_matrix * Geometry::assemble_transform(Vec3d::Zero(), 0.5 * PI * Vec3d::UnitY()); else if (axis == Y) @@ -352,5 +517,62 @@ void GLGizmoMove3D::render_grabber_extension(Axis axis, const BoundingBoxf3& box } #endif // !ENABLE_GIZMO_GRABBER_REFACTOR +#if ENABLE_WORLD_COORDINATE +#if ENABLE_GL_SHADERS_ATTRIBUTES +Transform3d GLGizmoMove3D::local_transform(const Selection& selection) const +{ + Transform3d ret = Geometry::assemble_transform(m_center); + if (!wxGetApp().obj_manipul()->is_world_coordinates()) { + const GLVolume& v = *selection.get_first_volume(); + Transform3d orient_matrix = v.get_instance_transformation().get_rotation_matrix(); + if (selection.is_single_volume_or_modifier() && wxGetApp().obj_manipul()->is_local_coordinates()) + orient_matrix = orient_matrix * v.get_volume_transformation().get_rotation_matrix(); + ret = ret * orient_matrix; + } + return ret; +} +#else +void GLGizmoMove3D::transform_to_local(const Selection& selection) const +{ + glsafe(::glTranslated(m_center.x(), m_center.y(), m_center.z())); + + if (!wxGetApp().obj_manipul()->is_world_coordinates()) { + const GLVolume& v = *selection.get_first_volume(); + Transform3d orient_matrix = v.get_instance_transformation().get_matrix(true, false, true, true); + if (selection.is_single_volume_or_modifier() && wxGetApp().obj_manipul()->is_local_coordinates()) + orient_matrix = orient_matrix * v.get_volume_transformation().get_matrix(true, false, true, true); + glsafe(::glMultMatrixd(orient_matrix.data())); + } +} +#endif // ENABLE_GL_SHADERS_ATTRIBUTES + +void GLGizmoMove3D::calc_selection_box_and_center() +{ + const Selection& selection = m_parent.get_selection(); + const ECoordinatesType coordinates_type = wxGetApp().obj_manipul()->get_coordinates_type(); + if (coordinates_type == ECoordinatesType::World) { + m_bounding_box = selection.get_bounding_box(); + m_center = m_bounding_box.center(); + } + else if (coordinates_type == ECoordinatesType::Local && selection.is_single_volume_or_modifier()) { + const GLVolume& v = *selection.get_first_volume(); + m_bounding_box = v.transformed_convex_hull_bounding_box( + v.get_instance_transformation().get_scaling_factor_matrix() * v.get_volume_transformation().get_scaling_factor_matrix()); + m_center = v.world_matrix() * m_bounding_box.center(); + } + else { + m_bounding_box.reset(); + const Selection::IndicesList& ids = selection.get_volume_idxs(); + for (unsigned int id : ids) { + const GLVolume& v = *selection.get_volume(id); + m_bounding_box.merge(v.transformed_convex_hull_bounding_box(v.get_volume_transformation().get_matrix())); + } + const Geometry::Transformation inst_trafo = selection.get_first_volume()->get_instance_transformation(); + m_bounding_box = m_bounding_box.transformed(inst_trafo.get_scaling_factor_matrix()); + m_center = inst_trafo.get_matrix_no_scaling_factor() * m_bounding_box.center(); + } +} +#endif // ENABLE_WORLD_COORDINATE + } // namespace GUI } // namespace Slic3r diff --git a/src/slic3r/GUI/Gizmos/GLGizmoMove.hpp b/src/slic3r/GUI/Gizmos/GLGizmoMove.hpp index 6a618c3e4..dc5618cc4 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoMove.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoMove.hpp @@ -7,11 +7,19 @@ namespace Slic3r { namespace GUI { +#if ENABLE_WORLD_COORDINATE +class Selection; +#endif // ENABLE_WORLD_COORDINATE + class GLGizmoMove3D : public GLGizmoBase { static const double Offset; Vec3d m_displacement{ Vec3d::Zero() }; +#if ENABLE_WORLD_COORDINATE + Vec3d m_center{ Vec3d::Zero() }; + BoundingBoxf3 m_bounding_box; +#endif // ENABLE_WORLD_COORDINATE double m_snap_step{ 1.0 }; Vec3d m_starting_drag_position{ Vec3d::Zero() }; Vec3d m_starting_box_center{ Vec3d::Zero() }; @@ -49,7 +57,6 @@ public: /// Detect reduction of move for wipetover on selection change /// void data_changed() override; - protected: bool on_init() override; std::string on_get_name() const override; @@ -62,11 +69,25 @@ protected: private: double calc_projection(const UpdateData& data) const; +#if ENABLE_WORLD_COORDINATE +#if ENABLE_GL_SHADERS_ATTRIBUTES + Transform3d local_transform(const Selection& selection) const; +#else + void transform_to_local(const Selection& selection) const; +#endif // ENABLE_GL_SHADERS_ATTRIBUTES + void calc_selection_box_and_center(); +#endif // ENABLE_WORLD_COORDINATE #if !ENABLE_GIZMO_GRABBER_REFACTOR +#if ENABLE_WORLD_COORDINATE && ENABLE_GL_SHADERS_ATTRIBUTES + void render_grabber_extension(Axis axis, const Transform3d& base_matrix, const BoundingBoxf3& box, bool picking); +#else void render_grabber_extension(Axis axis, const BoundingBoxf3& box, bool picking); +#endif // ENABLE_WORLD_COORDINATE && ENABLE_GL_SHADERS_ATTRIBUTES #endif // !ENABLE_GIZMO_GRABBER_REFACTOR }; + + } // namespace GUI } // namespace Slic3r diff --git a/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp index b8e255ca1..c9c55cca3 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp @@ -291,7 +291,11 @@ void GLGizmoPainterBase::render_cursor_sphere(const Transform3d& trafo) const return; #endif // ENABLE_LEGACY_OPENGL_REMOVAL +#if ENABLE_WORLD_COORDINATE + const Transform3d complete_scaling_matrix_inverse = Geometry::Transformation(trafo).get_scaling_factor_matrix().inverse(); +#else const Transform3d complete_scaling_matrix_inverse = Geometry::Transformation(trafo).get_matrix(true, true, false, true).inverse(); +#endif // ENABLE_WORLD_COORDINATE const bool is_left_handed = Geometry::Transformation(trafo).is_left_handed(); #if !ENABLE_GL_SHADERS_ATTRIBUTES @@ -508,7 +512,11 @@ bool GLGizmoPainterBase::gizmo_event(SLAGizmoEventType action, const Vec2d& mous const Selection &selection = m_parent.get_selection(); const ModelObject *mo = m_c->selection_info()->model_object(); const ModelInstance *mi = mo->instances[selection.get_instance_idx()]; +#if ENABLE_WORLD_COORDINATE + const Transform3d trafo_matrix_not_translate = mi->get_transformation().get_matrix_no_offset() * mo->volumes[m_rr.mesh_id]->get_matrix_no_offset(); +#else const Transform3d trafo_matrix_not_translate = mi->get_transformation().get_matrix(true) * mo->volumes[m_rr.mesh_id]->get_matrix(true); +#endif // ENABLE_WORLD_COORDINATE const Transform3d trafo_matrix = mi->get_transformation().get_matrix() * mo->volumes[m_rr.mesh_id]->get_matrix(); m_triangle_selectors[m_rr.mesh_id]->seed_fill_select_triangles(m_rr.hit, int(m_rr.facet), trafo_matrix_not_translate, this->get_clipping_plane_in_volume_coordinates(trafo_matrix), m_smart_fill_angle, m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f, true); @@ -547,7 +555,11 @@ bool GLGizmoPainterBase::gizmo_event(SLAGizmoEventType action, const Vec2d& mous const ModelObject *mo = m_c->selection_info()->model_object(); const ModelInstance *mi = mo->instances[selection.get_instance_idx()]; const Transform3d instance_trafo = mi->get_transformation().get_matrix(); +#if ENABLE_WORLD_COORDINATE + const Transform3d instance_trafo_not_translate = mi->get_transformation().get_matrix_no_offset(); +#else const Transform3d instance_trafo_not_translate = mi->get_transformation().get_matrix(true); +#endif // ENABLE_WORLD_COORDINATE // Precalculate transformations of individual meshes. std::vector trafo_matrices; @@ -555,7 +567,11 @@ bool GLGizmoPainterBase::gizmo_event(SLAGizmoEventType action, const Vec2d& mous for (const ModelVolume *mv : mo->volumes) if (mv->is_model_part()) { trafo_matrices.emplace_back(instance_trafo * mv->get_matrix()); +#if ENABLE_WORLD_COORDINATE + trafo_matrices_not_translate.emplace_back(instance_trafo_not_translate * mv->get_matrix_no_offset()); +#else trafo_matrices_not_translate.emplace_back(instance_trafo_not_translate * mv->get_matrix(true)); +#endif // ENABLE_WORLD_COORDINATE } std::vector> projected_mouse_positions_by_mesh = get_projected_mouse_positions(mouse_position, 1., trafo_matrices); @@ -637,7 +653,11 @@ bool GLGizmoPainterBase::gizmo_event(SLAGizmoEventType action, const Vec2d& mous const ModelObject *mo = m_c->selection_info()->model_object(); const ModelInstance *mi = mo->instances[selection.get_instance_idx()]; const Transform3d instance_trafo = mi->get_transformation().get_matrix(); +#if ENABLE_WORLD_COORDINATE + const Transform3d instance_trafo_not_translate = mi->get_transformation().get_matrix_no_offset(); +#else const Transform3d instance_trafo_not_translate = mi->get_transformation().get_matrix(true); +#endif // ENABLE_WORLD_COORDINATE // Precalculate transformations of individual meshes. std::vector trafo_matrices; @@ -645,7 +665,11 @@ bool GLGizmoPainterBase::gizmo_event(SLAGizmoEventType action, const Vec2d& mous for (const ModelVolume *mv : mo->volumes) if (mv->is_model_part()) { trafo_matrices.emplace_back(instance_trafo * mv->get_matrix()); +#if ENABLE_WORLD_COORDINATE + trafo_matrices_not_translate.emplace_back(instance_trafo_not_translate * mv->get_matrix_no_offset()); +#else trafo_matrices_not_translate.emplace_back(instance_trafo_not_translate * mv->get_matrix(true)); +#endif // ENABLE_WORLD_COORDINATE } // Now "click" into all the prepared points and spill paint around them. diff --git a/src/slic3r/GUI/Gizmos/GLGizmoRotate.cpp b/src/slic3r/GUI/Gizmos/GLGizmoRotate.cpp index 41f681894..9e2f9e08b 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoRotate.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoRotate.cpp @@ -2,16 +2,19 @@ #include "GLGizmoRotate.hpp" #include "slic3r/GUI/GLCanvas3D.hpp" #include "slic3r/GUI/ImGuiWrapper.hpp" - -#include +#if ENABLE_WORLD_COORDINATE +#include "slic3r/GUI/GUI_ObjectManipulation.hpp" +#endif // ENABLE_WORLD_COORDINATE #include "slic3r/GUI/GUI_App.hpp" #include "slic3r/GUI/GUI.hpp" #include "slic3r/GUI/Plater.hpp" +#include "slic3r/GUI/Jobs/RotoptimizeJob.hpp" + #include "libslic3r/PresetBundle.hpp" #include "libslic3r/Model.hpp" -#include "slic3r/GUI/Jobs/RotoptimizeJob.hpp" +#include namespace Slic3r { namespace GUI { @@ -106,6 +109,9 @@ bool GLGizmoRotate::on_init() void GLGizmoRotate::on_start_dragging() { +#if ENABLE_WORLD_COORDINATE + init_data_from_selection(m_parent.get_selection()); +#else const BoundingBoxf3& box = m_parent.get_selection().get_bounding_box(); m_center = m_has_forced_center ? m_forced_center : box.center(); m_radius = Offset + box.radius(); @@ -113,6 +119,7 @@ void GLGizmoRotate::on_start_dragging() m_snap_coarse_out_radius = 2.0f * m_snap_coarse_in_radius; m_snap_fine_in_radius = m_radius; m_snap_fine_out_radius = m_snap_fine_in_radius + m_radius * ScaleLongTooth; +#endif // ENABLE_WORLD_COORDINATE } void GLGizmoRotate::on_dragging(const UpdateData &data) @@ -158,15 +165,21 @@ void GLGizmoRotate::on_render() #endif // !ENABLE_GIZMO_GRABBER_REFACTOR const Selection& selection = m_parent.get_selection(); +#if !ENABLE_WORLD_COORDINATE const BoundingBoxf3& box = selection.get_bounding_box(); +#endif // !ENABLE_WORLD_COORDINATE if (m_hover_id != 0 && !m_grabbers.front().dragging) { +#if ENABLE_WORLD_COORDINATE + init_data_from_selection(selection); +#else m_center = m_has_forced_center ? m_forced_center : box.center(); m_radius = Offset + box.radius(); m_snap_coarse_in_radius = m_radius / 3.0f; m_snap_coarse_out_radius = 2.0f * m_snap_coarse_in_radius; m_snap_fine_in_radius = m_radius; m_snap_fine_out_radius = m_radius * (1.0f + ScaleLongTooth); +#endif // ENABLE_WORLD_COORDINATE } const double grabber_radius = (double)m_radius * (1.0 + (double)GrabberOffset); @@ -230,10 +243,17 @@ void GLGizmoRotate::on_render() render_angle(); #endif // ENABLE_LEGACY_OPENGL_REMOVAL +#if ENABLE_WORLD_COORDINATE + render_grabber(m_bounding_box); +#if !ENABLE_GIZMO_GRABBER_REFACTOR + render_grabber_extension(m_bounding_box, false); +#endif // !ENABLE_GIZMO_GRABBER_REFACTOR +#else render_grabber(box); #if !ENABLE_GIZMO_GRABBER_REFACTOR render_grabber_extension(box, false); #endif // !ENABLE_GIZMO_GRABBER_REFACTOR +#endif // ENABLE_WORLD_COORDINATE #if !ENABLE_GL_SHADERS_ATTRIBUTES glsafe(::glPopMatrix()); @@ -253,17 +273,73 @@ void GLGizmoRotate::on_render_for_picking() transform_to_local(selection); #endif // ENABLE_GL_SHADERS_ATTRIBUTES +#if ENABLE_WORLD_COORDINATE + render_grabbers_for_picking(m_bounding_box); +#if !ENABLE_GIZMO_GRABBER_REFACTOR + render_grabber_extension(m_bounding_box, true); +#endif // !ENABLE_GIZMO_GRABBER_REFACTOR +#else const BoundingBoxf3& box = selection.get_bounding_box(); render_grabbers_for_picking(box); #if !ENABLE_GIZMO_GRABBER_REFACTOR render_grabber_extension(box, true); #endif // !ENABLE_GIZMO_GRABBER_REFACTOR +#endif // ENABLE_WORLD_COORDINATE #if !ENABLE_GL_SHADERS_ATTRIBUTES glsafe(::glPopMatrix()); #endif // !ENABLE_GL_SHADERS_ATTRIBUTES } +#if ENABLE_WORLD_COORDINATE +void GLGizmoRotate::init_data_from_selection(const Selection& selection) +{ + ECoordinatesType coordinates_type; + if (selection.is_wipe_tower()) + coordinates_type = ECoordinatesType::Local; + else + coordinates_type = wxGetApp().obj_manipul()->get_coordinates_type(); + if (coordinates_type == ECoordinatesType::World) { + m_bounding_box = selection.get_bounding_box(); + m_center = m_bounding_box.center(); + } + else if (coordinates_type == ECoordinatesType::Local && selection.is_single_volume_or_modifier()) { + const GLVolume& v = *selection.get_first_volume(); + m_bounding_box = v.transformed_convex_hull_bounding_box( + v.get_instance_transformation().get_scaling_factor_matrix() * v.get_volume_transformation().get_scaling_factor_matrix()); + m_center = v.world_matrix() * m_bounding_box.center(); + } + else { + m_bounding_box.reset(); + const Selection::IndicesList& ids = selection.get_volume_idxs(); + for (unsigned int id : ids) { + const GLVolume& v = *selection.get_volume(id); + m_bounding_box.merge(v.transformed_convex_hull_bounding_box(v.get_volume_transformation().get_matrix())); + } + const Geometry::Transformation inst_trafo = selection.get_first_volume()->get_instance_transformation(); + m_bounding_box = m_bounding_box.transformed(inst_trafo.get_scaling_factor_matrix()); + m_center = inst_trafo.get_matrix_no_scaling_factor() * m_bounding_box.center(); + } + + m_radius = Offset + m_bounding_box.radius(); + m_snap_coarse_in_radius = m_radius / 3.0f; + m_snap_coarse_out_radius = 2.0f * m_snap_coarse_in_radius; + m_snap_fine_in_radius = m_radius; + m_snap_fine_out_radius = m_snap_fine_in_radius + m_radius * ScaleLongTooth; + + if (coordinates_type == ECoordinatesType::World) + m_orient_matrix = Transform3d::Identity(); + else if (coordinates_type == ECoordinatesType::Local && (selection.is_wipe_tower() || selection.is_single_volume_or_modifier())) { + const GLVolume& v = *selection.get_first_volume(); + m_orient_matrix = v.get_instance_transformation().get_rotation_matrix() * v.get_volume_transformation().get_rotation_matrix(); + } + else { + const GLVolume& v = *selection.get_first_volume(); + m_orient_matrix = v.get_instance_transformation().get_rotation_matrix(); + } +} +#endif // ENABLE_WORLD_COORDINATE + void GLGizmoRotate3D::on_render_input_window(float x, float y, float bottom_limit) { if (wxGetApp().preset_bundle->printers.get_edited_preset().printer_technology() != ptSLA) @@ -324,10 +400,10 @@ void GLGizmoRotate::render_circle() const #else ::glBegin(GL_LINE_LOOP); for (unsigned int i = 0; i < ScaleStepsCount; ++i) { - float angle = (float)i * ScaleStepRad; - float x = ::cos(angle) * m_radius; - float y = ::sin(angle) * m_radius; - float z = 0.0f; + const float angle = float(i) * ScaleStepRad; + const float x = ::cos(angle) * m_radius; + const float y = ::sin(angle) * m_radius; + const float z = 0.0f; ::glVertex3f((GLfloat)x, (GLfloat)y, (GLfloat)z); } glsafe(::glEnd()); @@ -526,10 +602,10 @@ void GLGizmoRotate::render_angle() const #else ::glBegin(GL_LINE_STRIP); for (unsigned int i = 0; i <= AngleResolution; ++i) { - float angle = (float)i * step_angle; - float x = ::cos(angle) * ex_radius; - float y = ::sin(angle) * ex_radius; - float z = 0.0f; + const float angle = float(i) * step_angle; + const float x = ::cos(angle) * ex_radius; + const float y = ::sin(angle) * ex_radius; + const float z = 0.0f; ::glVertex3f((GLfloat)x, (GLfloat)y, (GLfloat)z); } glsafe(::glEnd()); @@ -668,12 +744,20 @@ Transform3d GLGizmoRotate::local_transform(const Selection& selection) const { case X: { - ret = Geometry::assemble_transform(Vec3d::Zero(), Vec3d(0.0, 0.5 * PI, 0.0)) * Geometry::assemble_transform(Vec3d::Zero(), Vec3d(0.0, 0.0, -0.5 * PI)); +#if ENABLE_WORLD_COORDINATE + ret = Geometry::rotation_transform(0.5 * PI * Vec3d::UnitY()) * Geometry::rotation_transform(-0.5 * PI * Vec3d::UnitZ()); +#else + ret = Geometry::assemble_transform(Vec3d::Zero(), 0.5 * PI * Vec3d::UnitY()) * Geometry::assemble_transform(Vec3d::Zero(), -0.5 * PI * Vec3d::UnitZ()); +#endif // ENABLE_WORLD_COORDINATE break; } case Y: { - ret = Geometry::assemble_transform(Vec3d::Zero(), Vec3d(0.0, 0.0, -0.5 * PI)) * Geometry::assemble_transform(Vec3d::Zero(), Vec3d(0.0, -0.5 * PI, 0.0)); +#if ENABLE_WORLD_COORDINATE + ret = Geometry::rotation_transform(-0.5 * PI * Vec3d::UnitZ()) * Geometry::rotation_transform(-0.5 * PI * Vec3d::UnitY()); +#else + ret = Geometry::assemble_transform(Vec3d::Zero(), -0.5 * PI * Vec3d::UnitZ()) * Geometry::assemble_transform(Vec3d::Zero(), -0.5 * PI * Vec3d::UnitY()); +#endif // ENABLE_WORLD_COORDINATE break; } default: @@ -684,20 +768,28 @@ Transform3d GLGizmoRotate::local_transform(const Selection& selection) const } } +#if ENABLE_WORLD_COORDINATE + return Geometry::translation_transform(m_center) * m_orient_matrix * ret; +#else if (selection.is_single_volume() || selection.is_single_modifier() || selection.requires_local_axes()) - ret = selection.get_volume(*selection.get_volume_idxs().begin())->get_instance_transformation().get_matrix(true, false, true, true) * ret; + ret = selection.get_first_volume()->get_instance_transformation().get_matrix(true, false, true, true) * ret; return Geometry::assemble_transform(m_center) * ret; +#endif // ENABLE_WORLD_COORDINATE } #else void GLGizmoRotate::transform_to_local(const Selection& selection) const { glsafe(::glTranslated(m_center.x(), m_center.y(), m_center.z())); +#if ENABLE_WORLD_COORDINATE + glsafe(::glMultMatrixd(m_orient_matrix.data())); +#else if (selection.is_single_volume() || selection.is_single_modifier() || selection.requires_local_axes()) { - const Transform3d orient_matrix = selection.get_volume(*selection.get_volume_idxs().begin())->get_instance_transformation().get_matrix(true, false, true, true); + const Transform3d orient_matrix = selection.get_first_volume()->get_instance_transformation().get_matrix(true, false, true, true); glsafe(::glMultMatrixd(orient_matrix.data())); } +#endif // ENABLE_WORLD_COORDINATE switch (m_axis) { @@ -751,8 +843,12 @@ Vec3d GLGizmoRotate::mouse_position_in_local_plane(const Linef3& mouse_ray, cons } } +#if ENABLE_WORLD_COORDINATE + m = m * m_orient_matrix.inverse(); +#else if (selection.is_single_volume() || selection.is_single_modifier() || selection.requires_local_axes()) - m = m * selection.get_volume(*selection.get_volume_idxs().begin())->get_instance_transformation().get_matrix(true, false, true, true).inverse(); + m = m * selection.get_first_volume()->get_instance_transformation().get_matrix(true, false, true, true).inverse(); +#endif // ENABLE_WORLD_COORDINATE m.translate(-m_center); @@ -765,37 +861,59 @@ GLGizmoRotate3D::GLGizmoRotate3D(GLCanvas3D& parent, const std::string& icon_fil GLGizmoRotate(parent, GLGizmoRotate::X), GLGizmoRotate(parent, GLGizmoRotate::Y), GLGizmoRotate(parent, GLGizmoRotate::Z) }) -{} +{ + load_rotoptimize_state(); +} bool GLGizmoRotate3D::on_mouse(const wxMouseEvent &mouse_event) { if (mouse_event.Dragging() && m_dragging && !m_use_only_grabbers) { // Apply new temporary rotations - TransformationType transformation_type( - TransformationType::World_Relative_Joint); - if (mouse_event.AltDown()) transformation_type.set_independent(); +#if ENABLE_WORLD_COORDINATE + TransformationType transformation_type; + if (m_parent.get_selection().is_wipe_tower()) + transformation_type = TransformationType::Instance_Relative_Joint; + else { + switch (wxGetApp().obj_manipul()->get_coordinates_type()) + { + default: + case ECoordinatesType::World: { transformation_type = TransformationType::World_Relative_Joint; break; } + case ECoordinatesType::Instance: { transformation_type = TransformationType::Instance_Relative_Joint; break; } + case ECoordinatesType::Local: { transformation_type = TransformationType::Local_Relative_Joint; break; } + } + } +#else + TransformationType transformation_type(TransformationType::World_Relative_Joint); +#endif // ENABLE_WORLD_COORDINATE + if (mouse_event.AltDown()) + transformation_type.set_independent(); m_parent.get_selection().rotate(get_rotation(), transformation_type); } return use_grabbers(mouse_event); } void GLGizmoRotate3D::data_changed() { - const Selection &selection = m_parent.get_selection(); - bool is_wipe_tower = selection.is_wipe_tower(); - if (is_wipe_tower) { - DynamicPrintConfig& config = wxGetApp().preset_bundle->prints.get_edited_preset().config; - float wipe_tower_rotation_angle = - dynamic_cast( - config.option("wipe_tower_rotation_angle")) - ->value; + if (m_parent.get_selection().is_wipe_tower()) { +#if !ENABLE_WORLD_COORDINATE + const DynamicPrintConfig& config = wxGetApp().preset_bundle->prints.get_edited_preset().config; + const float wipe_tower_rotation_angle = + dynamic_cast( + config.option("wipe_tower_rotation_angle"))->value; set_rotation(Vec3d(0., 0., (M_PI / 180.) * wipe_tower_rotation_angle)); +#endif // !ENABLE_WORLD_COORDINATE m_gizmos[0].disable_grabber(); m_gizmos[1].disable_grabber(); - } else { + } + else { +#if !ENABLE_WORLD_COORDINATE set_rotation(Vec3d::Zero()); +#endif // !ENABLE_WORLD_COORDINATE m_gizmos[0].enable_grabber(); m_gizmos[1].enable_grabber(); } +#if ENABLE_WORLD_COORDINATE + set_rotation(Vec3d::Zero()); +#endif // ENABLE_WORLD_COORDINATE } bool GLGizmoRotate3D::on_init() diff --git a/src/slic3r/GUI/Gizmos/GLGizmoRotate.hpp b/src/slic3r/GUI/Gizmos/GLGizmoRotate.hpp index 395d7451b..9db024b14 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoRotate.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoRotate.hpp @@ -36,6 +36,10 @@ private: float m_snap_fine_out_radius{ 0.0f }; bool m_has_forced_center{false}; Vec3d m_forced_center{ Vec3d::Zero() }; +#if ENABLE_WORLD_COORDINATE + BoundingBoxf3 m_bounding_box; + Transform3d m_orient_matrix{ Transform3d::Identity() }; +#endif // ENABLE_WORLD_COORDINATE #if !ENABLE_GIZMO_GRABBER_REFACTOR GLModel m_cone; @@ -122,6 +126,10 @@ private: // returns the intersection of the mouse ray with the plane perpendicular to the gizmo axis, in local coordinate Vec3d mouse_position_in_local_plane(const Linef3& mouse_ray, const Selection& selection) const; + +#if ENABLE_WORLD_COORDINATE + void init_data_from_selection(const Selection& selection); +#endif // ENABLE_WORLD_COORDINATE }; class GLGizmoRotate3D : public GLGizmoBase @@ -133,7 +141,7 @@ public: GLGizmoRotate3D(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id); Vec3d get_rotation() const { return Vec3d(m_gizmos[X].get_angle(), m_gizmos[Y].get_angle(), m_gizmos[Z].get_angle()); } - void set_rotation(const Vec3d& rotation) { m_gizmos[X].set_angle(rotation(0)); m_gizmos[Y].set_angle(rotation(1)); m_gizmos[Z].set_angle(rotation(2)); } + void set_rotation(const Vec3d& rotation) { m_gizmos[X].set_angle(rotation.x()); m_gizmos[Y].set_angle(rotation.y()); m_gizmos[Z].set_angle(rotation.z()); } void set_center(const Vec3d& center) { m_gizmos[X].set_center(center); m_gizmos[Y].set_center(center); m_gizmos[Z].set_center(center); } void use_only_grabbers() { m_use_only_grabbers = true; } diff --git a/src/slic3r/GUI/Gizmos/GLGizmoScale.cpp b/src/slic3r/GUI/Gizmos/GLGizmoScale.cpp index 6d956d17e..4312a6122 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoScale.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoScale.cpp @@ -2,6 +2,9 @@ #include "GLGizmoScale.hpp" #include "slic3r/GUI/GLCanvas3D.hpp" #include "slic3r/GUI/GUI_App.hpp" +#if ENABLE_WORLD_COORDINATE +#include "slic3r/GUI/GUI_ObjectManipulation.hpp" +#endif // ENABLE_WORLD_COORDINATE #if ENABLE_GL_SHADERS_ATTRIBUTES #include "slic3r/GUI/Plater.hpp" #endif // ENABLE_GL_SHADERS_ATTRIBUTES @@ -9,13 +12,13 @@ #include -#include +#include namespace Slic3r { namespace GUI { -const float GLGizmoScale3D::Offset = 5.0f; +const double GLGizmoScale3D::Offset = 5.0; GLGizmoScale3D::GLGizmoScale3D(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id) : GLGizmoBase(parent, icon_filename, sprite_id) @@ -39,16 +42,17 @@ GLGizmoScale3D::GLGizmoScale3D(GLCanvas3D& parent, const std::string& icon_filen std::string GLGizmoScale3D::get_tooltip() const { +#if ENABLE_WORLD_COORDINATE + const Vec3d scale = 100.0 * m_scale; +#else const Selection& selection = m_parent.get_selection(); - bool single_instance = selection.is_single_full_instance(); - bool single_volume = selection.is_single_modifier() || selection.is_single_volume(); - - Vec3f scale = 100.0f * Vec3f::Ones(); - if (single_instance) - scale = 100.0f * selection.get_volume(*selection.get_volume_idxs().begin())->get_instance_scaling_factor().cast(); - else if (single_volume) - scale = 100.0f * selection.get_volume(*selection.get_volume_idxs().begin())->get_volume_scaling_factor().cast(); + Vec3d scale = 100.0 * Vec3d::Ones(); + if (selection.is_single_full_instance()) + scale = 100.0 * selection.get_first_volume()->get_instance_scaling_factor(); + else if (selection.is_single_modifier() || selection.is_single_volume()) + scale = 100.0 * selection.get_first_volume()->get_volume_scaling_factor(); +#endif // ENABLE_WORLD_COORDINATE if (m_hover_id == 0 || m_hover_id == 1 || m_grabbers[0].dragging || m_grabbers[1].dragging) return "X: " + format(scale.x(), 4) + "%"; @@ -73,12 +77,28 @@ bool GLGizmoScale3D::on_mouse(const wxMouseEvent &mouse_event) if (mouse_event.Dragging()) { if (m_dragging) { // Apply new temporary scale factors - TransformationType transformation_type(TransformationType::Local_Absolute_Joint); - if (mouse_event.AltDown()) transformation_type.set_independent(); +#if ENABLE_WORLD_COORDINATE + TransformationType transformation_type; + if (wxGetApp().obj_manipul()->is_local_coordinates()) + transformation_type.set_local(); + else if (wxGetApp().obj_manipul()->is_instance_coordinates()) + transformation_type.set_instance(); - Selection &selection = m_parent.get_selection(); - selection.scale(get_scale(), transformation_type); + transformation_type.set_relative(); +#else + TransformationType transformation_type(TransformationType::Local_Absolute_Joint); +#endif // ENABLE_WORLD_COORDINATE + + if (mouse_event.AltDown()) + transformation_type.set_independent(); + +#if ENABLE_WORLD_COORDINATE + m_parent.get_selection().scale_and_translate(m_scale, m_offset, transformation_type); +#else + Selection& selection = m_parent.get_selection(); + selection.scale(m_scale, transformation_type); if (mouse_event.CmdDown()) selection.translate(m_offset, true); +#endif // ENABLE_WORLD_COORDINATE } } return use_grabbers(mouse_event); @@ -86,26 +106,29 @@ bool GLGizmoScale3D::on_mouse(const wxMouseEvent &mouse_event) void GLGizmoScale3D::data_changed() { - const Selection &selection = m_parent.get_selection(); - bool enable_scale_xyz = selection.is_single_full_instance() || - selection.is_single_volume() || - selection.is_single_modifier(); +#if ENABLE_WORLD_COORDINATE + set_scale(Vec3d::Ones()); +#else + const Selection& selection = m_parent.get_selection(); + bool enable_scale_xyz = selection.is_single_full_instance() || + selection.is_single_volume() || + selection.is_single_modifier(); + for (unsigned int i = 0; i < 6; ++i) m_grabbers[i].enabled = enable_scale_xyz; if (enable_scale_xyz) { // all volumes in the selection belongs to the same instance, any of // them contains the needed data, so we take the first - const GLVolume *volume = selection.get_volume(*selection.get_volume_idxs().begin()); - if (selection.is_single_full_instance()) { + const GLVolume* volume = selection.get_first_volume(); + if (selection.is_single_full_instance()) set_scale(volume->get_instance_scaling_factor()); - } else if (selection.is_single_volume() || - selection.is_single_modifier()) { + else if (selection.is_single_volume() || selection.is_single_modifier()) set_scale(volume->get_volume_scaling_factor()); - } - } else { - set_scale(Vec3d::Ones()); } + else + set_scale(Vec3d::Ones()); +#endif // ENABLE_WORLD_COORDINATE } bool GLGizmoScale3D::on_init() @@ -114,15 +137,17 @@ bool GLGizmoScale3D::on_init() m_grabbers.push_back(Grabber()); } +#if !ENABLE_WORLD_COORDINATE double half_pi = 0.5 * (double)PI; // x axis - m_grabbers[0].angles(1) = half_pi; - m_grabbers[1].angles(1) = half_pi; + m_grabbers[0].angles.y() = half_pi; + m_grabbers[1].angles.y() = half_pi; // y axis - m_grabbers[2].angles(0) = half_pi; - m_grabbers[3].angles(0) = half_pi; + m_grabbers[2].angles.x() = half_pi; + m_grabbers[3].angles.x() = half_pi; +#endif // !ENABLE_WORLD_COORDINATE m_shortcut_key = WXK_CONTROL_S; @@ -148,9 +173,15 @@ bool GLGizmoScale3D::on_is_activable() const void GLGizmoScale3D::on_start_dragging() { assert(m_hover_id != -1); - m_starting.drag_position = m_grabbers[m_hover_id].center; m_starting.ctrl_down = wxGetKeyState(WXK_CONTROL); - m_starting.box = (m_starting.ctrl_down && (m_hover_id < 6)) ? m_box : m_parent.get_selection().get_bounding_box(); +#if ENABLE_WORLD_COORDINATE + m_starting.drag_position = m_grabbers_transform * m_grabbers[m_hover_id].center; + m_starting.box = m_bounding_box; + m_starting.center = m_center; + m_starting.instance_center = m_instance_center; +#else + m_starting.drag_position = m_grabbers[m_hover_id].center; + m_starting.box = (m_starting.ctrl_down && m_hover_id < 6) ? m_bounding_box : m_parent.get_selection().get_bounding_box(); const Vec3d& center = m_starting.box.center(); m_starting.pivots[0] = m_transform * Vec3d(m_starting.box.max.x(), center.y(), center.z()); @@ -159,6 +190,7 @@ void GLGizmoScale3D::on_start_dragging() m_starting.pivots[3] = m_transform * Vec3d(center.x(), m_starting.box.min.y(), center.z()); m_starting.pivots[4] = m_transform * Vec3d(center.x(), center.y(), m_starting.box.max.z()); m_starting.pivots[5] = m_transform * Vec3d(center.x(), center.y(), m_starting.box.min.z()); +#endif // ENABLE_WORLD_COORDINATE } void GLGizmoScale3D::on_stop_dragging() { @@ -181,78 +213,156 @@ void GLGizmoScale3D::on_render() { const Selection& selection = m_parent.get_selection(); - bool single_instance = selection.is_single_full_instance(); - bool single_volume = selection.is_single_modifier() || selection.is_single_volume(); - glsafe(::glClear(GL_DEPTH_BUFFER_BIT)); glsafe(::glEnable(GL_DEPTH_TEST)); - m_box.reset(); + m_bounding_box.reset(); +#if ENABLE_WORLD_COORDINATE + m_grabbers_transform = Transform3d::Identity(); + m_center = Vec3d::Zero(); + m_instance_center = Vec3d::Zero(); + if (selection.is_single_full_instance() && !wxGetApp().obj_manipul()->is_world_coordinates()) { +#else m_transform = Transform3d::Identity(); // Transforms grabbers' offsets to world refefence system Transform3d offsets_transform = Transform3d::Identity(); m_offsets_transform = Transform3d::Identity(); Vec3d angles = Vec3d::Zero(); - if (single_instance) { + if (selection.is_single_full_instance()) { +#endif // ENABLE_WORLD_COORDINATE // calculate bounding box in instance local reference system const Selection::IndicesList& idxs = selection.get_volume_idxs(); for (unsigned int idx : idxs) { - const GLVolume* vol = selection.get_volume(idx); - m_box.merge(vol->bounding_box().transformed(vol->get_volume_transformation().get_matrix())); + const GLVolume& v = *selection.get_volume(idx); + m_bounding_box.merge(v.transformed_convex_hull_bounding_box(v.get_volume_transformation().get_matrix())); } +#if ENABLE_WORLD_COORDINATE + m_bounding_box = m_bounding_box.transformed(selection.get_first_volume()->get_instance_transformation().get_scaling_factor_matrix()); +#endif // ENABLE_WORLD_COORDINATE + // gets transform from first selected volume - const GLVolume* v = selection.get_volume(*idxs.begin()); - m_transform = v->get_instance_transformation().get_matrix(); + const GLVolume& v = *selection.get_first_volume(); +#if ENABLE_WORLD_COORDINATE + const Transform3d inst_trafo = v.get_instance_transformation().get_matrix_no_scaling_factor(); + m_grabbers_transform = inst_trafo * Geometry::translation_transform(m_bounding_box.center()); + m_center = inst_trafo * m_bounding_box.center(); + m_instance_center = v.get_instance_offset(); + } + else if (selection.is_single_volume_or_modifier() && wxGetApp().obj_manipul()->is_instance_coordinates()) { +#else + m_transform = v.get_instance_transformation().get_matrix(); + // gets angles from first selected volume - angles = v->get_instance_rotation(); + angles = v.get_instance_rotation(); // consider rotation+mirror only components of the transform for offsets - offsets_transform = Geometry::assemble_transform(Vec3d::Zero(), angles, Vec3d::Ones(), v->get_instance_mirror()); + offsets_transform = Geometry::assemble_transform(Vec3d::Zero(), angles, Vec3d::Ones(), v.get_instance_mirror()); m_offsets_transform = offsets_transform; } - else if (single_volume) { - const GLVolume* v = selection.get_volume(*selection.get_volume_idxs().begin()); - m_box = v->bounding_box(); - m_transform = v->world_matrix(); + else if (selection.is_single_modifier() || selection.is_single_volume()) { +#endif // ENABLE_WORLD_COORDINATE + const GLVolume& v = *selection.get_first_volume(); +#if ENABLE_WORLD_COORDINATE + m_bounding_box.merge(v.transformed_convex_hull_bounding_box( + v.get_instance_transformation().get_scaling_factor_matrix() * v.get_volume_transformation().get_matrix_no_offset())); + Geometry::Transformation trafo(v.get_instance_transformation().get_rotation_matrix()); + trafo.set_offset(v.world_matrix().translation()); + m_grabbers_transform = trafo.get_matrix(); + m_center = v.world_matrix() * m_bounding_box.center(); + m_instance_center = m_center; + } + else if (selection.is_single_volume_or_modifier() && wxGetApp().obj_manipul()->is_local_coordinates()) { + const GLVolume& v = *selection.get_first_volume(); + m_bounding_box.merge(v.transformed_convex_hull_bounding_box( + v.get_instance_transformation().get_scaling_factor_matrix() * v.get_volume_transformation().get_scaling_factor_matrix())); + Geometry::Transformation trafo(v.get_instance_transformation().get_rotation_matrix() * v.get_volume_transformation().get_rotation_matrix()); + trafo.set_offset(v.world_matrix().translation()); + m_grabbers_transform = trafo.get_matrix(); + m_center = v.world_matrix() * m_bounding_box.center(); + m_instance_center = m_center; + } + else { + m_bounding_box = selection.get_bounding_box(); + m_grabbers_transform = Geometry::assemble_transform(m_bounding_box.center()); + m_center = m_bounding_box.center(); + m_instance_center = selection.is_single_full_instance() ? selection.get_first_volume()->get_instance_offset() : m_center; + } +#else + m_bounding_box = v.bounding_box(); + m_transform = v.world_matrix(); angles = Geometry::extract_euler_angles(m_transform); // consider rotation+mirror only components of the transform for offsets - offsets_transform = Geometry::assemble_transform(Vec3d::Zero(), angles, Vec3d::Ones(), v->get_instance_mirror()); - m_offsets_transform = Geometry::assemble_transform(Vec3d::Zero(), v->get_volume_rotation(), Vec3d::Ones(), v->get_volume_mirror()); + offsets_transform = Geometry::assemble_transform(Vec3d::Zero(), angles, Vec3d::Ones(), v.get_instance_mirror()); + m_offsets_transform = Geometry::assemble_transform(Vec3d::Zero(), v.get_volume_rotation(), Vec3d::Ones(), v.get_volume_mirror()); } else - m_box = selection.get_bounding_box(); + m_bounding_box = selection.get_bounding_box(); - const Vec3d& center = m_box.center(); + const Vec3d& center = m_bounding_box.center(); const Vec3d offset_x = offsets_transform * Vec3d((double)Offset, 0.0, 0.0); const Vec3d offset_y = offsets_transform * Vec3d(0.0, (double)Offset, 0.0); const Vec3d offset_z = offsets_transform * Vec3d(0.0, 0.0, (double)Offset); const bool ctrl_down = (m_dragging && m_starting.ctrl_down) || (!m_dragging && wxGetKeyState(WXK_CONTROL)); +#endif // ENABLE_WORLD_COORDINATE +#if ENABLE_WORLD_COORDINATE // x axis - m_grabbers[0].center = m_transform * Vec3d(m_box.min.x(), center.y(), center.z()) - offset_x; + const Vec3d box_half_size = 0.5 * m_bounding_box.size(); + bool use_constrain = wxGetKeyState(WXK_CONTROL) && (selection.is_single_full_instance() || selection.is_single_volume_or_modifier()); + + m_grabbers[0].center = { -(box_half_size.x() + Offset), 0.0, 0.0 }; + m_grabbers[0].color = (use_constrain && m_hover_id == 1) ? CONSTRAINED_COLOR : AXES_COLOR[0]; + m_grabbers[1].center = { box_half_size.x() + Offset, 0.0, 0.0 }; + m_grabbers[1].color = (use_constrain && m_hover_id == 0) ? CONSTRAINED_COLOR : AXES_COLOR[0]; + + // y axis + m_grabbers[2].center = { 0.0, -(box_half_size.y() + Offset), 0.0 }; + m_grabbers[2].color = (use_constrain && m_hover_id == 3) ? CONSTRAINED_COLOR : AXES_COLOR[1]; + m_grabbers[3].center = { 0.0, box_half_size.y() + Offset, 0.0 }; + m_grabbers[3].color = (use_constrain && m_hover_id == 2) ? CONSTRAINED_COLOR : AXES_COLOR[1]; + + // z axis + m_grabbers[4].center = { 0.0, 0.0, -(box_half_size.z() + Offset) }; + m_grabbers[4].color = (use_constrain && m_hover_id == 5) ? CONSTRAINED_COLOR : AXES_COLOR[2]; + m_grabbers[5].center = { 0.0, 0.0, box_half_size.z() + Offset }; + m_grabbers[5].color = (use_constrain && m_hover_id == 4) ? CONSTRAINED_COLOR : AXES_COLOR[2]; + + // uniform + m_grabbers[6].center = { -(box_half_size.x() + Offset), -(box_half_size.y() + Offset), 0.0 }; + m_grabbers[6].color = (use_constrain && m_hover_id == 8) ? CONSTRAINED_COLOR : m_highlight_color; + m_grabbers[7].center = { box_half_size.x() + Offset, -(box_half_size.y() + Offset), 0.0 }; + m_grabbers[7].color = (use_constrain && m_hover_id == 9) ? CONSTRAINED_COLOR : m_highlight_color; + m_grabbers[8].center = { box_half_size.x() + Offset, box_half_size.y() + Offset, 0.0 }; + m_grabbers[8].color = (use_constrain && m_hover_id == 6) ? CONSTRAINED_COLOR : m_highlight_color; + m_grabbers[9].center = { -(box_half_size.x() + Offset), box_half_size.y() + Offset, 0.0 }; + m_grabbers[9].color = (use_constrain && m_hover_id == 7) ? CONSTRAINED_COLOR : m_highlight_color; +#else + // x axis + m_grabbers[0].center = m_transform * Vec3d(m_bounding_box.min.x(), center.y(), center.z()) - offset_x; m_grabbers[0].color = (ctrl_down && m_hover_id == 1) ? CONSTRAINED_COLOR : AXES_COLOR[0]; - m_grabbers[1].center = m_transform * Vec3d(m_box.max.x(), center.y(), center.z()) + offset_x; + m_grabbers[1].center = m_transform * Vec3d(m_bounding_box.max.x(), center.y(), center.z()) + offset_x; m_grabbers[1].color = (ctrl_down && m_hover_id == 0) ? CONSTRAINED_COLOR : AXES_COLOR[0]; // y axis - m_grabbers[2].center = m_transform * Vec3d(center.x(), m_box.min.y(), center.z()) - offset_y; + m_grabbers[2].center = m_transform * Vec3d(center.x(), m_bounding_box.min.y(), center.z()) - offset_y; m_grabbers[2].color = (ctrl_down && m_hover_id == 3) ? CONSTRAINED_COLOR : AXES_COLOR[1]; - m_grabbers[3].center = m_transform * Vec3d(center.x(), m_box.max.y(), center.z()) + offset_y; + m_grabbers[3].center = m_transform * Vec3d(center.x(), m_bounding_box.max.y(), center.z()) + offset_y; m_grabbers[3].color = (ctrl_down && m_hover_id == 2) ? CONSTRAINED_COLOR : AXES_COLOR[1]; // z axis - m_grabbers[4].center = m_transform * Vec3d(center.x(), center.y(), m_box.min.z()) - offset_z; + m_grabbers[4].center = m_transform * Vec3d(center.x(), center.y(), m_bounding_box.min.z()) - offset_z; m_grabbers[4].color = (ctrl_down && m_hover_id == 5) ? CONSTRAINED_COLOR : AXES_COLOR[2]; - m_grabbers[5].center = m_transform * Vec3d(center.x(), center.y(), m_box.max.z()) + offset_z; + m_grabbers[5].center = m_transform * Vec3d(center.x(), center.y(), m_bounding_box.max.z()) + offset_z; m_grabbers[5].color = (ctrl_down && m_hover_id == 4) ? CONSTRAINED_COLOR : AXES_COLOR[2]; // uniform - m_grabbers[6].center = m_transform * Vec3d(m_box.min.x(), m_box.min.y(), center.z()) - offset_x - offset_y; - m_grabbers[7].center = m_transform * Vec3d(m_box.max.x(), m_box.min.y(), center.z()) + offset_x - offset_y; - m_grabbers[8].center = m_transform * Vec3d(m_box.max.x(), m_box.max.y(), center.z()) + offset_x + offset_y; - m_grabbers[9].center = m_transform * Vec3d(m_box.min.x(), m_box.max.y(), center.z()) - offset_x + offset_y; + m_grabbers[6].center = m_transform * Vec3d(m_bounding_box.min.x(), m_bounding_box.min.y(), center.z()) - offset_x - offset_y; + m_grabbers[7].center = m_transform * Vec3d(m_bounding_box.max.x(), m_bounding_box.min.y(), center.z()) + offset_x - offset_y; + m_grabbers[8].center = m_transform * Vec3d(m_bounding_box.max.x(), m_bounding_box.max.y(), center.z()) + offset_x + offset_y; + m_grabbers[9].center = m_transform * Vec3d(m_bounding_box.min.x(), m_bounding_box.max.y(), center.z()) - offset_x + offset_y; + for (int i = 6; i < 10; ++i) { m_grabbers[i].color = m_highlight_color; } @@ -261,12 +371,25 @@ void GLGizmoScale3D::on_render() for (int i = 0; i < 10; ++i) { m_grabbers[i].angles = angles; } +#endif // ENABLE_WORLD_COORDINATE glsafe(::glLineWidth((m_hover_id != -1) ? 2.0f : 1.5f)); +#if ENABLE_WORLD_COORDINATE +#if ENABLE_GL_SHADERS_ATTRIBUTES + const Transform3d base_matrix = local_transform(selection); + for (int i = 0; i < 10; ++i) { + m_grabbers[i].matrix = base_matrix; + } +#else + glsafe(::glPushMatrix()); + transform_to_local(selection); +#endif // ENABLE_GL_SHADERS_ATTRIBUTES + const float grabber_mean_size = (float)((m_bounding_box.size().x() + m_bounding_box.size().y() + m_bounding_box.size().z()) / 3.0); +#else const BoundingBoxf3& selection_box = selection.get_bounding_box(); - const float grabber_mean_size = (float)((selection_box.size().x() + selection_box.size().y() + selection_box.size().z()) / 3.0); +#endif // ENABLE_WORLD_COORDINATE if (m_hover_id == -1) { #if ENABLE_LEGACY_OPENGL_REMOVAL @@ -276,7 +399,11 @@ void GLGizmoScale3D::on_render() shader->start_using(); #if ENABLE_GL_SHADERS_ATTRIBUTES const Camera& camera = wxGetApp().plater()->get_camera(); +#if ENABLE_WORLD_COORDINATE + shader->set_uniform("view_model_matrix", camera.get_view_matrix() * base_matrix); +#else shader->set_uniform("view_model_matrix", camera.get_view_matrix()); +#endif // ENABLE_WORLD_COORDINATE shader->set_uniform("projection_matrix", camera.get_projection_matrix()); #endif // ENABLE_GL_SHADERS_ATTRIBUTES if (m_grabbers[0].enabled && m_grabbers[1].enabled) @@ -323,7 +450,11 @@ void GLGizmoScale3D::on_render() shader->start_using(); #if ENABLE_GL_SHADERS_ATTRIBUTES const Camera& camera = wxGetApp().plater()->get_camera(); +#if ENABLE_WORLD_COORDINATE + shader->set_uniform("view_model_matrix", camera.get_view_matrix() * base_matrix); +#else shader->set_uniform("view_model_matrix", camera.get_view_matrix()); +#endif // ENABLE_WORLD_COORDINATE shader->set_uniform("projection_matrix", camera.get_projection_matrix()); #endif // ENABLE_GL_SHADERS_ATTRIBUTES render_grabbers_connection(0, 1, m_grabbers[0].color); @@ -334,7 +465,7 @@ void GLGizmoScale3D::on_render() shader = wxGetApp().get_shader("gouraud_light"); #else // draw connection - glsafe(::glColor4fv(m_grabbers[0].color.data())); + glsafe(::glColor4fv(AXES_COLOR[0].data())); render_grabbers_connection(0, 1); // draw grabbers @@ -356,7 +487,11 @@ void GLGizmoScale3D::on_render() shader->start_using(); #if ENABLE_GL_SHADERS_ATTRIBUTES const Camera& camera = wxGetApp().plater()->get_camera(); +#if ENABLE_WORLD_COORDINATE + shader->set_uniform("view_model_matrix", camera.get_view_matrix() * base_matrix); +#else shader->set_uniform("view_model_matrix", camera.get_view_matrix()); +#endif // ENABLE_WORLD_COORDINATE shader->set_uniform("projection_matrix", camera.get_projection_matrix()); #endif // ENABLE_GL_SHADERS_ATTRIBUTES render_grabbers_connection(2, 3, m_grabbers[2].color); @@ -367,7 +502,7 @@ void GLGizmoScale3D::on_render() shader = wxGetApp().get_shader("gouraud_light"); #else // draw connection - glsafe(::glColor4fv(m_grabbers[2].color.data())); + glsafe(::glColor4fv(AXES_COLOR[1].data())); render_grabbers_connection(2, 3); // draw grabbers @@ -389,7 +524,11 @@ void GLGizmoScale3D::on_render() shader->start_using(); #if ENABLE_GL_SHADERS_ATTRIBUTES const Camera& camera = wxGetApp().plater()->get_camera(); +#if ENABLE_WORLD_COORDINATE + shader->set_uniform("view_model_matrix", camera.get_view_matrix() * base_matrix); +#else shader->set_uniform("view_model_matrix", camera.get_view_matrix()); +#endif // ENABLE_WORLD_COORDINATE shader->set_uniform("projection_matrix", camera.get_projection_matrix()); #endif // ENABLE_GL_SHADERS_ATTRIBUTES render_grabbers_connection(4, 5, m_grabbers[4].color); @@ -400,7 +539,7 @@ void GLGizmoScale3D::on_render() shader = wxGetApp().get_shader("gouraud_light"); #else // draw connection - glsafe(::glColor4fv(m_grabbers[4].color.data())); + glsafe(::glColor4fv(AXES_COLOR[2].data())); render_grabbers_connection(4, 5); // draw grabbers @@ -422,7 +561,11 @@ void GLGizmoScale3D::on_render() shader->start_using(); #if ENABLE_GL_SHADERS_ATTRIBUTES const Camera& camera = wxGetApp().plater()->get_camera(); +#if ENABLE_WORLD_COORDINATE + shader->set_uniform("view_model_matrix", camera.get_view_matrix() * base_matrix); +#else shader->set_uniform("view_model_matrix", camera.get_view_matrix()); +#endif // ENABLE_WORLD_COORDINATE shader->set_uniform("projection_matrix", camera.get_projection_matrix()); #endif // ENABLE_GL_SHADERS_ATTRIBUTES render_grabbers_connection(6, 7, m_drag_color); @@ -454,12 +597,34 @@ void GLGizmoScale3D::on_render() shader->stop_using(); } } + +#if ENABLE_WORLD_COORDINATE +#if !ENABLE_GL_SHADERS_ATTRIBUTES + glsafe(::glPopMatrix()); +#endif // !ENABLE_GL_SHADERS_ATTRIBUTES +#endif // ENABLE_WORLD_COORDINATE } void GLGizmoScale3D::on_render_for_picking() { glsafe(::glDisable(GL_DEPTH_TEST)); +#if ENABLE_WORLD_COORDINATE +#if ENABLE_GL_SHADERS_ATTRIBUTES + const Transform3d base_matrix = local_transform(m_parent.get_selection()); + for (int i = 0; i < 10; ++i) { + m_grabbers[i].matrix = base_matrix; + } +#else + glsafe(::glPushMatrix()); + transform_to_local(m_parent.get_selection()); +#endif // ENABLE_GL_SHADERS_ATTRIBUTES + render_grabbers_for_picking(m_bounding_box); +#if !ENABLE_GL_SHADERS_ATTRIBUTES + glsafe(::glPopMatrix()); +#endif // !ENABLE_GL_SHADERS_ATTRIBUTES +#else render_grabbers_for_picking(m_parent.get_selection().get_bounding_box()); +#endif // ENABLE_WORLD_COORDINATE } #if ENABLE_LEGACY_OPENGL_REMOVAL @@ -515,13 +680,67 @@ void GLGizmoScale3D::render_grabbers_connection(unsigned int id_1, unsigned int } #endif // ENABLE_LEGACY_OPENGL_REMOVAL +#if ENABLE_WORLD_COORDINATE void GLGizmoScale3D::do_scale_along_axis(Axis axis, const UpdateData& data) { double ratio = calc_ratio(data); + if (ratio > 0.0) { + Vec3d curr_scale = m_scale; + Vec3d starting_scale = m_starting.scale; + const Selection& selection = m_parent.get_selection(); + const ECoordinatesType coordinates_type = wxGetApp().obj_manipul()->get_coordinates_type(); + + curr_scale(axis) = starting_scale(axis) * ratio; + m_scale = curr_scale; + + if (m_starting.ctrl_down && (selection.is_single_full_instance() || selection.is_single_volume_or_modifier())) { + double local_offset = 0.5 * (ratio - 1.0) * m_starting.box.size()(axis); + + if (m_hover_id == 2 * axis) + local_offset *= -1.0; + + Vec3d center_offset = m_starting.instance_center - m_starting.center; // world coordinates (== Vec3d::Zero() for single volume selection) + if (selection.is_single_full_instance() && coordinates_type == ECoordinatesType::Local) + // from world coordinates to instance coordinates + center_offset = selection.get_first_volume()->get_instance_transformation().get_rotation_matrix().inverse() * center_offset; + + local_offset += (ratio - 1.0) * center_offset(axis); + + switch (axis) + { + case X: { m_offset = local_offset * Vec3d::UnitX(); break; } + case Y: { m_offset = local_offset * Vec3d::UnitY(); break; } + case Z: { m_offset = local_offset * Vec3d::UnitZ(); break; } + default: { m_offset = Vec3d::Zero(); break; } + } + + if (selection.is_single_full_instance() && coordinates_type == ECoordinatesType::Local) + // from instance coordinates to world coordinates + m_offset = selection.get_first_volume()->get_instance_transformation().get_rotation_matrix() * m_offset; + + if (selection.is_single_volume_or_modifier()) { + if (coordinates_type == ECoordinatesType::Instance) + m_offset = selection.get_first_volume()->get_instance_transformation().get_scaling_factor_matrix().inverse() * m_offset; + else if (coordinates_type == ECoordinatesType::Local) { + m_offset = selection.get_first_volume()->get_instance_transformation().get_scaling_factor_matrix().inverse() * + selection.get_first_volume()->get_volume_transformation().get_rotation_matrix() * m_offset; + } + } + } + else + m_offset = Vec3d::Zero(); + } +} +#else +void GLGizmoScale3D::do_scale_along_axis(Axis axis, const UpdateData& data) +{ + const double ratio = calc_ratio(data); if (ratio > 0.0) { m_scale(axis) = m_starting.scale(axis) * ratio; + if (m_starting.ctrl_down) { double local_offset = 0.5 * (m_scale(axis) - m_starting.scale(axis)) * m_starting.box.size()(axis); + if (m_hover_id == 2 * axis) local_offset *= -1.0; @@ -540,36 +759,86 @@ void GLGizmoScale3D::do_scale_along_axis(Axis axis, const UpdateData& data) m_offset = Vec3d::Zero(); } } +#endif // ENABLE_WORLD_COORDINATE +#if ENABLE_WORLD_COORDINATE +void GLGizmoScale3D::do_scale_uniform(const UpdateData & data) +{ + const double ratio = calc_ratio(data); + if (ratio > 0.0) { + m_scale = m_starting.scale * ratio; + + const Selection& selection = m_parent.get_selection(); + const ECoordinatesType coordinates_type = wxGetApp().obj_manipul()->get_coordinates_type(); + if (m_starting.ctrl_down && (selection.is_single_full_instance() || selection.is_single_volume_or_modifier())) { + m_offset = 0.5 * (ratio - 1.0) * m_starting.box.size(); + + if (m_hover_id == 6 || m_hover_id == 9) + m_offset.x() *= -1.0; + if (m_hover_id == 6 || m_hover_id == 7) + m_offset.y() *= -1.0; + + Vec3d center_offset = m_starting.instance_center - m_starting.center; // world coordinates (== Vec3d::Zero() for single volume selection) + + if (selection.is_single_full_instance() && coordinates_type == ECoordinatesType::Local) + // from world coordinates to instance coordinates + center_offset = selection.get_first_volume()->get_instance_transformation().get_rotation_matrix().inverse() * center_offset; + + m_offset += (ratio - 1.0) * center_offset; + + if (selection.is_single_full_instance() && coordinates_type == ECoordinatesType::Local) + // from instance coordinates to world coordinates + m_offset = selection.get_first_volume()->get_instance_transformation().get_rotation_matrix() * m_offset; + + if (selection.is_single_volume_or_modifier()) { + if (coordinates_type == ECoordinatesType::Instance) + m_offset = selection.get_first_volume()->get_instance_transformation().get_scaling_factor_matrix().inverse() * m_offset; + else if (coordinates_type == ECoordinatesType::Local) { + m_offset = selection.get_first_volume()->get_instance_transformation().get_scaling_factor_matrix().inverse() * + selection.get_first_volume()->get_volume_transformation().get_rotation_matrix() * m_offset; + } + } + } + else + m_offset = Vec3d::Zero(); + } +} +#else void GLGizmoScale3D::do_scale_uniform(const UpdateData& data) { - double ratio = calc_ratio(data); + const double ratio = calc_ratio(data); if (ratio > 0.0) { m_scale = m_starting.scale * ratio; m_offset = Vec3d::Zero(); } } +#endif // ENABLE_WORLD_COORDINATE double GLGizmoScale3D::calc_ratio(const UpdateData& data) const { double ratio = 0.0; - Vec3d pivot = (m_starting.ctrl_down && m_hover_id < 6) ? m_starting.pivots[m_hover_id] : m_starting.box.center(); +#if ENABLE_WORLD_COORDINATE + const Vec3d starting_vec = m_starting.drag_position - m_starting.center; +#else + const Vec3d pivot = (m_starting.ctrl_down && m_hover_id < 6) ? m_starting.pivots[m_hover_id] : m_starting.box.center(); + const Vec3d starting_vec = m_starting.drag_position - pivot; +#endif // ENABLE_WORLD_COORDINATE + + const double len_starting_vec = starting_vec.norm(); - Vec3d starting_vec = m_starting.drag_position - pivot; - double len_starting_vec = starting_vec.norm(); if (len_starting_vec != 0.0) { - Vec3d mouse_dir = data.mouse_ray.unit_vector(); + const Vec3d mouse_dir = data.mouse_ray.unit_vector(); // finds the intersection of the mouse ray with the plane parallel to the camera viewport and passing throught the starting position // use ray-plane intersection see i.e. https://en.wikipedia.org/wiki/Line%E2%80%93plane_intersection algebric form // in our case plane normal and ray direction are the same (orthogonal view) // when moving to perspective camera the negative z unit axis of the camera needs to be transformed in world space and used as plane normal - Vec3d inters = data.mouse_ray.a + (m_starting.drag_position - data.mouse_ray.a).dot(mouse_dir) / mouse_dir.squaredNorm() * mouse_dir; + const Vec3d inters = data.mouse_ray.a + (m_starting.drag_position - data.mouse_ray.a).dot(mouse_dir) / mouse_dir.squaredNorm() * mouse_dir; // vector from the starting position to the found intersection - Vec3d inters_vec = inters - m_starting.drag_position; + const Vec3d inters_vec = inters - m_starting.drag_position; // finds projection of the vector along the staring direction - double proj = inters_vec.dot(starting_vec.normalized()); + const double proj = inters_vec.dot(starting_vec.normalized()); ratio = (len_starting_vec + proj) / len_starting_vec; } @@ -580,5 +849,34 @@ double GLGizmoScale3D::calc_ratio(const UpdateData& data) const return ratio; } +#if ENABLE_WORLD_COORDINATE +#if ENABLE_GL_SHADERS_ATTRIBUTES +Transform3d GLGizmoScale3D::local_transform(const Selection& selection) const +{ + Transform3d ret = Geometry::assemble_transform(m_center); + if (!wxGetApp().obj_manipul()->is_world_coordinates()) { + const GLVolume& v = *selection.get_first_volume(); + Transform3d orient_matrix = v.get_instance_transformation().get_rotation_matrix(); + if (selection.is_single_volume_or_modifier() && wxGetApp().obj_manipul()->is_local_coordinates()) + orient_matrix = orient_matrix * v.get_volume_transformation().get_rotation_matrix(); + ret = ret * orient_matrix; + } + return ret; +} +#else +void GLGizmoScale3D::transform_to_local(const Selection& selection) const +{ + glsafe(::glTranslated(m_center.x(), m_center.y(), m_center.z())); + + if (!wxGetApp().obj_manipul()->is_world_coordinates()) { + Transform3d orient_matrix = selection.get_first_volume()->get_instance_transformation().get_matrix(true, false, true, true); + if (selection.is_single_volume_or_modifier() && wxGetApp().obj_manipul()->is_local_coordinates()) + orient_matrix = orient_matrix * selection.get_first_volume()->get_volume_transformation().get_matrix(true, false, true, true); + glsafe(::glMultMatrixd(orient_matrix.data())); + } +} +#endif // ENABLE_GL_SHADERS_ATTRIBUTES +#endif // ENABLE_WORLD_COORDINATE + } // namespace GUI } // namespace Slic3r diff --git a/src/slic3r/GUI/Gizmos/GLGizmoScale.hpp b/src/slic3r/GUI/Gizmos/GLGizmoScale.hpp index f4efe052a..fb4ff09b6 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoScale.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoScale.hpp @@ -3,31 +3,46 @@ #include "GLGizmoBase.hpp" +#if !ENABLE_WORLD_COORDINATE #include "libslic3r/BoundingBox.hpp" - +#endif // !ENABLE_WORLD_COORDINATE namespace Slic3r { namespace GUI { +#if ENABLE_WORLD_COORDINATE +class Selection; +#endif // ENABLE_WORLD_COORDINATE + class GLGizmoScale3D : public GLGizmoBase { - static const float Offset; + static const double Offset; struct StartingData { - Vec3d scale; - Vec3d drag_position; + bool ctrl_down{ false }; + Vec3d scale{ Vec3d::Ones() }; + Vec3d drag_position{ Vec3d::Zero() }; +#if ENABLE_WORLD_COORDINATE + Vec3d center{ Vec3d::Zero() }; + Vec3d instance_center{ Vec3d::Zero() }; +#endif // ENABLE_WORLD_COORDINATE BoundingBoxf3 box; - Vec3d pivots[6]; - bool ctrl_down; - - StartingData() : scale(Vec3d::Ones()), drag_position(Vec3d::Zero()), ctrl_down(false) { for (int i = 0; i < 5; ++i) { pivots[i] = Vec3d::Zero(); } } +#if !ENABLE_WORLD_COORDINATE + std::array pivots{ Vec3d::Zero(), Vec3d::Zero(), Vec3d::Zero(), Vec3d::Zero(), Vec3d::Zero(), Vec3d::Zero() }; +#endif // !ENABLE_WORLD_COORDINATE }; - mutable BoundingBoxf3 m_box; - mutable Transform3d m_transform; + BoundingBoxf3 m_bounding_box; +#if ENABLE_WORLD_COORDINATE + Transform3d m_grabbers_transform; + Vec3d m_center{ Vec3d::Zero() }; + Vec3d m_instance_center{ Vec3d::Zero() }; +#else + Transform3d m_transform; // Transforms grabbers offsets to the proper reference system (world for instances, instance for volumes) - mutable Transform3d m_offsets_transform; + Transform3d m_offsets_transform; +#endif // ENABLE_WORLD_COORDINATE Vec3d m_scale{ Vec3d::Ones() }; Vec3d m_offset{ Vec3d::Zero() }; double m_snap_step{ 0.05 }; @@ -54,7 +69,11 @@ public: void set_snap_step(double step) { m_snap_step = step; } const Vec3d& get_scale() const { return m_scale; } +#if ENABLE_WORLD_COORDINATE + void set_scale(const Vec3d& scale) { m_starting.scale = scale; m_scale = scale; m_offset = Vec3d::Zero(); } +#else void set_scale(const Vec3d& scale) { m_starting.scale = scale; m_scale = scale; } +#endif // ENABLE_WORLD_COORDINATE std::string get_tooltip() const override; @@ -87,6 +106,13 @@ private: void do_scale_uniform(const UpdateData& data); double calc_ratio(const UpdateData& data) const; +#if ENABLE_WORLD_COORDINATE +#if ENABLE_GL_SHADERS_ATTRIBUTES + Transform3d local_transform(const Selection& selection) const; +#else + void transform_to_local(const Selection& selection) const; +#endif // ENABLE_GL_SHADERS_ATTRIBUTES +#endif // ENABLE_WORLD_COORDINATE }; diff --git a/src/slic3r/GUI/Gizmos/GLGizmoSimplify.cpp b/src/slic3r/GUI/Gizmos/GLGizmoSimplify.cpp index 00bfd8ceb..43169df10 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoSimplify.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoSimplify.cpp @@ -147,7 +147,7 @@ void GLGizmoSimplify::add_simplify_suggestion_notification( for (size_t object_id : big_ids) { std::string t = GUI::format(_L( "Processing model '%1%' with more than 1M triangles " - "could be slow. It is highly recommend to reduce " + "could be slow. It is highly recommended to reduce " "amount of triangles."), objects[object_id]->name); std::string hypertext = _u8L("Simplify model"); diff --git a/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.cpp b/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.cpp index 655263aa1..bb6233469 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.cpp @@ -146,9 +146,13 @@ void GLGizmoSlaSupports::render_points(const Selection& selection, bool picking) }); #endif // ENABLE_LEGACY_OPENGL_REMOVAL - const GLVolume* vol = selection.get_volume(*selection.get_volume_idxs().begin()); + const GLVolume* vol = selection.get_first_volume(); Geometry::Transformation transformation(vol->get_instance_transformation().get_matrix() * vol->get_volume_transformation().get_matrix()); +#if ENABLE_WORLD_COORDINATE + const Transform3d instance_scaling_matrix_inverse = transformation.get_scaling_factor_matrix().inverse(); +#else const Transform3d& instance_scaling_matrix_inverse = transformation.get_matrix(true, true, false, true).inverse(); +#endif // ENABLE_WORLD_COORDINATE #if ENABLE_GL_SHADERS_ATTRIBUTES const Transform3d instance_matrix = Geometry::assemble_transform(m_c->selection_info()->get_sla_shift() * Vec3d::UnitZ()) * transformation.get_matrix(); const Camera& camera = wxGetApp().plater()->get_camera(); @@ -357,7 +361,7 @@ bool GLGizmoSlaSupports::unproject_on_mesh(const Vec2d& mouse_pos, std::pairget_camera(); const Selection& selection = m_parent.get_selection(); - const GLVolume* volume = selection.get_volume(*selection.get_volume_idxs().begin()); + const GLVolume* volume = selection.get_first_volume(); Geometry::Transformation trafo = volume->get_instance_transformation() * volume->get_volume_transformation(); trafo.set_offset(trafo.get_offset() + Vec3d(0., 0., m_c->selection_info()->get_sla_shift())); diff --git a/src/slic3r/GUI/Gizmos/GLGizmosCommon.cpp b/src/slic3r/GUI/Gizmos/GLGizmosCommon.cpp index c28fe6356..be35f40a0 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmosCommon.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmosCommon.cpp @@ -119,7 +119,7 @@ void SelectionInfo::on_update() const Selection& selection = get_pool()->get_canvas()->get_selection(); if (selection.is_single_full_instance()) { m_model_object = selection.get_model()->objects[selection.get_object_idx()]; - m_z_shift = selection.get_volume(*selection.get_volume_idxs().begin())->get_sla_shift_z(); + m_z_shift = selection.get_first_volume()->get_sla_shift_z(); } else m_model_object = nullptr; diff --git a/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp b/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp index 1955f0f4b..09276ef57 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp @@ -410,7 +410,8 @@ bool GLGizmosManager::gizmos_toolbar_on_mouse(const wxMouseEvent &mouse_event) { // at this moment is enebled to process mouse move under gizmo // tools bar e.g. Do not interupt dragging. return false; - } else if (mc.exist_tooltip) { + } + else if (mc.exist_tooltip) { // first move out of gizmo tool bar - unselect tooltip mc.exist_tooltip = false; update_hover_state(Undefined); @@ -425,10 +426,12 @@ bool GLGizmosManager::gizmos_toolbar_on_mouse(const wxMouseEvent &mouse_event) { mc.left = true; open_gizmo(gizmo); return true; - } else if (mouse_event.RightDown()) { + } + else if (mouse_event.RightDown()) { mc.right = true; return true; - } else if (mouse_event.MiddleDown()) { + } + else if (mouse_event.MiddleDown()) { mc.middle = true; return true; } @@ -443,14 +446,17 @@ bool GLGizmosManager::gizmos_toolbar_on_mouse(const wxMouseEvent &mouse_event) { update_hover_state(Undefined); } // draging start on toolbar so no propagation into scene - return true; - } else if (mc.left && mouse_event.LeftUp()) { + return true; + } + else if (mc.left && mouse_event.LeftUp()) { mc.left = false; return true; - } else if (mc.right && mouse_event.RightUp()) { + } + else if (mc.right && mouse_event.RightUp()) { mc.right = false; return true; - } else if (mc.middle && mouse_event.MiddleUp()) { + } + else if (mc.middle && mouse_event.MiddleUp()) { mc.middle = false; return true; } diff --git a/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp b/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp index 187afd889..2e9e6bb65 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp @@ -205,7 +205,7 @@ public: bool handle_shortcut(int key); bool is_dragging() const; - + ClippingPlane get_clipping_plane() const; bool wants_reslice_supports_on_undo() const; diff --git a/src/slic3r/GUI/ImGuiWrapper.cpp b/src/slic3r/GUI/ImGuiWrapper.cpp index ff084b0a4..8c27db56e 100644 --- a/src/slic3r/GUI/ImGuiWrapper.cpp +++ b/src/slic3r/GUI/ImGuiWrapper.cpp @@ -37,9 +37,8 @@ #endif // ENABLE_GL_IMGUI_SHADERS #include "../Utils/MacDarkMode.hpp" - -#include "nanosvg/nanosvg.h" -#include "nanosvg/nanosvgrast.h" +#include +#include namespace Slic3r { namespace GUI { diff --git a/src/slic3r/GUI/Jobs/PlaterWorker.hpp b/src/slic3r/GUI/Jobs/PlaterWorker.hpp index 573590272..37b18b3b8 100644 --- a/src/slic3r/GUI/Jobs/PlaterWorker.hpp +++ b/src/slic3r/GUI/Jobs/PlaterWorker.hpp @@ -38,16 +38,18 @@ class PlaterWorker: public Worker { void update_status(int st, const std::string &msg = "") override { - wxWakeUpIdle(); ctl.update_status(st, msg); + wxWakeUpIdle(); } bool was_canceled() const override { return ctl.was_canceled(); } std::future call_on_main_thread(std::function fn) override { + auto ftr = ctl.call_on_main_thread(std::move(fn)); wxWakeUpIdle(); - return ctl.call_on_main_thread(std::move(fn)); + + return ftr; } } wctl{c}; diff --git a/src/slic3r/GUI/Jobs/RotoptimizeJob.cpp b/src/slic3r/GUI/Jobs/RotoptimizeJob.cpp index e88d24fcd..b277aa206 100644 --- a/src/slic3r/GUI/Jobs/RotoptimizeJob.cpp +++ b/src/slic3r/GUI/Jobs/RotoptimizeJob.cpp @@ -101,16 +101,18 @@ void RotoptimizeJob::finalize(bool canceled, std::exception_ptr &eptr) auto trmatrix = oi->get_transformation().get_matrix(); Polygon trchull = o->convex_hull_2d(trmatrix); - - MinAreaBoundigBox rotbb(trchull, MinAreaBoundigBox::pcConvex); - double phi = rotbb.angle_to_X(); - - // The box should be landscape - if(rotbb.width() < rotbb.height()) phi += PI / 2; - - Vec3d rt = oi->get_rotation(); rt(Z) += phi; - - oi->set_rotation(rt); + + if (!trchull.empty()) { + MinAreaBoundigBox rotbb(trchull, MinAreaBoundigBox::pcConvex); + double phi = rotbb.angle_to_X(); + + // The box should be landscape + if(rotbb.width() < rotbb.height()) phi += PI / 2; + + Vec3d rt = oi->get_rotation(); rt(Z) += phi; + + oi->set_rotation(rt); + } } // Correct the z offset of the object which was corrupted be diff --git a/src/slic3r/GUI/Jobs/UIThreadWorker.hpp b/src/slic3r/GUI/Jobs/UIThreadWorker.hpp new file mode 100644 index 000000000..91213c239 --- /dev/null +++ b/src/slic3r/GUI/Jobs/UIThreadWorker.hpp @@ -0,0 +1,133 @@ +#ifndef UITHREADWORKER_HPP +#define UITHREADWORKER_HPP + +#include +#include + +#include "Worker.hpp" +#include "ProgressIndicator.hpp" + +namespace Slic3r { namespace GUI { + +// Implementation of a worker which does not create any additional threads. +class UIThreadWorker : public Worker, private Job::Ctl { + std::queue, std::deque>> m_jobqueue; + std::shared_ptr m_progress; + bool m_running = false; + bool m_canceled = false; + + void process_front() + { + std::unique_ptr job; + + if (!m_jobqueue.empty()) { + job = std::move(m_jobqueue.front()); + m_jobqueue.pop(); + } + + if (job) { + std::exception_ptr eptr; + m_running = true; + + try { + job->process(*this); + } catch (...) { + eptr= std::current_exception(); + } + + job->finalize(m_canceled, eptr); + + // Unhandled exceptions are rethrown without mercy. + if (eptr) + std::rethrow_exception(eptr); + + m_running = false; + + m_canceled = false; + } + } + +protected: + // Implement Job::Ctl interface: + + void update_status(int st, const std::string &msg = "") override + { + if (m_progress) { + m_progress->set_progress(st); + m_progress->set_status_text(msg.c_str()); + } + } + + bool was_canceled() const override { return m_canceled; } + + std::future call_on_main_thread(std::function fn) override + { + std::future ftr = std::async(std::launch::deferred, [fn]{ fn(); }); + + // So, it seems that the destructor of std::future will not call the + // packaged function. The future needs to be accessed at least ones + // or waited upon. Calling wait() instead of get() will keep the + // returned future's state valid. + ftr.wait(); + + return ftr; + } + +public: + explicit UIThreadWorker(std::shared_ptr pri, + const std::string & /*name*/ = "") + : m_progress{pri} + { + if (m_progress) + m_progress->set_cancel_callback([this]() { cancel(); }); + } + + UIThreadWorker() = default; + + bool push(std::unique_ptr job) override + { + m_canceled = false; + + if (job) + m_jobqueue.push(std::move(job)); + + return bool(job); + } + + bool is_idle() const override { return !m_running && m_jobqueue.empty(); } + + void cancel() override { m_canceled = true; } + + void cancel_all() override + { + m_canceled = true; + process_front(); + + while (!m_jobqueue.empty()) + m_jobqueue.pop(); + } + + void process_events() override { + while (!m_jobqueue.empty()) + process_front(); + } + + bool wait_for_current_job(unsigned /*timeout_ms*/ = 0) override { + process_front(); + + return true; + } + + bool wait_for_idle(unsigned /*timeout_ms*/ = 0) override { + process_events(); + + return true; + } + + ProgressIndicator * get_pri() { return m_progress.get(); } + const ProgressIndicator * get_pri() const { return m_progress.get(); } +}; + +}} // namespace Slic3r::GUI + +#endif // UITHREADWORKER_HPP diff --git a/src/slic3r/GUI/KBShortcutsDialog.cpp b/src/slic3r/GUI/KBShortcutsDialog.cpp index ba3f6675f..bf4fe9dc7 100644 --- a/src/slic3r/GUI/KBShortcutsDialog.cpp +++ b/src/slic3r/GUI/KBShortcutsDialog.cpp @@ -57,8 +57,8 @@ KBShortcutsDialog::KBShortcutsDialog() void KBShortcutsDialog::on_dpi_changed(const wxRect& suggested_rect) { - m_logo_bmp.msw_rescale(); - m_header_bitmap->SetBitmap(m_logo_bmp.bmp()); + //m_logo_bmp.msw_rescale(); + //m_header_bitmap->SetBitmap(m_logo_bmp.bmp()); msw_buttons_rescale(this, em_unit(), { wxID_OK }); Layout(); @@ -270,8 +270,8 @@ wxPanel* KBShortcutsDialog::create_header(wxWindow* parent, const wxFont& bold_f sizer->AddStretchSpacer(); // logo - m_logo_bmp = ScalableBitmap(this, wxGetApp().logo_name(), 32); - m_header_bitmap = new wxStaticBitmap(panel, wxID_ANY, m_logo_bmp.bmp()); + m_header_bitmap = new wxStaticBitmap(panel, wxID_ANY, *get_bmp_bundle(wxGetApp().logo_name(), 32)); + sizer->Add(m_header_bitmap, 0, wxEXPAND | wxLEFT | wxRIGHT, 10); // text diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index cde7a05a2..bf1f67994 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -14,6 +14,7 @@ #include #include +#include #include #include "libslic3r/Print.hpp" @@ -637,7 +638,9 @@ void MainFrame::update_title() } } - std::string build_id = wxGetApp().is_editor() ? SLIC3R_BUILD_ID : GCODEVIEWER_BUILD_ID; + std::string build_id = SLIC3R_BUILD_ID; + if (! wxGetApp().is_editor()) + boost::replace_first(build_id, SLIC3R_APP_NAME, GCODEVIEWER_APP_NAME); size_t idx_plus = build_id.find('+'); if (idx_plus != build_id.npos) { // Parse what is behind the '+'. If there is a number, then it is a build number after the label, and full build ID is shown. @@ -1011,9 +1014,6 @@ void MainFrame::on_dpi_changed(const wxRect& suggested_rect) for (auto tab : wxGetApp().tabs_list) tab->msw_rescale(); - for (size_t id = 0; id < m_menubar->GetMenuCount(); id++) - msw_rescale_menu(m_menubar->GetMenu(id)); - // Workarounds for correct Window rendering after rescale /* Even if Window is maximized during moving, @@ -1048,7 +1048,7 @@ void MainFrame::on_sys_color_changed() #ifdef _MSW_DARK_MODE // update common mode sizer if (!wxGetApp().tabs_as_menu()) - dynamic_cast(m_tabpanel)->Rescale(); + dynamic_cast(m_tabpanel)->OnColorsChanged(); #endif #endif @@ -1605,9 +1605,9 @@ void MainFrame::update_menubar() m_changeable_menu_items[miSend] ->SetItemLabel((is_fff ? _L("S&end G-code") : _L("S&end to print")) + dots + "\tCtrl+Shift+G"); m_changeable_menu_items[miMaterialTab] ->SetItemLabel((is_fff ? _L("&Filament Settings Tab") : _L("Mate&rial Settings Tab")) + "\tCtrl+3"); - m_changeable_menu_items[miMaterialTab] ->SetBitmap(create_menu_bitmap(is_fff ? "spool" : "resin")); + m_changeable_menu_items[miMaterialTab] ->SetBitmap(*get_bmp_bundle(is_fff ? "spool" : "resin")); - m_changeable_menu_items[miPrinterTab] ->SetBitmap(create_menu_bitmap(is_fff ? "printer" : "sla_printer")); + m_changeable_menu_items[miPrinterTab] ->SetBitmap(*get_bmp_bundle(is_fff ? "printer" : "sla_printer")); } #if 0 diff --git a/src/slic3r/GUI/MeshUtils.cpp b/src/slic3r/GUI/MeshUtils.cpp index 171979406..3479c0fff 100644 --- a/src/slic3r/GUI/MeshUtils.cpp +++ b/src/slic3r/GUI/MeshUtils.cpp @@ -153,7 +153,11 @@ void MeshClipper::render_contour() void MeshClipper::recalculate_triangles() { +#if ENABLE_WORLD_COORDINATE + const Transform3f instance_matrix_no_translation_no_scaling = m_trafo.get_rotation_matrix().cast(); +#else const Transform3f& instance_matrix_no_translation_no_scaling = m_trafo.get_matrix(true,false,true).cast(); +#endif // ENABLE_WORLD_COORDINATE // Calculate clipping plane normal in mesh coordinates. const Vec3f up_noscale = instance_matrix_no_translation_no_scaling.inverse() * m_plane.get_normal().cast(); const Vec3d up = up_noscale.cast().cwiseProduct(m_trafo.get_scaling_factor()); @@ -441,7 +445,11 @@ std::vector MeshRaycaster::get_unobscured_idxs(const Geometry::Transfo { std::vector out; +#if ENABLE_WORLD_COORDINATE + const Transform3d instance_matrix_no_translation_no_scaling = trafo.get_rotation_matrix(); +#else const Transform3d& instance_matrix_no_translation_no_scaling = trafo.get_matrix(true,false,true); +#endif // ENABLE_WORLD_COORDINATE Vec3d direction_to_camera = -camera.get_dir_forward(); Vec3d direction_to_camera_mesh = (instance_matrix_no_translation_no_scaling.inverse() * direction_to_camera).normalized().eval(); direction_to_camera_mesh = direction_to_camera_mesh.cwiseProduct(trafo.get_scaling_factor()); diff --git a/src/slic3r/GUI/MsgDialog.cpp b/src/slic3r/GUI/MsgDialog.cpp index 94e9ca5f3..43e13841c 100644 --- a/src/slic3r/GUI/MsgDialog.cpp +++ b/src/slic3r/GUI/MsgDialog.cpp @@ -99,9 +99,10 @@ void MsgDialog::apply_style(long style) if (style & wxNO) add_button(wxID_NO, (style & wxNO_DEFAULT)); if (style & wxCANCEL) add_button(wxID_CANCEL, (style & wxCANCEL_DEFAULT)); - logo->SetBitmap( create_scaled_bitmap(style & wxICON_WARNING ? "exclamation" : - style & wxICON_INFORMATION ? "info" : - style & wxICON_QUESTION ? "question" : "PrusaSlicer", this, 64, style & wxICON_ERROR)); + std::string icon_name = style & wxICON_WARNING ? "exclamation" : + style & wxICON_INFORMATION ? "info" : + style & wxICON_QUESTION ? "question" : "PrusaSlicer"; + logo->SetBitmap(*get_bmp_bundle(icon_name, 64)); } void MsgDialog::finalize() @@ -230,7 +231,7 @@ ErrorDialog::ErrorDialog(wxWindow *parent, const wxString &msg, bool monospaced_ add_msg_content(this, content_sizer, msg, monospaced_font); // Use a small bitmap with monospaced font, as the error text will not be wrapped. - logo->SetBitmap(create_scaled_bitmap("PrusaSlicer_192px_grayscale.png", this, monospaced_font ? 48 : /*1*/84)); + logo->SetBitmap(*get_bmp_bundle("PrusaSlicer_192px_grayscale.png", monospaced_font ? 48 : /*1*/84)); SetMaxSize(wxSize(-1, CONTENT_MAX_HEIGHT*wxGetApp().em_unit())); diff --git a/src/slic3r/GUI/Notebook.cpp b/src/slic3r/GUI/Notebook.cpp index 9c5ccb834..380b402d5 100644 --- a/src/slic3r/GUI/Notebook.cpp +++ b/src/slic3r/GUI/Notebook.cpp @@ -95,10 +95,6 @@ void ButtonsListCtrl::UpdateMode() void ButtonsListCtrl::Rescale() { - m_mode_sizer->msw_rescale(); - for (ScalableButton* btn : m_pageButtons) - btn->msw_rescale(); - int em = em_unit(this); m_btn_margin = std::lround(0.3 * em); m_line_margin = std::lround(0.1 * em); @@ -108,6 +104,14 @@ void ButtonsListCtrl::Rescale() m_sizer->Layout(); } +void ButtonsListCtrl::OnColorsChanged() +{ + for (ScalableButton* btn : m_pageButtons) + btn->sys_color_changed(); + + m_sizer->Layout(); +} + void ButtonsListCtrl::SetSelection(int sel) { if (m_selection == sel) diff --git a/src/slic3r/GUI/Notebook.hpp b/src/slic3r/GUI/Notebook.hpp index af03a6a08..bd6c5d85a 100644 --- a/src/slic3r/GUI/Notebook.hpp +++ b/src/slic3r/GUI/Notebook.hpp @@ -21,6 +21,7 @@ public: void SetSelection(int sel); void UpdateMode(); void Rescale(); + void OnColorsChanged(); bool InsertPage(size_t n, const wxString& text, bool bSelect = false, const std::string& bmp_name = ""); void RemovePage(size_t n); bool SetPageImage(size_t n, const std::string& bmp_name) const; @@ -245,6 +246,11 @@ public: GetBtnsListCtrl()->Rescale(); } + void OnColorsChanged() + { + GetBtnsListCtrl()->OnColorsChanged(); + } + void OnNavigationKey(wxNavigationKeyEvent& event) { if (event.IsWindowChange()) { diff --git a/src/slic3r/GUI/OG_CustomCtrl.cpp b/src/slic3r/GUI/OG_CustomCtrl.cpp index 400db751a..2c367d62a 100644 --- a/src/slic3r/GUI/OG_CustomCtrl.cpp +++ b/src/slic3r/GUI/OG_CustomCtrl.cpp @@ -19,12 +19,12 @@ static bool is_point_in_rect(const wxPoint& pt, const wxRect& rect) rect.GetTop() <= pt.y && pt.y <= rect.GetBottom(); } -static wxSize get_bitmap_size(const wxBitmap& bmp) +static wxSize get_bitmap_size(const wxBitmapBundle* bmp, wxWindow* parent) { -#ifdef __APPLE__ - return bmp.GetScaledSize(); +#ifdef __WIN32__ + return bmp->GetBitmapFor(parent).GetSize(); #else - return bmp.GetSize(); + return bmp->GetDefaultSize(); #endif } @@ -45,8 +45,8 @@ OG_CustomCtrl::OG_CustomCtrl( wxWindow* parent, m_v_gap = lround(1.0 * m_em_unit); m_h_gap = lround(0.2 * m_em_unit); - m_bmp_mode_sz = get_bitmap_size(create_scaled_bitmap("mode_simple", this, wxOSX ? 10 : 12)); - m_bmp_blinking_sz = get_bitmap_size(create_scaled_bitmap("search_blink", this)); + m_bmp_mode_sz = get_bitmap_size(get_bmp_bundle("mode_simple", wxOSX ? 10 : 12), this); + m_bmp_blinking_sz = get_bitmap_size(get_bmp_bundle("search_blink"), this); init_ctrl_lines();// from og.lines() @@ -416,8 +416,8 @@ void OG_CustomCtrl::msw_rescale() m_v_gap = lround(1.0 * m_em_unit); m_h_gap = lround(0.2 * m_em_unit); - m_bmp_mode_sz = create_scaled_bitmap("mode_simple", this, wxOSX ? 10 : 12).GetSize(); - m_bmp_blinking_sz = create_scaled_bitmap("search_blink", this).GetSize(); + m_bmp_mode_sz = get_bitmap_size(get_bmp_bundle("mode_simple", wxOSX ? 10 : 12), this); + m_bmp_blinking_sz = get_bitmap_size(get_bmp_bundle("search_blink"), this); m_max_win_width = 0; @@ -497,7 +497,7 @@ void OG_CustomCtrl::CtrlLine::msw_rescale() { // if we have a single option with no label, no sidetext if (draw_just_act_buttons) - height = get_bitmap_size(create_scaled_bitmap("empty")).GetHeight(); + height = get_bitmap_size(get_bmp_bundle("empty"), ctrl).GetHeight(); if (ctrl->opt_group->label_width != 0 && !og_line.label.IsEmpty()) { wxSize label_sz = ctrl->GetTextExtent(og_line.label); @@ -666,13 +666,13 @@ wxCoord OG_CustomCtrl::CtrlLine::draw_mode_bmp(wxDC& dc, wxCoord v_pos) ConfigOptionMode mode = og_line.get_options()[0].opt.mode; const std::string& bmp_name = mode == ConfigOptionMode::comSimple ? "mode_simple" : mode == ConfigOptionMode::comAdvanced ? "mode_advanced" : "mode_expert"; - wxBitmap bmp = create_scaled_bitmap(bmp_name, ctrl, wxOSX ? 10 : 12); - wxCoord y_draw = v_pos + lround((height - get_bitmap_size(bmp).GetHeight()) / 2); + wxBitmapBundle* bmp = get_bmp_bundle(bmp_name, wxOSX ? 10 : 12); + wxCoord y_draw = v_pos + lround((height - get_bitmap_size(bmp, ctrl).GetHeight()) / 2); if (og_line.get_options().front().opt.gui_type != ConfigOptionDef::GUIType::legend) - dc.DrawBitmap(bmp, 0, y_draw); + dc.DrawBitmap(bmp->GetBitmapFor(ctrl), 0, y_draw); - return get_bitmap_size(bmp).GetWidth() + ctrl->m_h_gap; + return get_bitmap_size(bmp, ctrl).GetWidth() + ctrl->m_h_gap; } wxCoord OG_CustomCtrl::CtrlLine::draw_text(wxDC& dc, wxPoint pos, const wxString& text, const wxColour* color, int width, bool is_url/* = false*/) @@ -734,33 +734,33 @@ wxCoord OG_CustomCtrl::CtrlLine::draw_text(wxDC& dc, wxPoint pos, const wxStr wxPoint OG_CustomCtrl::CtrlLine::draw_blinking_bmp(wxDC& dc, wxPoint pos, bool is_blinking) { - wxBitmap bmp_blinking = create_scaled_bitmap(is_blinking ? "search_blink" : "empty", ctrl); + wxBitmapBundle* bmp_blinking = get_bmp_bundle(is_blinking ? "search_blink" : "empty"); wxCoord h_pos = pos.x; - wxCoord v_pos = pos.y + lround((height - get_bitmap_size(bmp_blinking).GetHeight()) / 2); + wxCoord v_pos = pos.y + lround((height - get_bitmap_size(bmp_blinking, ctrl).GetHeight()) / 2); - dc.DrawBitmap(bmp_blinking, h_pos, v_pos); + dc.DrawBitmap(bmp_blinking->GetBitmapFor(ctrl), h_pos, v_pos); - int bmp_dim = get_bitmap_size(bmp_blinking).GetWidth(); + int bmp_dim = get_bitmap_size(bmp_blinking, ctrl).GetWidth(); h_pos += bmp_dim + ctrl->m_h_gap; return wxPoint(h_pos, v_pos); } -wxCoord OG_CustomCtrl::CtrlLine::draw_act_bmps(wxDC& dc, wxPoint pos, const wxBitmap& bmp_undo_to_sys, const wxBitmap& bmp_undo, bool is_blinking, size_t rect_id) +wxCoord OG_CustomCtrl::CtrlLine::draw_act_bmps(wxDC& dc, wxPoint pos, const wxBitmapBundle& bmp_undo_to_sys, const wxBitmapBundle& bmp_undo, bool is_blinking, size_t rect_id) { pos = draw_blinking_bmp(dc, pos, is_blinking); wxCoord h_pos = pos.x; wxCoord v_pos = pos.y; - dc.DrawBitmap(bmp_undo_to_sys, h_pos, v_pos); + dc.DrawBitmap(bmp_undo_to_sys.GetBitmapFor(ctrl), h_pos, v_pos); - int bmp_dim = get_bitmap_size(bmp_undo_to_sys).GetWidth(); + int bmp_dim = get_bitmap_size(&bmp_undo_to_sys, ctrl).GetWidth(); rects_undo_to_sys_icon[rect_id] = wxRect(h_pos, v_pos, bmp_dim, bmp_dim); h_pos += bmp_dim + ctrl->m_h_gap; - dc.DrawBitmap(bmp_undo, h_pos, v_pos); + dc.DrawBitmap(bmp_undo.GetBitmapFor(ctrl), h_pos, v_pos); - bmp_dim = get_bitmap_size(bmp_undo).GetWidth(); + bmp_dim = get_bitmap_size(&bmp_undo, ctrl).GetWidth(); rects_undo_icon[rect_id] = wxRect(h_pos, v_pos, bmp_dim, bmp_dim); h_pos += bmp_dim + ctrl->m_h_gap; diff --git a/src/slic3r/GUI/OG_CustomCtrl.hpp b/src/slic3r/GUI/OG_CustomCtrl.hpp index c15132fec..0308322f7 100644 --- a/src/slic3r/GUI/OG_CustomCtrl.hpp +++ b/src/slic3r/GUI/OG_CustomCtrl.hpp @@ -63,7 +63,7 @@ class OG_CustomCtrl :public wxPanel wxCoord draw_mode_bmp(wxDC& dc, wxCoord v_pos); wxCoord draw_text (wxDC& dc, wxPoint pos, const wxString& text, const wxColour* color, int width, bool is_url = false); wxPoint draw_blinking_bmp(wxDC& dc, wxPoint pos, bool is_blinking); - wxCoord draw_act_bmps(wxDC& dc, wxPoint pos, const wxBitmap& bmp_undo_to_sys, const wxBitmap& bmp_undo, bool is_blinking, size_t rect_id = 0); + wxCoord draw_act_bmps(wxDC& dc, wxPoint pos, const wxBitmapBundle& bmp_undo_to_sys, const wxBitmapBundle& bmp_undo, bool is_blinking, size_t rect_id = 0); bool launch_browser() const; bool is_separator() const { return og_line.is_separator(); } diff --git a/src/slic3r/GUI/ObjectDataViewModel.cpp b/src/slic3r/GUI/ObjectDataViewModel.cpp index 9c5fac89e..23668ee99 100644 --- a/src/slic3r/GUI/ObjectDataViewModel.cpp +++ b/src/slic3r/GUI/ObjectDataViewModel.cpp @@ -58,6 +58,7 @@ const std::map INFO_ITEMS{ ObjectDataViewModelNode::ObjectDataViewModelNode(ObjectDataViewModelNode* parent, const wxString& sub_obj_name, Slic3r::ModelVolumeType type, + const wxBitmapBundle& bmp, const wxString& extruder, const int idx/* = -1*/) : m_parent(parent), @@ -99,7 +100,7 @@ ObjectDataViewModelNode::ObjectDataViewModelNode(ObjectDataViewModelNode* parent } else if (type == itLayerRoot) { - m_bmp = create_scaled_bitmap(LayerRootIcon); // FIXME: pass window ptr + m_bmp = *get_bmp_bundle(LayerRootIcon); m_name = _(L("Layers")); } else if (type == itInfo) @@ -130,7 +131,7 @@ ObjectDataViewModelNode::ObjectDataViewModelNode(ObjectDataViewModelNode* parent } const std::string label_range = (boost::format(" %.2f-%.2f ") % layer_range.first % layer_range.second).str(); m_name = _(L("Range")) + label_range + "(" + _(L("mm")) + ")"; - m_bmp = create_scaled_bitmap(LayerIcon); // FIXME: pass window ptr + m_bmp = *get_bmp_bundle(LayerIcon); set_action_and_extruder_icons(); init_container(); @@ -149,7 +150,7 @@ void ObjectDataViewModelNode::set_action_and_extruder_icons() { m_action_icon_name = m_type & itObject ? "advanced_plus" : m_type & (itVolume | itLayer) ? "cog" : /*m_type & itInstance*/ "set_separate_obj"; - m_action_icon = create_scaled_bitmap(m_action_icon_name); // FIXME: pass window ptr + m_action_icon = *get_bmp_bundle(m_action_icon_name); // set extruder bitmap set_extruder_icon(); @@ -168,7 +169,7 @@ void ObjectDataViewModelNode::set_printable_icon(PrintIndicator printable) { m_printable = printable; m_printable_icon = m_printable == piUndef ? m_empty_bmp : - create_scaled_bitmap(m_printable == piPrintable ? "eye_open.png" : "eye_closed.png"); + *get_bmp_bundle(m_printable == piPrintable ? "eye_open" : "eye_closed"); } void ObjectDataViewModelNode::update_settings_digest_bitmaps() @@ -176,14 +177,14 @@ void ObjectDataViewModelNode::update_settings_digest_bitmaps() m_bmp = m_empty_bmp; std::string scaled_bitmap_name = m_name.ToUTF8().data(); - scaled_bitmap_name += "-em" + std::to_string(wxGetApp().em_unit()) + (wxGetApp().dark_mode() ? "-dm" : ""); + scaled_bitmap_name += (wxGetApp().dark_mode() ? "-dm" : ""); - wxBitmap *bmp = m_bitmap_cache->find(scaled_bitmap_name); + wxBitmapBundle *bmp = m_bitmap_cache->find_bndl(scaled_bitmap_name); if (bmp == nullptr) { - std::vector bmps; + std::vector bmps; for (auto& category : m_opt_categories) - bmps.emplace_back(SettingsFactory::get_category_bitmap(category, false)); - bmp = m_bitmap_cache->insert(scaled_bitmap_name, bmps); + bmps.emplace_back(SettingsFactory::get_category_bitmap(category)); + bmp = m_bitmap_cache->insert_bndl(scaled_bitmap_name, bmps); } m_bmp = *bmp; @@ -207,13 +208,13 @@ bool ObjectDataViewModelNode::update_settings_digest(const std::vectorGetExtruder().IsEmpty()) @@ -1709,15 +1711,15 @@ wxDataViewItem ObjectDataViewModel::SetObjectPrintableState( return obj_item; } -void ObjectDataViewModel::Rescale() +void ObjectDataViewModel::UpdateBitmaps() { m_volume_bmps = MenuFactory::get_volume_bitmaps(); - m_warning_bmp = create_scaled_bitmap(WarningIcon); - m_warning_manifold_bmp = create_scaled_bitmap(WarningManifoldIcon); + m_warning_bmp = *get_bmp_bundle(WarningIcon); + m_warning_manifold_bmp = *get_bmp_bundle(WarningManifoldIcon); m_lock_bmp = create_scaled_bitmap(LockIcon); for (auto item : INFO_ITEMS) - m_info_bmps[item.first] = create_scaled_bitmap(item.second.bmp_name); + m_info_bmps[item.first] = get_bmp_bundle(item.second.bmp_name); wxDataViewItemArray all_items; GetAllChildren(wxDataViewItem(0), all_items); @@ -1728,7 +1730,7 @@ void ObjectDataViewModel::Rescale() continue; ObjectDataViewModelNode *node = static_cast(item.GetID()); - node->msw_rescale(); + node->sys_color_changed(); switch (node->m_type) { @@ -1737,11 +1739,11 @@ void ObjectDataViewModel::Rescale() UpdateBitmapForNode(node); break; case itLayerRoot: - node->m_bmp = create_scaled_bitmap(LayerRootIcon); + node->m_bmp = *get_bmp_bundle(LayerRootIcon); case itLayer: - node->m_bmp = create_scaled_bitmap(LayerIcon); + node->m_bmp = *get_bmp_bundle(LayerIcon); case itInfo: - node->m_bmp = m_info_bmps.at(node->m_info_item_type); + node->m_bmp = *m_info_bmps.at(node->m_info_item_type); break; default: break; } @@ -1749,7 +1751,28 @@ void ObjectDataViewModel::Rescale() ItemChanged(item); } } +/* +wxBitmapBundle ObjectDataViewModel::GetVolumeIcon(const Slic3r::ModelVolumeType vol_type, const std::string& warning_icon_name/* = std::string()*/) +{ + if (warning_icon_name.empty()) + return *m_volume_bmps[static_cast(vol_type)]; + std::string scaled_bitmap_name = warning_icon_name + std::to_string(static_cast(vol_type)); + scaled_bitmap_name += "-em" + std::to_string(wxGetApp().em_unit()) + (wxGetApp().dark_mode() ? "-dm" : "-lm"); + + wxBitmapBundle *bmp = m_bitmap_cache->find_bndl(scaled_bitmap_name); + if (bmp == nullptr) { + std::vector bmps; + + bmps.emplace_back(&GetWarningBitmap(warning_icon_name)); + bmps.emplace_back(m_volume_bmps[static_cast(vol_type)]); + + bmp = m_bitmap_cache->insert_bndl(scaled_bitmap_name, bmps); + } + + return *bmp; +} +*/ void ObjectDataViewModel::AddWarningIcon(const wxDataViewItem& item, const std::string& warning_icon_name) { if (!item.IsOk()) @@ -1781,6 +1804,10 @@ void ObjectDataViewModel::DeleteWarningIcon(const wxDataViewItem& item, const bo node->SetWarningIconName(std::string()); UpdateBitmapForNode(node); +/* if (node->GetType() & itVolume) { + node->SetWarningBitmap(*m_volume_bmps[static_cast(node->volume_type())], ""); + return; + }*/ if (unmark_object) { diff --git a/src/slic3r/GUI/ObjectDataViewModel.hpp b/src/slic3r/GUI/ObjectDataViewModel.hpp index f5c61a6d4..995fb1033 100644 --- a/src/slic3r/GUI/ObjectDataViewModel.hpp +++ b/src/slic3r/GUI/ObjectDataViewModel.hpp @@ -64,21 +64,21 @@ class ObjectDataViewModelNode { ObjectDataViewModelNode* m_parent; MyObjectTreeModelNodePtrArray m_children; - wxBitmap m_empty_bmp; + wxBitmapBundle m_empty_bmp; size_t m_volumes_cnt = 0; std::vector< std::string > m_opt_categories; t_layer_height_range m_layer_range = { 0.0f, 0.0f }; wxString m_name; - wxBitmap& m_bmp = m_empty_bmp; + wxBitmapBundle& m_bmp = m_empty_bmp; ItemType m_type; int m_idx = -1; bool m_container = false; wxString m_extruder = "default"; - wxBitmap m_extruder_bmp; - wxBitmap m_action_icon; + wxBitmapBundle m_extruder_bmp; + wxBitmapBundle m_action_icon; PrintIndicator m_printable {piUndef}; - wxBitmap m_printable_icon; + wxBitmapBundle m_printable_icon; std::string m_warning_icon_name{ "" }; bool m_has_lock{false}; @@ -101,6 +101,7 @@ public: ObjectDataViewModelNode(ObjectDataViewModelNode* parent, const wxString& sub_obj_name, Slic3r::ModelVolumeType type, + const wxBitmapBundle& bmp, const wxString& extruder, const int idx = -1 ); @@ -178,12 +179,12 @@ public: } bool SetValue(const wxVariant &variant, unsigned int col); - void SetVolumeType(ModelVolumeType type) { m_volume_type = type; } - void SetBitmap(const wxBitmap &icon) { m_bmp = icon; } - void SetExtruder(const wxString &extruder) { m_extruder = extruder; } - void SetWarningIconName(const std::string& warning_icon_name) { m_warning_icon_name = warning_icon_name; } + void SetVolumeType(ModelVolumeType type) { m_volume_type = type; } + void SetBitmap(const wxBitmapBundle &icon) { m_bmp = icon; } + void SetExtruder(const wxString &extruder) { m_extruder = extruder; } + void SetWarningBitmap(const wxBitmapBundle& icon, const std::string& warning_icon_name) { /*m_bmp = icon; */m_warning_icon_name = warning_icon_name; } void SetLock(bool has_lock) { m_has_lock = has_lock; } - const wxBitmap& GetBitmap() const { return m_bmp; } + const wxBitmapBundle& GetBitmap() const { return m_bmp; } const wxString& GetName() const { return m_name; } ItemType GetType() const { return m_type; } InfoItemType GetInfoItemType() const { return m_info_item_type; } @@ -233,7 +234,7 @@ public: void update_settings_digest_bitmaps(); bool update_settings_digest(const std::vector& categories); int volume_type() const { return int(m_volume_type); } - void msw_rescale(); + void sys_color_changed(); #ifndef NDEBUG bool valid(); @@ -258,11 +259,11 @@ wxDECLARE_EVENT(wxCUSTOMEVT_LAST_VOLUME_IS_DELETED, wxCommandEvent); class ObjectDataViewModel :public wxDataViewModel { std::vector m_objects; - std::vector m_volume_bmps; - std::map m_info_bmps; - wxBitmap m_empty_bmp; - wxBitmap m_warning_bmp; - wxBitmap m_warning_manifold_bmp; + std::vector m_volume_bmps; + std::map m_info_bmps; + wxBitmapBundle m_empty_bmp; + wxBitmapBundle m_warning_bmp; + wxBitmapBundle m_warning_manifold_bmp; wxBitmap m_lock_bmp; wxDataViewCtrl* m_ctrl { nullptr }; @@ -318,7 +319,7 @@ public: // helper method for wxLog wxString GetName(const wxDataViewItem &item) const; - wxBitmap& GetBitmap(const wxDataViewItem &item) const; + wxBitmapBundle& GetBitmap(const wxDataViewItem &item) const; wxString GetExtruder(const wxDataViewItem &item) const; int GetExtruderNumber(const wxDataViewItem &item) const; @@ -386,8 +387,10 @@ public: void SetAssociatedControl(wxDataViewCtrl* ctrl) { m_ctrl = ctrl; } // Rescale bitmaps for existing Items - void Rescale(); + void UpdateBitmaps(); + wxBitmapBundle GetVolumeIcon(const Slic3r::ModelVolumeType vol_type, + const std::string& warning_icon_name = std::string()); void AddWarningIcon(const wxDataViewItem& item, const std::string& warning_name); void DeleteWarningIcon(const wxDataViewItem& item, const bool unmark_object = false); void UpdateWarningIcon(const wxDataViewItem& item, const std::string& warning_name); @@ -405,6 +408,7 @@ private: wxDataViewItem AddInstanceRoot(const wxDataViewItem& parent_item); void AddAllChildren(const wxDataViewItem& parent); +// wxBitmapBundle& GetWarningBitmap(const std::string& warning_icon_name); void UpdateBitmapForNode(ObjectDataViewModelNode* node); void UpdateBitmapForNode(ObjectDataViewModelNode* node, const std::string& warning_icon_name, bool has_lock); }; diff --git a/src/slic3r/GUI/OptionsGroup.cpp b/src/slic3r/GUI/OptionsGroup.cpp index 257fe2532..6055a8e78 100644 --- a/src/slic3r/GUI/OptionsGroup.cpp +++ b/src/slic3r/GUI/OptionsGroup.cpp @@ -728,7 +728,6 @@ void ConfigOptionsGroup::msw_rescale() // check if window is ScalableButton ScalableButton* sc_btn = dynamic_cast(win); if (sc_btn) { - sc_btn->msw_rescale(); sc_btn->SetSize(sc_btn->GetBestSize()); return; } @@ -773,7 +772,7 @@ void ConfigOptionsGroup::sys_color_changed() wxWindow* win = item->GetWindow(); // check if window is ScalableButton if (ScalableButton* sc_btn = dynamic_cast(win)) { - sc_btn->msw_rescale(); + sc_btn->sys_color_changed(); return; } wxGetApp().UpdateDarkUI(win, dynamic_cast(win) != nullptr); diff --git a/src/slic3r/GUI/PhysicalPrinterDialog.cpp b/src/slic3r/GUI/PhysicalPrinterDialog.cpp index b2983f97f..04ee9d090 100644 --- a/src/slic3r/GUI/PhysicalPrinterDialog.cpp +++ b/src/slic3r/GUI/PhysicalPrinterDialog.cpp @@ -142,10 +142,10 @@ void PresetForPrinter::AllowDelete() m_presets_list->update(); } -void PresetForPrinter::msw_rescale() +void PresetForPrinter::on_sys_color_changed() { - m_presets_list->msw_rescale(); - m_delete_preset_btn->msw_rescale(); + m_presets_list->sys_color_changed(); + m_delete_preset_btn->sys_color_changed(); } @@ -603,19 +603,10 @@ void PhysicalPrinterDialog::on_dpi_changed(const wxRect& suggested_rect) { const int& em = em_unit(); - m_add_preset_btn->msw_rescale(); - m_printhost_browse_btn->msw_rescale(); - m_printhost_test_btn->msw_rescale(); - if (m_printhost_cafile_browse_btn) - m_printhost_cafile_browse_btn->msw_rescale(); - m_optgroup->msw_rescale(); msw_buttons_rescale(this, em, { wxID_OK, wxID_CANCEL }); - for (PresetForPrinter* preset : m_presets) - preset->msw_rescale(); - const wxSize& size = wxSize(45 * em, 35 * em); SetMinSize(size); @@ -623,6 +614,18 @@ void PhysicalPrinterDialog::on_dpi_changed(const wxRect& suggested_rect) Refresh(); } +void PhysicalPrinterDialog::on_sys_color_changed() +{ + m_add_preset_btn->sys_color_changed(); + m_printhost_browse_btn->sys_color_changed(); + m_printhost_test_btn->sys_color_changed(); + if (m_printhost_cafile_browse_btn) + m_printhost_cafile_browse_btn->sys_color_changed(); + + for (PresetForPrinter* preset : m_presets) + preset->on_sys_color_changed(); +} + void PhysicalPrinterDialog::OnOK(wxEvent& event) { wxString printer_name = m_printer_name->GetValue(); diff --git a/src/slic3r/GUI/PhysicalPrinterDialog.hpp b/src/slic3r/GUI/PhysicalPrinterDialog.hpp index cb9a48b3e..d8bb70d3c 100644 --- a/src/slic3r/GUI/PhysicalPrinterDialog.hpp +++ b/src/slic3r/GUI/PhysicalPrinterDialog.hpp @@ -48,8 +48,7 @@ public: void SuppressDelete(); void AllowDelete(); - void msw_rescale(); - void on_sys_color_changed() {}; + void on_sys_color_changed(); }; @@ -98,7 +97,7 @@ public: void DeletePreset(PresetForPrinter* preset_for_printer); protected: void on_dpi_changed(const wxRect& suggested_rect) override; - void on_sys_color_changed() override {}; + void on_sys_color_changed() override; bool had_all_mk3; }; diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 26c7da784..00089dde6 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -57,6 +57,9 @@ #include "GUI_ObjectManipulation.hpp" #include "GUI_ObjectLayers.hpp" #include "GUI_Utils.hpp" +#if ENABLE_WORLD_COORDINATE +#include "GUI_Geometry.hpp" +#endif // ENABLE_WORLD_COORDINATE #include "GUI_Factories.hpp" #include "wxExtensions.hpp" #include "MainFrame.hpp" @@ -177,7 +180,6 @@ public: bool showing_manifold_warning_icon; void show_sizer(bool show); - void msw_rescale(); void update_warning_icon(const std::string& warning_icon_name); }; @@ -207,7 +209,7 @@ ObjectInfo::ObjectInfo(wxWindow *parent) : init_info_label(&info_size, _L("Size")); - info_icon = new wxStaticBitmap(parent, wxID_ANY, create_scaled_bitmap("info")); + info_icon = new wxStaticBitmap(parent, wxID_ANY, *get_bmp_bundle("info")); info_icon->SetToolTip(_L("For a multipart object, this value isn't accurate.\n" "It doesn't take account of intersections and negative volumes.")); auto* volume_info_sizer = new wxBoxSizer(wxHORIZONTAL); @@ -220,7 +222,7 @@ ObjectInfo::ObjectInfo(wxWindow *parent) : info_manifold = new wxStaticText(parent, wxID_ANY, ""); info_manifold->SetFont(wxGetApp().small_font()); - manifold_warning_icon = new wxStaticBitmap(parent, wxID_ANY, create_scaled_bitmap(m_warning_icon_name)); + manifold_warning_icon = new wxStaticBitmap(parent, wxID_ANY, *get_bmp_bundle(m_warning_icon_name)); auto *sizer_manifold = new wxBoxSizer(wxHORIZONTAL); sizer_manifold->Add(manifold_warning_icon, 0, wxLEFT, 2); sizer_manifold->Add(info_manifold, 0, wxLEFT, 2); @@ -239,17 +241,11 @@ void ObjectInfo::show_sizer(bool show) manifold_warning_icon->Show(showing_manifold_warning_icon && show); } -void ObjectInfo::msw_rescale() -{ - manifold_warning_icon->SetBitmap(create_scaled_bitmap(m_warning_icon_name)); - info_icon->SetBitmap(create_scaled_bitmap("info")); -} - void ObjectInfo::update_warning_icon(const std::string& warning_icon_name) { if ((showing_manifold_warning_icon = !warning_icon_name.empty())) { m_warning_icon_name = warning_icon_name; - manifold_warning_icon->SetBitmap(create_scaled_bitmap(m_warning_icon_name)); + manifold_warning_icon->SetBitmap(*get_bmp_bundle(m_warning_icon_name)); } } @@ -347,9 +343,6 @@ void FreqChangedParams::msw_rescale() { m_og->msw_rescale(); m_og_sla->msw_rescale(); - - for (auto btn: m_empty_buttons) - btn->msw_rescale(); } void FreqChangedParams::sys_color_changed() @@ -358,7 +351,7 @@ void FreqChangedParams::sys_color_changed() m_og_sla->sys_color_changed(); for (auto btn: m_empty_buttons) - btn->msw_rescale(); + btn->sys_color_changed(); wxGetApp().UpdateDarkUI(m_wiping_dialog_button, true); } @@ -447,7 +440,7 @@ FreqChangedParams::FreqChangedParams(wxWindow* parent) : */ auto empty_widget = [this] (wxWindow* parent) { auto sizer = new wxBoxSizer(wxHORIZONTAL); - auto btn = new ScalableButton(parent, wxID_ANY, "mirroring_transparent.png", wxEmptyString, + auto btn = new ScalableButton(parent, wxID_ANY, "mirroring_transparent", wxEmptyString, wxDefaultSize, wxDefaultPosition, wxBU_EXACTFIT | wxNO_BORDER | wxTRANSPARENT_WINDOW); sizer->Add(btn, 0, wxALIGN_CENTER_VERTICAL | wxLEFT | wxRIGHT, int(0.3 * wxGetApp().em_unit())); m_empty_buttons.push_back(btn); @@ -505,7 +498,7 @@ FreqChangedParams::FreqChangedParams(wxWindow* parent) : } })); - auto btn = new ScalableButton(parent, wxID_ANY, "mirroring_transparent.png", wxEmptyString, + auto btn = new ScalableButton(parent, wxID_ANY, "mirroring_transparent", wxEmptyString, wxDefaultSize, wxDefaultPosition, wxBU_EXACTFIT | wxNO_BORDER | wxTRANSPARENT_WINDOW); sizer->Add(btn , 0, wxALIGN_CENTER_VERTICAL | wxLEFT | wxRIGHT, int(0.3 * wxGetApp().em_unit())); @@ -1100,9 +1093,6 @@ void Sidebar::msw_rescale() { SetMinSize(wxSize(40 * wxGetApp().em_unit(), -1)); - if (p->mode_sizer) - p->mode_sizer->msw_rescale(); - for (PlaterPresetComboBox* combo : std::vector { p->combo_print, p->combo_sla_print, p->combo_sla_material, @@ -1114,14 +1104,8 @@ void Sidebar::msw_rescale() p->frequently_changed_parameters->msw_rescale(); p->object_list->msw_rescale(); p->object_manipulation->msw_rescale(); - p->object_settings->msw_rescale(); p->object_layers->msw_rescale(); - p->object_info->msw_rescale(); - - p->btn_send_gcode->msw_rescale(); -// p->btn_eject_device->msw_rescale(); - p->btn_export_gcode_removable->msw_rescale(); #ifdef _WIN32 const int scaled_height = p->btn_export_gcode_removable->GetBitmapHeight(); #else @@ -1142,14 +1126,13 @@ void Sidebar::sys_color_changed() for (wxWindow* win : std::vector{ this, p->sliced_info->GetStaticBox(), p->object_info->GetStaticBox(), p->btn_reslice, p->btn_export_gcode }) wxGetApp().UpdateDarkUI(win); - p->object_info->msw_rescale(); for (wxWindow* win : std::vector{ p->scrolled, p->presets_panel }) wxGetApp().UpdateAllStaticTextDarkUI(win); for (wxWindow* btn : std::vector{ p->btn_reslice, p->btn_export_gcode }) wxGetApp().UpdateDarkUI(btn, true); if (p->mode_sizer) - p->mode_sizer->msw_rescale(); + p->mode_sizer->sys_color_changed(); p->frequently_changed_parameters->sys_color_changed(); p->object_settings->sys_color_changed(); #endif @@ -1167,11 +1150,12 @@ void Sidebar::sys_color_changed() p->object_layers->sys_color_changed(); // btn...->msw_rescale() updates icon on button, so use it - p->btn_send_gcode->msw_rescale(); + p->btn_send_gcode->sys_color_changed(); // p->btn_eject_device->msw_rescale(); - p->btn_export_gcode_removable->msw_rescale(); + p->btn_export_gcode_removable->sys_color_changed(); p->scrolled->Layout(); + p->scrolled->Refresh(); p->searcher.dlg_sys_color_changed(); } @@ -1514,6 +1498,11 @@ void Sidebar::update_mode() wxWindowUpdateLocker noUpdates(this); +#if ENABLE_WORLD_COORDINATE + if (m_mode == comSimple) + p->object_manipulation->set_coordinates_type(ECoordinatesType::World); +#endif // ENABLE_WORLD_COORDINATE + p->object_list->get_sizer()->Show(m_mode > comSimple); p->object_list->unselect_objects(); @@ -1578,12 +1567,15 @@ std::string& Sidebar::get_search_line() class PlaterDropTarget : public wxFileDropTarget { public: - PlaterDropTarget(Plater* plater) : m_plater(plater) { this->SetDefaultAction(wxDragCopy); } + PlaterDropTarget(MainFrame& mainframe, Plater& plater) : m_mainframe(mainframe), m_plater(plater) { + this->SetDefaultAction(wxDragCopy); + } virtual bool OnDropFiles(wxCoord x, wxCoord y, const wxArrayString &filenames); private: - Plater* m_plater; + MainFrame& m_mainframe; + Plater& m_plater; }; bool PlaterDropTarget::OnDropFiles(wxCoord x, wxCoord y, const wxArrayString &filenames) @@ -1593,8 +1585,11 @@ bool PlaterDropTarget::OnDropFiles(wxCoord x, wxCoord y, const wxArrayString &fi this->MSWUpdateDragImageOnLeave(); #endif // WIN32 - bool res = (m_plater != nullptr) ? m_plater->load_files(filenames) : false; - wxGetApp().mainframe->update_title(); + m_mainframe.Raise(); + m_mainframe.select_tab(size_t(0)); + m_plater.select_view_3D("3D"); + bool res = m_plater.load_files(filenames); + m_mainframe.update_title(); return res; } @@ -1648,6 +1643,9 @@ 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. + // + // UIThreadWorker can be used as a replacement for BoostThreadWorker if + // no additional worker threads are desired (useful for debugging or profiling) PlaterWorker m_worker; SLAImportDialog * m_sla_import_dlg; @@ -2074,6 +2072,9 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) view3D_canvas->Bind(EVT_GLCANVAS_WIPETOWER_MOVED, &priv::on_wipetower_moved, this); view3D_canvas->Bind(EVT_GLCANVAS_WIPETOWER_ROTATED, &priv::on_wipetower_rotated, this); view3D_canvas->Bind(EVT_GLCANVAS_INSTANCE_ROTATED, [this](SimpleEvent&) { update(); }); +#if ENABLE_WORLD_COORDINATE + view3D_canvas->Bind(EVT_GLCANVAS_RESET_SKEW, [this](SimpleEvent&) { update(); }); +#endif // ENABLE_WORLD_COORDINATE view3D_canvas->Bind(EVT_GLCANVAS_INSTANCE_SCALED, [this](SimpleEvent&) { update(); }); view3D_canvas->Bind(EVT_GLCANVAS_ENABLE_ACTION_BUTTONS, [this](Event& evt) { this->sidebar->enable_buttons(evt.data); }); view3D_canvas->Bind(EVT_GLCANVAS_UPDATE_GEOMETRY, &priv::on_update_geometry, this); @@ -2130,7 +2131,7 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) } // Drop target: - q->SetDropTarget(new PlaterDropTarget(q)); // if my understanding is right, wxWindow takes the owenership + main_frame->SetDropTarget(new PlaterDropTarget(*main_frame, *q)); // if my understanding is right, wxWindow takes the owenership q->Layout(); set_current_panel(wxGetApp().is_editor() ? static_cast(view3D) : static_cast(preview)); @@ -2920,7 +2921,7 @@ int Plater::priv::get_selected_volume_idx() const if ((0 > idx) || (idx > 1000)) #endif // ENABLE_WIPETOWER_OBJECTID_1000_REMOVAL return-1; - const GLVolume* v = selection.get_volume(*selection.get_volume_idxs().begin()); + const GLVolume* v = selection.get_first_volume(); if (model.objects[idx]->volumes.size() > 1) return v->volume_idx(); return -1; @@ -3499,7 +3500,11 @@ bool Plater::priv::replace_volume_with_stl(int object_idx, int volume_idx, const new_volume->set_type(old_volume->type()); new_volume->set_material_id(old_volume->material_id()); new_volume->set_transformation(old_volume->get_transformation()); +#if ENABLE_WORLD_COORDINATE + new_volume->translate(new_volume->get_transformation().get_matrix_no_offset() * (new_volume->source.mesh_offset - old_volume->source.mesh_offset)); +#else new_volume->translate(new_volume->get_transformation().get_matrix(true) * (new_volume->source.mesh_offset - old_volume->source.mesh_offset)); +#endif // ENABLE_WORLD_COORDINATE assert(!old_volume->source.is_converted_from_inches || !old_volume->source.is_converted_from_meters); if (old_volume->source.is_converted_from_inches) new_volume->convert_from_imperial_units(); @@ -3536,7 +3541,7 @@ void Plater::priv::replace_with_stl() if (selection.is_wipe_tower() || get_selection().get_volume_idxs().size() != 1) return; - const GLVolume* v = selection.get_volume(*selection.get_volume_idxs().begin()); + const GLVolume* v = selection.get_first_volume(); int object_idx = v->object_idx(); int volume_idx = v->volume_idx(); @@ -3854,10 +3859,16 @@ void Plater::priv::reload_from_disk() new_volume->config.apply(old_volume->config); new_volume->set_type(old_volume->type()); new_volume->set_material_id(old_volume->material_id()); +#if ENABLE_WORLD_COORDINATE + new_volume->set_transformation(Geometry::translation_transform(old_volume->source.transform.get_offset()) * + old_volume->get_transformation().get_matrix_no_offset() * old_volume->source.transform.get_matrix_no_offset()); + new_volume->translate(new_volume->get_transformation().get_matrix_no_offset() * (new_volume->source.mesh_offset - old_volume->source.mesh_offset)); +#else new_volume->set_transformation(Geometry::assemble_transform(old_volume->source.transform.get_offset()) * old_volume->get_transformation().get_matrix(true) * old_volume->source.transform.get_matrix(true)); new_volume->translate(new_volume->get_transformation().get_matrix(true) * (new_volume->source.mesh_offset - old_volume->source.mesh_offset)); +#endif // ENABLE_WORLD_COORDINATE new_volume->source.object_idx = old_volume->source.object_idx; new_volume->source.volume_idx = old_volume->source.volume_idx; assert(!old_volume->source.is_converted_from_inches || !old_volume->source.is_converted_from_meters); @@ -4463,8 +4474,12 @@ void Plater::priv::on_right_click(RBtnEvent& evt) const bool is_some_full_instances = selection.is_single_full_instance() || selection.is_single_full_object() || selection.is_multiple_full_instance(); +#if ENABLE_WORLD_COORDINATE + const bool is_part = selection.is_single_volume_or_modifier(); +#else const bool is_part = selection.is_single_volume() || selection.is_single_modifier(); - menu = is_some_full_instances ? menus.object_menu() : +#endif // ENABLE_WORLD_COORDINATE + menu = is_some_full_instances ? menus.object_menu() : is_part ? menus.part_menu() : menus.multi_selection_menu(); } } @@ -6079,7 +6094,7 @@ void Plater::export_stl_obj(bool extended, bool selection_only) if (selection.get_mode() == Selection::Instance) mesh = mesh_to_export(*model_object, (selection.is_single_full_object() && model_object->instances.size() > 1) ? -1 : selection.get_instance_idx()); else { - const GLVolume* volume = selection.get_volume(*selection.get_volume_idxs().begin()); + const GLVolume* volume = selection.get_first_volume(); mesh = model_object->volumes[volume->volume_idx()]->mesh(); mesh.transform(volume->get_volume_transformation().get_matrix(), true); } @@ -6989,8 +7004,6 @@ void Plater::msw_rescale() p->sidebar->msw_rescale(); - p->menus.msw_rescale(); - Layout(); GetParent()->Layout(); } diff --git a/src/slic3r/GUI/Preferences.cpp b/src/slic3r/GUI/Preferences.cpp index 660cfc7f0..27cd113fb 100644 --- a/src/slic3r/GUI/Preferences.cpp +++ b/src/slic3r/GUI/Preferences.cpp @@ -1,960 +1,964 @@ -#include "Preferences.hpp" -#include "OptionsGroup.hpp" -#include "GUI_App.hpp" -#include "Plater.hpp" -#include "MsgDialog.hpp" -#include "I18N.hpp" -#include "libslic3r/AppConfig.hpp" -#include -#include "Notebook.hpp" -#include "ButtonsDescription.hpp" -#include "OG_CustomCtrl.hpp" -#include "GLCanvas3D.hpp" - -namespace Slic3r { - - static t_config_enum_names enum_names_from_keys_map(const t_config_enum_values& enum_keys_map) - { - t_config_enum_names names; - int cnt = 0; - for (const auto& kvp : enum_keys_map) - cnt = std::max(cnt, kvp.second); - cnt += 1; - names.assign(cnt, ""); - for (const auto& kvp : enum_keys_map) - names[kvp.second] = kvp.first; - return names; - } - -#define CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(NAME) \ - static t_config_enum_names s_keys_names_##NAME = enum_names_from_keys_map(s_keys_map_##NAME); \ - template<> const t_config_enum_values& ConfigOptionEnum::get_enum_values() { return s_keys_map_##NAME; } \ - template<> const t_config_enum_names& ConfigOptionEnum::get_enum_names() { return s_keys_names_##NAME; } - - - - static const t_config_enum_values s_keys_map_NotifyReleaseMode = { - {"all", NotifyReleaseAll}, - {"release", NotifyReleaseOnly}, - {"none", NotifyReleaseNone}, - }; - - CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(NotifyReleaseMode) - -namespace GUI { - -PreferencesDialog::PreferencesDialog(wxWindow* parent) : - DPIDialog(parent, wxID_ANY, _L("Preferences"), wxDefaultPosition, - wxDefaultSize, wxDEFAULT_DIALOG_STYLE) -{ -#ifdef __WXOSX__ - isOSX = true; -#endif - build(); - - m_highlighter.set_timer_owner(this, 0); -} - -static void update_color(wxColourPickerCtrl* color_pckr, const wxColour& color) -{ - if (color_pckr->GetColour() != color) { - color_pckr->SetColour(color); - wxPostEvent(color_pckr, wxCommandEvent(wxEVT_COLOURPICKER_CHANGED)); - } -} - -void PreferencesDialog::show(const std::string& highlight_opt_key /*= std::string()*/, const std::string& tab_name/*= std::string()*/) -{ - int selected_tab = 0; - for ( ; selected_tab < int(tabs->GetPageCount()); selected_tab++) - if (tabs->GetPageText(selected_tab) == _(tab_name)) - break; - if (selected_tab < int(tabs->GetPageCount())) - tabs->SetSelection(selected_tab); - - if (!highlight_opt_key.empty()) - init_highlighter(highlight_opt_key); - - // cache input values for custom toolbar size - m_custom_toolbar_size = atoi(get_app_config()->get("custom_toolbar_size").c_str()); - m_use_custom_toolbar_size = get_app_config()->get("use_custom_toolbar_size") == "1"; - - // update colors for color pickers - update_color(m_sys_colour, wxGetApp().get_label_clr_sys()); - update_color(m_mod_colour, wxGetApp().get_label_clr_modified()); - - this->ShowModal(); -} - -static std::shared_ptrcreate_options_tab(const wxString& title, wxBookCtrlBase* tabs) -{ - wxPanel* tab = new wxPanel(tabs, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBK_LEFT | wxTAB_TRAVERSAL); - tabs->AddPage(tab, _(title)); - tab->SetFont(wxGetApp().normal_font()); - - wxBoxSizer* sizer = new wxBoxSizer(wxVERTICAL); - sizer->SetSizeHints(tab); - tab->SetSizer(sizer); - - std::shared_ptr optgroup = std::make_shared(tab); - optgroup->label_width = 40; - optgroup->set_config_category_and_type(title, int(Preset::TYPE_PREFERENCES)); - return optgroup; -} - -static void activate_options_tab(std::shared_ptr optgroup) -{ - optgroup->activate([](){}, wxALIGN_RIGHT); - optgroup->update_visibility(comSimple); - wxBoxSizer* sizer = static_cast(static_cast(optgroup->parent())->GetSizer()); - sizer->Add(optgroup->sizer, 0, wxEXPAND | wxALL, 10); - - // apply sercher - wxGetApp().sidebar().get_searcher().append_preferences_options(optgroup->get_lines()); -} - -static void append_bool_option( std::shared_ptr optgroup, - const std::string& opt_key, - const std::string& label, - const std::string& tooltip, - bool def_val, - ConfigOptionMode mode = comSimple) -{ - ConfigOptionDef def = {opt_key, coBool}; - def.label = label; - def.tooltip = tooltip; - def.mode = mode; - def.set_default_value(new ConfigOptionBool{ def_val }); - Option option(def, opt_key); - optgroup->append_single_option_line(option); - - // fill data to the Search Dialog - wxGetApp().sidebar().get_searcher().add_key(opt_key, Preset::TYPE_PREFERENCES, optgroup->config_category(), L("Preferences")); -} - -static void append_enum_option( std::shared_ptr optgroup, - const std::string& opt_key, - const std::string& label, - const std::string& tooltip, - const ConfigOption* def_val, - const t_config_enum_values *enum_keys_map, - std::initializer_list enum_values, - std::initializer_list enum_labels, - ConfigOptionMode mode = comSimple) -{ - ConfigOptionDef def = {opt_key, coEnum }; - def.label = label; - def.tooltip = tooltip; - def.mode = mode; - def.enum_keys_map = enum_keys_map; - def.enum_values = std::vector(enum_values); - def.enum_labels = std::vector(enum_labels); - - def.set_default_value(def_val); - Option option(def, opt_key); - optgroup->append_single_option_line(option); - - // fill data to the Search Dialog - wxGetApp().sidebar().get_searcher().add_key(opt_key, Preset::TYPE_PREFERENCES, optgroup->config_category(), L("Preferences")); -} - -static void append_preferences_option_to_searcer(std::shared_ptr optgroup, - const std::string& opt_key, - const wxString& label) -{ - // fill data to the Search Dialog - wxGetApp().sidebar().get_searcher().add_key(opt_key, Preset::TYPE_PREFERENCES, optgroup->config_category(), L("Preferences")); - // apply sercher - wxGetApp().sidebar().get_searcher().append_preferences_option(Line(opt_key, label, "")); -} - -void PreferencesDialog::build() -{ -#ifdef _WIN32 - wxGetApp().UpdateDarkUI(this); -#else - SetBackgroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); -#endif - const wxFont& font = wxGetApp().normal_font(); - SetFont(font); - - auto app_config = get_app_config(); - -#ifdef _MSW_DARK_MODE - tabs = new Notebook(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxNB_TOP | wxTAB_TRAVERSAL | wxNB_NOPAGETHEME | wxNB_DEFAULT); -#else - tabs = new wxNotebook(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxNB_TOP | wxTAB_TRAVERSAL |wxNB_NOPAGETHEME | wxNB_DEFAULT ); - tabs->SetBackgroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); -#endif - - // Add "General" tab - m_optgroup_general = create_options_tab(L("General"), tabs); - m_optgroup_general->m_on_change = [this](t_config_option_key opt_key, boost::any value) { - if (auto it = m_values.find(opt_key); it != m_values.end()) { - m_values.erase(it); // we shouldn't change value, if some of those parameters were selected, and then deselected - return; - } - if (opt_key == "default_action_on_close_application" || opt_key == "default_action_on_select_preset" || opt_key == "default_action_on_new_project") - m_values[opt_key] = boost::any_cast(value) ? "none" : "discard"; - else if (opt_key == "default_action_on_dirty_project") - m_values[opt_key] = boost::any_cast(value) ? "" : "0"; - else - m_values[opt_key] = boost::any_cast(value) ? "1" : "0"; - }; - - bool is_editor = wxGetApp().is_editor(); - - if (is_editor) { - append_bool_option(m_optgroup_general, "remember_output_path", - L("Remember output directory"), - L("If this is enabled, Slic3r will prompt the last output directory instead of the one containing the input files."), - app_config->has("remember_output_path") ? app_config->get("remember_output_path") == "1" : true); - - append_bool_option(m_optgroup_general, "autocenter", - L("Auto-center parts"), - L("If this is enabled, Slic3r will auto-center objects around the print bed center."), - app_config->get("autocenter") == "1"); - - append_bool_option(m_optgroup_general, "background_processing", - L("Background processing"), - L("If this is enabled, Slic3r will pre-process objects as soon " - "as they\'re loaded in order to save time when exporting G-code."), - app_config->get("background_processing") == "1"); - - m_optgroup_general->append_separator(); - - // Please keep in sync with ConfigWizard - append_bool_option(m_optgroup_general, "export_sources_full_pathnames", - L("Export sources full pathnames to 3mf and amf"), - L("If enabled, allows the Reload from disk command to automatically find and load the files when invoked."), - app_config->get("export_sources_full_pathnames") == "1"); - -#ifdef _WIN32 - // Please keep in sync with ConfigWizard - append_bool_option(m_optgroup_general, "associate_3mf", - L("Associate .3mf files to PrusaSlicer"), - L("If enabled, sets PrusaSlicer as default application to open .3mf files."), - app_config->get("associate_3mf") == "1"); - - append_bool_option(m_optgroup_general, "associate_stl", - L("Associate .stl files to PrusaSlicer"), - L("If enabled, sets PrusaSlicer as default application to open .stl files."), - app_config->get("associate_stl") == "1"); -#endif // _WIN32 - - m_optgroup_general->append_separator(); - - // Please keep in sync with ConfigWizard - append_bool_option(m_optgroup_general, "preset_update", - L("Update built-in Presets automatically"), - L("If enabled, Slic3r downloads updates of built-in system presets in the background. These updates are downloaded " - "into a separate temporary location. When a new preset version becomes available it is offered at application startup."), - app_config->get("preset_update") == "1"); - - append_bool_option(m_optgroup_general, "no_defaults", - L("Suppress \" - default - \" presets"), - L("Suppress \" - default - \" presets in the Print / Filament / Printer selections once there are any other valid presets available."), - app_config->get("no_defaults") == "1"); - - append_bool_option(m_optgroup_general, "show_incompatible_presets", - L("Show incompatible print and filament presets"), - L("When checked, the print and filament presets are shown in the preset editor " - "even if they are marked as incompatible with the active printer"), - app_config->get("show_incompatible_presets") == "1"); - - m_optgroup_general->append_separator(); - - append_bool_option(m_optgroup_general, "show_drop_project_dialog", - L("Show drop project dialog"), - L("When checked, whenever dragging and dropping a project file on the application, shows a dialog asking to select the action to take on the file to load."), - app_config->get("show_drop_project_dialog") == "1"); - - append_bool_option(m_optgroup_general, "single_instance", -#if __APPLE__ - L("Allow just a single PrusaSlicer instance"), - L("On OSX there is always only one instance of app running by default. However it is allowed to run multiple instances " - "of same app from the command line. In such case this settings will allow only one instance."), -#else - L("Allow just a single PrusaSlicer instance"), - L("If this is enabled, when starting PrusaSlicer and another instance of the same PrusaSlicer is already running, that instance will be reactivated instead."), -#endif - app_config->has("single_instance") ? app_config->get("single_instance") == "1" : false ); - - m_optgroup_general->append_separator(); - - append_bool_option(m_optgroup_general, "default_action_on_dirty_project", - L("Ask for unsaved changes in project"), - L("Always ask for unsaved changes in project, when: \n" - "- Closing PrusaSlicer,\n" - "- Loading or creating a new project"), - app_config->get("default_action_on_dirty_project").empty()); - - m_optgroup_general->append_separator(); - - append_bool_option(m_optgroup_general, "default_action_on_close_application", - L("Ask to save unsaved changes in presets when closing the application or when loading a new project"), - L("Always ask for unsaved changes in presets, when: \n" - "- Closing PrusaSlicer while some presets are modified,\n" - "- Loading a new project while some presets are modified"), - app_config->get("default_action_on_close_application") == "none"); - - append_bool_option(m_optgroup_general, "default_action_on_select_preset", - L("Ask for unsaved changes in presets when selecting new preset"), - L("Always ask for unsaved changes in presets when selecting new preset or resetting a preset"), - app_config->get("default_action_on_select_preset") == "none"); - - append_bool_option(m_optgroup_general, "default_action_on_new_project", - L("Ask for unsaved changes in presets when creating new project"), - L("Always ask for unsaved changes in presets when creating new project"), - app_config->get("default_action_on_new_project") == "none"); - } -#ifdef _WIN32 - else { - append_bool_option(m_optgroup_general, "associate_gcode", - L("Associate .gcode files to PrusaSlicer G-code Viewer"), - L("If enabled, sets PrusaSlicer G-code Viewer as default application to open .gcode files."), - app_config->get("associate_gcode") == "1"); - } -#endif // _WIN32 - -#if __APPLE__ - append_bool_option(m_optgroup_general, "use_retina_opengl", - L("Use Retina resolution for the 3D scene"), - L("If enabled, the 3D scene will be rendered in Retina resolution. " - "If you are experiencing 3D performance problems, disabling this option may help."), - app_config->get("use_retina_opengl") == "1"); -#endif - - m_optgroup_general->append_separator(); - - // Show/Hide splash screen - append_bool_option(m_optgroup_general, "show_splash_screen", - L("Show splash screen"), - L("Show splash screen"), - app_config->get("show_splash_screen") == "1"); - - append_bool_option(m_optgroup_general, "restore_win_position", - L("Restore window position on start"), - L("If enabled, PrusaSlicer will be open at the position it was closed"), - app_config->get("restore_win_position") == "1"); - - // Clear Undo / Redo stack on new project - append_bool_option(m_optgroup_general, "clear_undo_redo_stack_on_new_project", - L("Clear Undo / Redo stack on new project"), - L("Clear Undo / Redo stack on new project or when an existing project is loaded."), - app_config->get("clear_undo_redo_stack_on_new_project") == "1"); - -#if defined(_WIN32) || defined(__APPLE__) - append_bool_option(m_optgroup_general, "use_legacy_3DConnexion", - L("Enable support for legacy 3DConnexion devices"), - L("If enabled, the legacy 3DConnexion devices settings dialog is available by pressing CTRL+M"), - app_config->get("use_legacy_3DConnexion") == "1"); -#endif // _WIN32 || __APPLE__ - - activate_options_tab(m_optgroup_general); - - // Add "Camera" tab - m_optgroup_camera = create_options_tab(L("Camera"), tabs); - m_optgroup_camera->m_on_change = [this](t_config_option_key opt_key, boost::any value) { - if (auto it = m_values.find(opt_key);it != m_values.end()) { - m_values.erase(it); // we shouldn't change value, if some of those parameters were selected, and then deselected - return; - } - m_values[opt_key] = boost::any_cast(value) ? "1" : "0"; - }; - - append_bool_option(m_optgroup_camera, "use_perspective_camera", - L("Use perspective camera"), - L("If enabled, use perspective camera. If not enabled, use orthographic camera."), - app_config->get("use_perspective_camera") == "1"); - - append_bool_option(m_optgroup_camera, "use_free_camera", - L("Use free camera"), - L("If enabled, use free camera. If not enabled, use constrained camera."), - app_config->get("use_free_camera") == "1"); - - append_bool_option(m_optgroup_camera, "reverse_mouse_wheel_zoom", - L("Reverse direction of zoom with mouse wheel"), - L("If enabled, reverses the direction of zoom with mouse wheel"), - app_config->get("reverse_mouse_wheel_zoom") == "1"); - - activate_options_tab(m_optgroup_camera); - - // Add "GUI" tab - m_optgroup_gui = create_options_tab(L("GUI"), tabs); - m_optgroup_gui->m_on_change = [this](t_config_option_key opt_key, boost::any value) { - if (opt_key == "notify_release") { - int val_int = boost::any_cast(value); - for (const auto& item : s_keys_map_NotifyReleaseMode) { - if (item.second == val_int) { - m_values[opt_key] = item.first; - return; - } - } - } - if (opt_key == "use_custom_toolbar_size") { - m_icon_size_sizer->ShowItems(boost::any_cast(value)); - refresh_og(m_optgroup_gui); - get_app_config()->set("use_custom_toolbar_size", boost::any_cast(value) ? "1" : "0"); - get_app_config()->save(); - wxGetApp().plater()->get_current_canvas3D()->render(); - return; - } - if (opt_key == "tabs_as_menu") { - bool disable_new_layout = boost::any_cast(value); - m_rb_new_settings_layout_mode->Show(!disable_new_layout); - if (disable_new_layout && m_rb_new_settings_layout_mode->GetValue()) { - m_rb_new_settings_layout_mode->SetValue(false); - m_rb_old_settings_layout_mode->SetValue(true); - } - refresh_og(m_optgroup_gui); - } - - if (auto it = m_values.find(opt_key); it != m_values.end()) { - m_values.erase(it); // we shouldn't change value, if some of those parameters were selected, and then deselected - return; - } - - if (opt_key == "suppress_hyperlinks") - m_values[opt_key] = boost::any_cast(value) ? "1" : ""; - else - m_values[opt_key] = boost::any_cast(value) ? "1" : "0"; - }; - - append_bool_option(m_optgroup_gui, "seq_top_layer_only", - L("Sequential slider applied only to top layer"), - L("If enabled, changes made using the sequential slider, in preview, apply only to gcode top layer." - "If disabled, changes made using the sequential slider, in preview, apply to the whole gcode."), - app_config->get("seq_top_layer_only") == "1"); - - if (is_editor) { - append_bool_option(m_optgroup_gui, "show_collapse_button", - L("Show sidebar collapse/expand button"), - L("If enabled, the button for the collapse sidebar will be appeared in top right corner of the 3D Scene"), - app_config->get("show_collapse_button") == "1"); - - append_bool_option(m_optgroup_gui, "suppress_hyperlinks", - L("Suppress to open hyperlink in browser"), - L("If enabled, PrusaSlicer will not open a hyperlinks in your browser."), - //L("If enabled, the descriptions of configuration parameters in settings tabs wouldn't work as hyperlinks. " - // "If disabled, the descriptions of configuration parameters in settings tabs will work as hyperlinks."), - app_config->get("suppress_hyperlinks") == "1"); - - append_bool_option(m_optgroup_gui, "color_mapinulation_panel", - L("Use colors for axes values in Manipulation panel"), - L("If enabled, the axes names and axes values will be colorized according to the axes colors. " - "If disabled, old UI will be used."), - app_config->get("color_mapinulation_panel") == "1"); - - append_bool_option(m_optgroup_gui, "order_volumes", - L("Order object volumes by types"), - L("If enabled, volumes will be always ordered inside the object. Correct order is Model Part, Negative Volume, Modifier, Support Blocker and Support Enforcer. " - "If disabled, you can reorder Model Parts, Negative Volumes and Modifiers. But one of the model parts have to be on the first place."), - app_config->get("order_volumes") == "1"); - -#if ENABLE_SHOW_NON_MANIFOLD_EDGES - append_bool_option(m_optgroup_gui, "non_manifold_edges", - L("Show non-manifold edges"), - L("If enabled, shows non-manifold edges."), - app_config->get("non_manifold_edges") == "1"); -#endif // ENABLE_SHOW_NON_MANIFOLD_EDGES - -#ifdef _MSW_DARK_MODE - append_bool_option(m_optgroup_gui, "tabs_as_menu", - L("Set settings tabs as menu items (experimental)"), - L("If enabled, Settings Tabs will be placed as menu items. If disabled, old UI will be used."), - app_config->get("tabs_as_menu") == "1"); -#endif - - m_optgroup_gui->append_separator(); - - append_bool_option(m_optgroup_gui, "show_hints", - L("Show \"Tip of the day\" notification after start"), - L("If enabled, useful hints are displayed at startup."), - app_config->get("show_hints") == "1"); - - append_enum_option(m_optgroup_gui, "notify_release", - L("Notify about new releases"), - L("You will be notified about new release after startup acordingly: All = Regular release and alpha / beta releases. Release only = regular release."), - new ConfigOptionEnum(static_cast(s_keys_map_NotifyReleaseMode.at(app_config->get("notify_release")))), - &ConfigOptionEnum::get_enum_values(), - {"all", "release", "none"}, - {L("All"), L("Release only"), L("None")}); - - m_optgroup_gui->append_separator(); - - append_bool_option(m_optgroup_gui, "use_custom_toolbar_size", - L("Use custom size for toolbar icons"), - L("If enabled, you can change size of toolbar icons manually."), - app_config->get("use_custom_toolbar_size") == "1"); - } - - activate_options_tab(m_optgroup_gui); - - if (is_editor) { - // set Field for notify_release to its value to activate the object - boost::any val = s_keys_map_NotifyReleaseMode.at(app_config->get("notify_release")); - m_optgroup_gui->get_field("notify_release")->set_value(val, false); - - create_icon_size_slider(); - m_icon_size_sizer->ShowItems(app_config->get("use_custom_toolbar_size") == "1"); - - create_settings_mode_widget(); - create_settings_text_color_widget(); - -#if ENABLE_ENVIRONMENT_MAP - // Add "Render" tab - m_optgroup_render = create_options_tab(L("Render"), tabs); - m_optgroup_render->m_on_change = [this](t_config_option_key opt_key, boost::any value) { - if (auto it = m_values.find(opt_key); it != m_values.end()) { - m_values.erase(it); // we shouldn't change value, if some of those parameters were selected, and then deselected - return; - } - m_values[opt_key] = boost::any_cast(value) ? "1" : "0"; - }; - - append_bool_option(m_optgroup_render, "use_environment_map", - L("Use environment map"), - L("If enabled, renders object using the environment map."), - app_config->get("use_environment_map") == "1"); - - activate_options_tab(m_optgroup_render); -#endif // ENABLE_ENVIRONMENT_MAP - -#ifdef _WIN32 - // Add "Dark Mode" tab - m_optgroup_dark_mode = create_options_tab(_L("Dark mode (experimental)"), tabs); - m_optgroup_dark_mode->m_on_change = [this](t_config_option_key opt_key, boost::any value) { - if (auto it = m_values.find(opt_key); it != m_values.end()) { - m_values.erase(it); // we shouldn't change value, if some of those parameters were selected, and then deselected - return; - } - m_values[opt_key] = boost::any_cast(value) ? "1" : "0"; - }; - - append_bool_option(m_optgroup_dark_mode, "dark_color_mode", - L("Enable dark mode"), - L("If enabled, UI will use Dark mode colors. If disabled, old UI will be used."), - app_config->get("dark_color_mode") == "1"); - - if (wxPlatformInfo::Get().GetOSMajorVersion() >= 10) // Use system menu just for Window newer then Windows 10 - // Use menu with ownerdrawn items by default on systems older then Windows 10 - { - append_bool_option(m_optgroup_dark_mode, "sys_menu_enabled", - L("Use system menu for application"), - L("If enabled, application will use the standart Windows system menu,\n" - "but on some combination od display scales it can look ugly. If disabled, old UI will be used."), - app_config->get("sys_menu_enabled") == "1"); - } - - activate_options_tab(m_optgroup_dark_mode); -#endif //_WIN32 - } - - // update alignment of the controls for all tabs - update_ctrls_alignment(); - - auto sizer = new wxBoxSizer(wxVERTICAL); - sizer->Add(tabs, 1, wxEXPAND | wxTOP | wxLEFT | wxRIGHT, 5); - - auto buttons = CreateStdDialogButtonSizer(wxOK | wxCANCEL); - this->Bind(wxEVT_BUTTON, &PreferencesDialog::accept, this, wxID_OK); - this->Bind(wxEVT_BUTTON, &PreferencesDialog::revert, this, wxID_CANCEL); - - for (int id : {wxID_OK, wxID_CANCEL}) - wxGetApp().UpdateDarkUI(static_cast(FindWindowById(id, this))); - - sizer->Add(buttons, 0, wxALIGN_CENTER_HORIZONTAL | wxBOTTOM | wxTOP, 10); - - SetSizer(sizer); - sizer->SetSizeHints(this); - this->CenterOnParent(); -} - -std::vector PreferencesDialog::optgroups() -{ - std::vector out; - out.reserve(4); - for (ConfigOptionsGroup* opt : { m_optgroup_general.get(), m_optgroup_camera.get(), m_optgroup_gui.get() -#ifdef _WIN32 - , m_optgroup_dark_mode.get() -#endif // _WIN32 -#if ENABLE_ENVIRONMENT_MAP - , m_optgroup_render.get() -#endif // ENABLE_ENVIRONMENT_MAP - }) - if (opt) - out.emplace_back(opt); - return out; -} - -void PreferencesDialog::update_ctrls_alignment() -{ - int max_ctrl_width{ 0 }; - for (ConfigOptionsGroup* og : this->optgroups()) - if (int max = og->custom_ctrl->get_max_win_width(); - max_ctrl_width < max) - max_ctrl_width = max; - if (max_ctrl_width) - for (ConfigOptionsGroup* og : this->optgroups()) - og->custom_ctrl->set_max_win_width(max_ctrl_width); -} - -void PreferencesDialog::accept(wxEvent&) -{ - std::vector options_to_recreate_GUI = { "no_defaults", "tabs_as_menu", "sys_menu_enabled" }; - - for (const std::string& option : options_to_recreate_GUI) { - if (m_values.find(option) != m_values.end()) { - wxString title = wxGetApp().is_editor() ? wxString(SLIC3R_APP_NAME) : wxString(GCODEVIEWER_APP_NAME); - title += " - " + _L("Changes for the critical options"); - MessageDialog dialog(nullptr, - _L("Changing some options will trigger application restart.\n" - "You will lose the content of the plater.") + "\n\n" + - _L("Do you want to proceed?"), - title, - wxICON_QUESTION | wxYES | wxNO); - if (dialog.ShowModal() == wxID_YES) { - m_recreate_GUI = true; - } - else { - for (const std::string& option : options_to_recreate_GUI) - m_values.erase(option); - } - break; - } - } - - auto app_config = get_app_config(); - - m_seq_top_layer_only_changed = false; - if (auto it = m_values.find("seq_top_layer_only"); it != m_values.end()) - m_seq_top_layer_only_changed = app_config->get("seq_top_layer_only") != it->second; - - m_settings_layout_changed = false; - for (const std::string& key : { "old_settings_layout_mode", - "new_settings_layout_mode", - "dlg_settings_layout_mode" }) - { - auto it = m_values.find(key); - if (it != m_values.end() && app_config->get(key) != it->second) { - m_settings_layout_changed = true; - break; - } - } - -#if 0 //#ifdef _WIN32 // #ysDarkMSW - Allow it when we deside to support the sustem colors for application - if (m_values.find("always_dark_color_mode") != m_values.end()) - wxGetApp().force_sys_colors_update(); -#endif - - for (std::map::iterator it = m_values.begin(); it != m_values.end(); ++it) - app_config->set(it->first, it->second); - - app_config->save(); - if (wxGetApp().is_editor()) { - wxGetApp().set_label_clr_sys(m_sys_colour->GetColour()); - wxGetApp().set_label_clr_modified(m_mod_colour->GetColour()); - } - - EndModal(wxID_OK); - -#ifdef _WIN32 - if (m_values.find("dark_color_mode") != m_values.end()) - wxGetApp().force_colors_update(); -#ifdef _MSW_DARK_MODE - if (m_values.find("sys_menu_enabled") != m_values.end()) - wxGetApp().force_menu_update(); -#endif //_MSW_DARK_MODE -#endif // _WIN32 - - wxGetApp().update_ui_from_settings(); - clear_cache(); -} - -void PreferencesDialog::revert(wxEvent&) -{ - auto app_config = get_app_config(); - - bool save_app_config = false; - if (m_custom_toolbar_size != atoi(app_config->get("custom_toolbar_size").c_str())) { - app_config->set("custom_toolbar_size", (boost::format("%d") % m_custom_toolbar_size).str()); - m_icon_size_slider->SetValue(m_custom_toolbar_size); - save_app_config |= true; - } - if (m_use_custom_toolbar_size != (get_app_config()->get("use_custom_toolbar_size") == "1")) { - app_config->set("use_custom_toolbar_size", m_use_custom_toolbar_size ? "1" : "0"); - save_app_config |= true; - - m_optgroup_gui->set_value("use_custom_toolbar_size", m_use_custom_toolbar_size); - m_icon_size_sizer->ShowItems(m_use_custom_toolbar_size); - refresh_og(m_optgroup_gui); - } - if (save_app_config) - app_config->save(); - - - for (auto value : m_values) { - bool reverted = false; - const std::string& key = value.first; - - if (key == "default_action_on_dirty_project") { - m_optgroup_general->set_value(key, app_config->get(key).empty()); - continue; - } - if (key == "default_action_on_close_application" || key == "default_action_on_select_preset" || key == "default_action_on_new_project") { - m_optgroup_general->set_value(key, app_config->get(key) == "none"); - continue; - } - if (key == "notify_release") { - m_optgroup_gui->set_value(key, s_keys_map_NotifyReleaseMode.at(app_config->get(key))); - continue; - } - if (key == "old_settings_layout_mode") { - m_rb_old_settings_layout_mode->SetValue(app_config->get(key) == "1"); - continue; - } - if (key == "new_settings_layout_mode") { - m_rb_new_settings_layout_mode->SetValue(app_config->get(key) == "1"); - continue; - } - if (key == "dlg_settings_layout_mode") { - m_rb_dlg_settings_layout_mode->SetValue(app_config->get(key) == "1"); - continue; - } - - for (auto opt_group : { m_optgroup_general, m_optgroup_camera, m_optgroup_gui -#ifdef _WIN32 - , m_optgroup_dark_mode -#endif // _WIN32 -#if ENABLE_ENVIRONMENT_MAP - , m_optgroup_render -#endif // ENABLE_ENVIRONMENT_MAP - }) { - if (opt_group->set_value(key, app_config->get(key) == "1")) - break; - } - if (key == "tabs_as_menu") { - m_rb_new_settings_layout_mode->Show(app_config->get(key) != "1"); - refresh_og(m_optgroup_gui); - continue; - } - } - - clear_cache(); - EndModal(wxID_CANCEL); -} - -void PreferencesDialog::msw_rescale() -{ - for (ConfigOptionsGroup* og : this->optgroups()) - og->msw_rescale(); -#ifdef _WIN32 - m_optgroup_dark_mode->msw_rescale(); -#endif //_WIN32 -#if ENABLE_ENVIRONMENT_MAP - m_optgroup_render->msw_rescale(); -#endif // ENABLE_ENVIRONMENT_MAP - - msw_buttons_rescale(this, em_unit(), { wxID_OK, wxID_CANCEL }); - - layout(); -} - -void PreferencesDialog::on_sys_color_changed() -{ -#ifdef _WIN32 - wxGetApp().UpdateDlgDarkUI(this); -#endif -} - -void PreferencesDialog::layout() -{ - const int em = em_unit(); - - SetMinSize(wxSize(47 * em, 28 * em)); - Fit(); - - Refresh(); -} - -void PreferencesDialog::clear_cache() -{ - m_values.clear(); - m_custom_toolbar_size = -1; -} - -void PreferencesDialog::refresh_og(std::shared_ptr og) -{ - og->parent()->Layout(); - tabs->Layout(); - this->layout(); -} - -void PreferencesDialog::create_icon_size_slider() -{ - const auto app_config = get_app_config(); - - const int em = em_unit(); - - m_icon_size_sizer = new wxBoxSizer(wxHORIZONTAL); - - wxWindow* parent = m_optgroup_gui->parent(); - wxGetApp().UpdateDarkUI(parent); - - if (isOSX) - // For correct rendering of the slider and value label under OSX - // we should use system default background - parent->SetBackgroundStyle(wxBG_STYLE_ERASE); - - auto label = new wxStaticText(parent, wxID_ANY, _L("Icon size in a respect to the default size") + " (%) :"); - - m_icon_size_sizer->Add(label, 0, wxALIGN_CENTER_VERTICAL| wxRIGHT | (isOSX ? 0 : wxLEFT), em); - - const int def_val = atoi(app_config->get("custom_toolbar_size").c_str()); - - long style = wxSL_HORIZONTAL; - if (!isOSX) - style |= wxSL_LABELS | wxSL_AUTOTICKS; - - m_icon_size_slider = new wxSlider(parent, wxID_ANY, def_val, 30, 100, - wxDefaultPosition, wxDefaultSize, style); - - m_icon_size_slider->SetTickFreq(10); - m_icon_size_slider->SetPageSize(10); - m_icon_size_slider->SetToolTip(_L("Select toolbar icon size in respect to the default one.")); - - m_icon_size_sizer->Add(m_icon_size_slider, 1, wxEXPAND); - - wxStaticText* val_label{ nullptr }; - if (isOSX) { - val_label = new wxStaticText(parent, wxID_ANY, wxString::Format("%d", def_val)); - m_icon_size_sizer->Add(val_label, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, em); - } - - m_icon_size_slider->Bind(wxEVT_SLIDER, ([this, val_label, app_config](wxCommandEvent e) { - auto val = m_icon_size_slider->GetValue(); - - app_config->set("custom_toolbar_size", (boost::format("%d") % val).str()); - app_config->save(); - wxGetApp().plater()->get_current_canvas3D()->render(); - - if (val_label) - val_label->SetLabelText(wxString::Format("%d", val)); - }), m_icon_size_slider->GetId()); - - for (wxWindow* win : std::vector{ m_icon_size_slider, label, val_label }) { - if (!win) continue; - win->SetFont(wxGetApp().normal_font()); - - if (isOSX) continue; // under OSX we use wxBG_STYLE_ERASE - win->SetBackgroundStyle(wxBG_STYLE_PAINT); - } - - m_optgroup_gui->sizer->Add(m_icon_size_sizer, 0, wxEXPAND | wxALL, em); -} - -void PreferencesDialog::create_settings_mode_widget() -{ - wxWindow* parent = m_optgroup_gui->parent(); - wxGetApp().UpdateDarkUI(parent); - - wxString title = L("Layout Options"); - wxStaticBox* stb = new wxStaticBox(parent, wxID_ANY, _(title)); - wxGetApp().UpdateDarkUI(stb); - if (!wxOSX) stb->SetBackgroundStyle(wxBG_STYLE_PAINT); - stb->SetFont(wxGetApp().normal_font()); - - wxSizer* stb_sizer = new wxStaticBoxSizer(stb, wxVERTICAL); - - auto app_config = get_app_config(); - std::vector choices = { _L("Old regular layout with the tab bar"), - _L("New layout, access via settings button in the top menu"), - _L("Settings in non-modal window") }; - int id = -1; - auto add_radio = [this, parent, stb_sizer, choices](wxRadioButton** rb, int id, bool select) { - *rb = new wxRadioButton(parent, wxID_ANY, choices[id], wxDefaultPosition, wxDefaultSize, id == 0 ? wxRB_GROUP : 0); - stb_sizer->Add(*rb); - (*rb)->SetValue(select); - (*rb)->Bind(wxEVT_RADIOBUTTON, [this, id](wxCommandEvent&) { - m_values["old_settings_layout_mode"] = (id == 0) ? "1" : "0"; - m_values["new_settings_layout_mode"] = (id == 1) ? "1" : "0"; - m_values["dlg_settings_layout_mode"] = (id == 2) ? "1" : "0"; - }); - }; - - add_radio(&m_rb_old_settings_layout_mode, ++id, app_config->get("old_settings_layout_mode") == "1"); - add_radio(&m_rb_new_settings_layout_mode, ++id, app_config->get("new_settings_layout_mode") == "1"); - add_radio(&m_rb_dlg_settings_layout_mode, ++id, app_config->get("dlg_settings_layout_mode") == "1"); - -#ifdef _MSW_DARK_MODE - if (app_config->get("tabs_as_menu") == "1") { - m_rb_new_settings_layout_mode->Hide(); - if (m_rb_new_settings_layout_mode->GetValue()) { - m_rb_new_settings_layout_mode->SetValue(false); - m_rb_old_settings_layout_mode->SetValue(true); - } - } -#endif - - std::string opt_key = "settings_layout_mode"; - m_blinkers[opt_key] = new BlinkingBitmap(parent); - - auto sizer = new wxBoxSizer(wxHORIZONTAL); - sizer->Add(m_blinkers[opt_key], 0, wxRIGHT, 2); - sizer->Add(stb_sizer, 1, wxALIGN_CENTER_VERTICAL); - m_optgroup_gui->sizer->Add(sizer, 0, wxEXPAND | wxTOP, em_unit()); - - append_preferences_option_to_searcer(m_optgroup_gui, opt_key, title); -} - -void PreferencesDialog::create_settings_text_color_widget() -{ - wxWindow* parent = m_optgroup_gui->parent(); - - wxString title = L("Text colors"); - wxStaticBox* stb = new wxStaticBox(parent, wxID_ANY, _(title)); - wxGetApp().UpdateDarkUI(stb); - if (!wxOSX) stb->SetBackgroundStyle(wxBG_STYLE_PAINT); - - std::string opt_key = "text_colors"; - m_blinkers[opt_key] = new BlinkingBitmap(parent); - - wxSizer* stb_sizer = new wxStaticBoxSizer(stb, wxVERTICAL); - ButtonsDescription::FillSizerWithTextColorDescriptions(stb_sizer, parent, &m_sys_colour, &m_mod_colour); - - auto sizer = new wxBoxSizer(wxHORIZONTAL); - sizer->Add(m_blinkers[opt_key], 0, wxRIGHT, 2); - sizer->Add(stb_sizer, 1, wxALIGN_CENTER_VERTICAL); - - m_optgroup_gui->sizer->Add(sizer, 0, wxEXPAND | wxTOP, em_unit()); - - append_preferences_option_to_searcer(m_optgroup_gui, opt_key, title); -} - -void PreferencesDialog::init_highlighter(const t_config_option_key& opt_key) -{ - if (m_blinkers.find(opt_key) != m_blinkers.end()) - if (BlinkingBitmap* blinker = m_blinkers.at(opt_key); blinker) { - m_highlighter.init(blinker); - return; - } - - for (auto opt_group : { m_optgroup_general, m_optgroup_camera, m_optgroup_gui -#ifdef _WIN32 - , m_optgroup_dark_mode -#endif // _WIN32 -#if ENABLE_ENVIRONMENT_MAP - , m_optgroup_render -#endif // ENABLE_ENVIRONMENT_MAP - }) { - std::pair ctrl = opt_group->get_custom_ctrl_with_blinking_ptr(opt_key, -1); - if (ctrl.first && ctrl.second) { - m_highlighter.init(ctrl); - break; - } - } -} - -} // GUI -} // Slic3r +#include "Preferences.hpp" +#include "OptionsGroup.hpp" +#include "GUI_App.hpp" +#include "Plater.hpp" +#include "MsgDialog.hpp" +#include "I18N.hpp" +#include "libslic3r/AppConfig.hpp" +#include +#include "Notebook.hpp" +#include "ButtonsDescription.hpp" +#include "OG_CustomCtrl.hpp" +#include "GLCanvas3D.hpp" + +namespace Slic3r { + + static t_config_enum_names enum_names_from_keys_map(const t_config_enum_values& enum_keys_map) + { + t_config_enum_names names; + int cnt = 0; + for (const auto& kvp : enum_keys_map) + cnt = std::max(cnt, kvp.second); + cnt += 1; + names.assign(cnt, ""); + for (const auto& kvp : enum_keys_map) + names[kvp.second] = kvp.first; + return names; + } + +#define CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(NAME) \ + static t_config_enum_names s_keys_names_##NAME = enum_names_from_keys_map(s_keys_map_##NAME); \ + template<> const t_config_enum_values& ConfigOptionEnum::get_enum_values() { return s_keys_map_##NAME; } \ + template<> const t_config_enum_names& ConfigOptionEnum::get_enum_names() { return s_keys_names_##NAME; } + + + + static const t_config_enum_values s_keys_map_NotifyReleaseMode = { + {"all", NotifyReleaseAll}, + {"release", NotifyReleaseOnly}, + {"none", NotifyReleaseNone}, + }; + + CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(NotifyReleaseMode) + +namespace GUI { + +PreferencesDialog::PreferencesDialog(wxWindow* parent) : + DPIDialog(parent, wxID_ANY, _L("Preferences"), wxDefaultPosition, + wxDefaultSize, wxDEFAULT_DIALOG_STYLE) +{ +#ifdef __WXOSX__ + isOSX = true; +#endif + build(); + + m_highlighter.set_timer_owner(this, 0); +} + +static void update_color(wxColourPickerCtrl* color_pckr, const wxColour& color) +{ + if (color_pckr->GetColour() != color) { + color_pckr->SetColour(color); + wxPostEvent(color_pckr, wxCommandEvent(wxEVT_COLOURPICKER_CHANGED)); + } +} + +void PreferencesDialog::show(const std::string& highlight_opt_key /*= std::string()*/, const std::string& tab_name/*= std::string()*/) +{ + int selected_tab = 0; + for ( ; selected_tab < int(tabs->GetPageCount()); selected_tab++) + if (tabs->GetPageText(selected_tab) == _(tab_name)) + break; + if (selected_tab < int(tabs->GetPageCount())) + tabs->SetSelection(selected_tab); + + if (!highlight_opt_key.empty()) + init_highlighter(highlight_opt_key); + + // cache input values for custom toolbar size + m_custom_toolbar_size = atoi(get_app_config()->get("custom_toolbar_size").c_str()); + m_use_custom_toolbar_size = get_app_config()->get("use_custom_toolbar_size") == "1"; + + if (wxGetApp().is_editor()) { + // update colors for color pickers + update_color(m_sys_colour, wxGetApp().get_label_clr_sys()); + update_color(m_mod_colour, wxGetApp().get_label_clr_modified()); + } + + this->ShowModal(); +} + +static std::shared_ptrcreate_options_tab(const wxString& title, wxBookCtrlBase* tabs) +{ + wxPanel* tab = new wxPanel(tabs, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBK_LEFT | wxTAB_TRAVERSAL); + tabs->AddPage(tab, _(title)); + tab->SetFont(wxGetApp().normal_font()); + + wxBoxSizer* sizer = new wxBoxSizer(wxVERTICAL); + sizer->SetSizeHints(tab); + tab->SetSizer(sizer); + + std::shared_ptr optgroup = std::make_shared(tab); + optgroup->label_width = 40; + optgroup->set_config_category_and_type(title, int(Preset::TYPE_PREFERENCES)); + return optgroup; +} + +static void activate_options_tab(std::shared_ptr optgroup) +{ + optgroup->activate([](){}, wxALIGN_RIGHT); + optgroup->update_visibility(comSimple); + wxBoxSizer* sizer = static_cast(static_cast(optgroup->parent())->GetSizer()); + sizer->Add(optgroup->sizer, 0, wxEXPAND | wxALL, 10); + + // apply sercher + wxGetApp().sidebar().get_searcher().append_preferences_options(optgroup->get_lines()); +} + +static void append_bool_option( std::shared_ptr optgroup, + const std::string& opt_key, + const std::string& label, + const std::string& tooltip, + bool def_val, + ConfigOptionMode mode = comSimple) +{ + ConfigOptionDef def = {opt_key, coBool}; + def.label = label; + def.tooltip = tooltip; + def.mode = mode; + def.set_default_value(new ConfigOptionBool{ def_val }); + Option option(def, opt_key); + optgroup->append_single_option_line(option); + + // fill data to the Search Dialog + wxGetApp().sidebar().get_searcher().add_key(opt_key, Preset::TYPE_PREFERENCES, optgroup->config_category(), L("Preferences")); +} + +static void append_enum_option( std::shared_ptr optgroup, + const std::string& opt_key, + const std::string& label, + const std::string& tooltip, + const ConfigOption* def_val, + const t_config_enum_values *enum_keys_map, + std::initializer_list enum_values, + std::initializer_list enum_labels, + ConfigOptionMode mode = comSimple) +{ + ConfigOptionDef def = {opt_key, coEnum }; + def.label = label; + def.tooltip = tooltip; + def.mode = mode; + def.enum_keys_map = enum_keys_map; + def.enum_values = std::vector(enum_values); + def.enum_labels = std::vector(enum_labels); + + def.set_default_value(def_val); + Option option(def, opt_key); + optgroup->append_single_option_line(option); + + // fill data to the Search Dialog + wxGetApp().sidebar().get_searcher().add_key(opt_key, Preset::TYPE_PREFERENCES, optgroup->config_category(), L("Preferences")); +} + +static void append_preferences_option_to_searcer(std::shared_ptr optgroup, + const std::string& opt_key, + const wxString& label) +{ + // fill data to the Search Dialog + wxGetApp().sidebar().get_searcher().add_key(opt_key, Preset::TYPE_PREFERENCES, optgroup->config_category(), L("Preferences")); + // apply sercher + wxGetApp().sidebar().get_searcher().append_preferences_option(Line(opt_key, label, "")); +} + +void PreferencesDialog::build() +{ +#ifdef _WIN32 + wxGetApp().UpdateDarkUI(this); +#else + SetBackgroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); +#endif + const wxFont& font = wxGetApp().normal_font(); + SetFont(font); + + auto app_config = get_app_config(); + +#ifdef _MSW_DARK_MODE + tabs = new Notebook(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxNB_TOP | wxTAB_TRAVERSAL | wxNB_NOPAGETHEME | wxNB_DEFAULT); +#else + tabs = new wxNotebook(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxNB_TOP | wxTAB_TRAVERSAL |wxNB_NOPAGETHEME | wxNB_DEFAULT ); + tabs->SetBackgroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); +#endif + + // Add "General" tab + m_optgroup_general = create_options_tab(L("General"), tabs); + m_optgroup_general->m_on_change = [this](t_config_option_key opt_key, boost::any value) { + if (auto it = m_values.find(opt_key); it != m_values.end()) { + m_values.erase(it); // we shouldn't change value, if some of those parameters were selected, and then deselected + return; + } + if (opt_key == "default_action_on_close_application" || opt_key == "default_action_on_select_preset" || opt_key == "default_action_on_new_project") + m_values[opt_key] = boost::any_cast(value) ? "none" : "discard"; + else if (opt_key == "default_action_on_dirty_project") + m_values[opt_key] = boost::any_cast(value) ? "" : "0"; + else + m_values[opt_key] = boost::any_cast(value) ? "1" : "0"; + }; + + bool is_editor = wxGetApp().is_editor(); + + if (is_editor) { + append_bool_option(m_optgroup_general, "remember_output_path", + L("Remember output directory"), + L("If this is enabled, Slic3r will prompt the last output directory instead of the one containing the input files."), + app_config->has("remember_output_path") ? app_config->get("remember_output_path") == "1" : true); + + append_bool_option(m_optgroup_general, "autocenter", + L("Auto-center parts"), + L("If this is enabled, Slic3r will auto-center objects around the print bed center."), + app_config->get("autocenter") == "1"); + + append_bool_option(m_optgroup_general, "background_processing", + L("Background processing"), + L("If this is enabled, Slic3r will pre-process objects as soon " + "as they\'re loaded in order to save time when exporting G-code."), + app_config->get("background_processing") == "1"); + + m_optgroup_general->append_separator(); + + // Please keep in sync with ConfigWizard + append_bool_option(m_optgroup_general, "export_sources_full_pathnames", + L("Export sources full pathnames to 3mf and amf"), + L("If enabled, allows the Reload from disk command to automatically find and load the files when invoked."), + app_config->get("export_sources_full_pathnames") == "1"); + +#ifdef _WIN32 + // Please keep in sync with ConfigWizard + append_bool_option(m_optgroup_general, "associate_3mf", + L("Associate .3mf files to PrusaSlicer"), + L("If enabled, sets PrusaSlicer as default application to open .3mf files."), + app_config->get("associate_3mf") == "1"); + + append_bool_option(m_optgroup_general, "associate_stl", + L("Associate .stl files to PrusaSlicer"), + L("If enabled, sets PrusaSlicer as default application to open .stl files."), + app_config->get("associate_stl") == "1"); +#endif // _WIN32 + + m_optgroup_general->append_separator(); + + // Please keep in sync with ConfigWizard + append_bool_option(m_optgroup_general, "preset_update", + L("Update built-in Presets automatically"), + L("If enabled, Slic3r downloads updates of built-in system presets in the background. These updates are downloaded " + "into a separate temporary location. When a new preset version becomes available it is offered at application startup."), + app_config->get("preset_update") == "1"); + + append_bool_option(m_optgroup_general, "no_defaults", + L("Suppress \" - default - \" presets"), + L("Suppress \" - default - \" presets in the Print / Filament / Printer selections once there are any other valid presets available."), + app_config->get("no_defaults") == "1"); + + append_bool_option(m_optgroup_general, "show_incompatible_presets", + L("Show incompatible print and filament presets"), + L("When checked, the print and filament presets are shown in the preset editor " + "even if they are marked as incompatible with the active printer"), + app_config->get("show_incompatible_presets") == "1"); + + m_optgroup_general->append_separator(); + + append_bool_option(m_optgroup_general, "show_drop_project_dialog", + L("Show drop project dialog"), + L("When checked, whenever dragging and dropping a project file on the application, shows a dialog asking to select the action to take on the file to load."), + app_config->get("show_drop_project_dialog") == "1"); + + append_bool_option(m_optgroup_general, "single_instance", +#if __APPLE__ + L("Allow just a single PrusaSlicer instance"), + L("On OSX there is always only one instance of app running by default. However it is allowed to run multiple instances " + "of same app from the command line. In such case this settings will allow only one instance."), +#else + L("Allow just a single PrusaSlicer instance"), + L("If this is enabled, when starting PrusaSlicer and another instance of the same PrusaSlicer is already running, that instance will be reactivated instead."), +#endif + app_config->has("single_instance") ? app_config->get("single_instance") == "1" : false ); + + m_optgroup_general->append_separator(); + + append_bool_option(m_optgroup_general, "default_action_on_dirty_project", + L("Ask for unsaved changes in project"), + L("Always ask for unsaved changes in project, when: \n" + "- Closing PrusaSlicer,\n" + "- Loading or creating a new project"), + app_config->get("default_action_on_dirty_project").empty()); + + m_optgroup_general->append_separator(); + + append_bool_option(m_optgroup_general, "default_action_on_close_application", + L("Ask to save unsaved changes in presets when closing the application or when loading a new project"), + L("Always ask for unsaved changes in presets, when: \n" + "- Closing PrusaSlicer while some presets are modified,\n" + "- Loading a new project while some presets are modified"), + app_config->get("default_action_on_close_application") == "none"); + + append_bool_option(m_optgroup_general, "default_action_on_select_preset", + L("Ask for unsaved changes in presets when selecting new preset"), + L("Always ask for unsaved changes in presets when selecting new preset or resetting a preset"), + app_config->get("default_action_on_select_preset") == "none"); + + append_bool_option(m_optgroup_general, "default_action_on_new_project", + L("Ask for unsaved changes in presets when creating new project"), + L("Always ask for unsaved changes in presets when creating new project"), + app_config->get("default_action_on_new_project") == "none"); + } +#ifdef _WIN32 + else { + append_bool_option(m_optgroup_general, "associate_gcode", + L("Associate .gcode files to PrusaSlicer G-code Viewer"), + L("If enabled, sets PrusaSlicer G-code Viewer as default application to open .gcode files."), + app_config->get("associate_gcode") == "1"); + } +#endif // _WIN32 + +#if __APPLE__ + append_bool_option(m_optgroup_general, "use_retina_opengl", + L("Use Retina resolution for the 3D scene"), + L("If enabled, the 3D scene will be rendered in Retina resolution. " + "If you are experiencing 3D performance problems, disabling this option may help."), + app_config->get("use_retina_opengl") == "1"); +#endif + + m_optgroup_general->append_separator(); + + // Show/Hide splash screen + append_bool_option(m_optgroup_general, "show_splash_screen", + L("Show splash screen"), + L("Show splash screen"), + app_config->get("show_splash_screen") == "1"); + + append_bool_option(m_optgroup_general, "restore_win_position", + L("Restore window position on start"), + L("If enabled, PrusaSlicer will be open at the position it was closed"), + app_config->get("restore_win_position") == "1"); + + // Clear Undo / Redo stack on new project + append_bool_option(m_optgroup_general, "clear_undo_redo_stack_on_new_project", + L("Clear Undo / Redo stack on new project"), + L("Clear Undo / Redo stack on new project or when an existing project is loaded."), + app_config->get("clear_undo_redo_stack_on_new_project") == "1"); + +#if defined(_WIN32) || defined(__APPLE__) + append_bool_option(m_optgroup_general, "use_legacy_3DConnexion", + L("Enable support for legacy 3DConnexion devices"), + L("If enabled, the legacy 3DConnexion devices settings dialog is available by pressing CTRL+M"), + app_config->get("use_legacy_3DConnexion") == "1"); +#endif // _WIN32 || __APPLE__ + + activate_options_tab(m_optgroup_general); + + // Add "Camera" tab + m_optgroup_camera = create_options_tab(L("Camera"), tabs); + m_optgroup_camera->m_on_change = [this](t_config_option_key opt_key, boost::any value) { + if (auto it = m_values.find(opt_key);it != m_values.end()) { + m_values.erase(it); // we shouldn't change value, if some of those parameters were selected, and then deselected + return; + } + m_values[opt_key] = boost::any_cast(value) ? "1" : "0"; + }; + + append_bool_option(m_optgroup_camera, "use_perspective_camera", + L("Use perspective camera"), + L("If enabled, use perspective camera. If not enabled, use orthographic camera."), + app_config->get("use_perspective_camera") == "1"); + + append_bool_option(m_optgroup_camera, "use_free_camera", + L("Use free camera"), + L("If enabled, use free camera. If not enabled, use constrained camera."), + app_config->get("use_free_camera") == "1"); + + append_bool_option(m_optgroup_camera, "reverse_mouse_wheel_zoom", + L("Reverse direction of zoom with mouse wheel"), + L("If enabled, reverses the direction of zoom with mouse wheel"), + app_config->get("reverse_mouse_wheel_zoom") == "1"); + + activate_options_tab(m_optgroup_camera); + + // Add "GUI" tab + m_optgroup_gui = create_options_tab(L("GUI"), tabs); + m_optgroup_gui->m_on_change = [this](t_config_option_key opt_key, boost::any value) { + if (opt_key == "notify_release") { + int val_int = boost::any_cast(value); + for (const auto& item : s_keys_map_NotifyReleaseMode) { + if (item.second == val_int) { + m_values[opt_key] = item.first; + return; + } + } + } + if (opt_key == "use_custom_toolbar_size") { + m_icon_size_sizer->ShowItems(boost::any_cast(value)); + refresh_og(m_optgroup_gui); + get_app_config()->set("use_custom_toolbar_size", boost::any_cast(value) ? "1" : "0"); + get_app_config()->save(); + wxGetApp().plater()->get_current_canvas3D()->render(); + return; + } + if (opt_key == "tabs_as_menu") { + bool disable_new_layout = boost::any_cast(value); + m_rb_new_settings_layout_mode->Show(!disable_new_layout); + if (disable_new_layout && m_rb_new_settings_layout_mode->GetValue()) { + m_rb_new_settings_layout_mode->SetValue(false); + m_rb_old_settings_layout_mode->SetValue(true); + } + refresh_og(m_optgroup_gui); + } + + if (auto it = m_values.find(opt_key); it != m_values.end()) { + m_values.erase(it); // we shouldn't change value, if some of those parameters were selected, and then deselected + return; + } + + if (opt_key == "suppress_hyperlinks") + m_values[opt_key] = boost::any_cast(value) ? "1" : ""; + else + m_values[opt_key] = boost::any_cast(value) ? "1" : "0"; + }; + + append_bool_option(m_optgroup_gui, "seq_top_layer_only", + L("Sequential slider applied only to top layer"), + L("If enabled, changes made using the sequential slider, in preview, apply only to gcode top layer." + "If disabled, changes made using the sequential slider, in preview, apply to the whole gcode."), + app_config->get("seq_top_layer_only") == "1"); + + if (is_editor) { + append_bool_option(m_optgroup_gui, "show_collapse_button", + L("Show sidebar collapse/expand button"), + L("If enabled, the button for the collapse sidebar will be appeared in top right corner of the 3D Scene"), + app_config->get("show_collapse_button") == "1"); + + append_bool_option(m_optgroup_gui, "suppress_hyperlinks", + L("Suppress to open hyperlink in browser"), + L("If enabled, PrusaSlicer will not open a hyperlinks in your browser."), + //L("If enabled, the descriptions of configuration parameters in settings tabs wouldn't work as hyperlinks. " + // "If disabled, the descriptions of configuration parameters in settings tabs will work as hyperlinks."), + app_config->get("suppress_hyperlinks") == "1"); + + append_bool_option(m_optgroup_gui, "color_mapinulation_panel", + L("Use colors for axes values in Manipulation panel"), + L("If enabled, the axes names and axes values will be colorized according to the axes colors. " + "If disabled, old UI will be used."), + app_config->get("color_mapinulation_panel") == "1"); + + append_bool_option(m_optgroup_gui, "order_volumes", + L("Order object volumes by types"), + L("If enabled, volumes will be always ordered inside the object. Correct order is Model Part, Negative Volume, Modifier, Support Blocker and Support Enforcer. " + "If disabled, you can reorder Model Parts, Negative Volumes and Modifiers. But one of the model parts have to be on the first place."), + app_config->get("order_volumes") == "1"); + +#if ENABLE_SHOW_NON_MANIFOLD_EDGES + append_bool_option(m_optgroup_gui, "non_manifold_edges", + L("Show non-manifold edges"), + L("If enabled, shows non-manifold edges."), + app_config->get("non_manifold_edges") == "1"); +#endif // ENABLE_SHOW_NON_MANIFOLD_EDGES + +#ifdef _MSW_DARK_MODE + append_bool_option(m_optgroup_gui, "tabs_as_menu", + L("Set settings tabs as menu items (experimental)"), + L("If enabled, Settings Tabs will be placed as menu items. If disabled, old UI will be used."), + app_config->get("tabs_as_menu") == "1"); +#endif + + m_optgroup_gui->append_separator(); + + append_bool_option(m_optgroup_gui, "show_hints", + L("Show \"Tip of the day\" notification after start"), + L("If enabled, useful hints are displayed at startup."), + app_config->get("show_hints") == "1"); + + append_enum_option(m_optgroup_gui, "notify_release", + L("Notify about new releases"), + L("You will be notified about new release after startup acordingly: All = Regular release and alpha / beta releases. Release only = regular release."), + new ConfigOptionEnum(static_cast(s_keys_map_NotifyReleaseMode.at(app_config->get("notify_release")))), + &ConfigOptionEnum::get_enum_values(), + {"all", "release", "none"}, + {L("All"), L("Release only"), L("None")}); + + m_optgroup_gui->append_separator(); + + append_bool_option(m_optgroup_gui, "use_custom_toolbar_size", + L("Use custom size for toolbar icons"), + L("If enabled, you can change size of toolbar icons manually."), + app_config->get("use_custom_toolbar_size") == "1"); + } + + activate_options_tab(m_optgroup_gui); + + if (is_editor) { + // set Field for notify_release to its value to activate the object + boost::any val = s_keys_map_NotifyReleaseMode.at(app_config->get("notify_release")); + m_optgroup_gui->get_field("notify_release")->set_value(val, false); + + create_icon_size_slider(); + m_icon_size_sizer->ShowItems(app_config->get("use_custom_toolbar_size") == "1"); + + create_settings_mode_widget(); + create_settings_text_color_widget(); + +#if ENABLE_ENVIRONMENT_MAP + // Add "Render" tab + m_optgroup_render = create_options_tab(L("Render"), tabs); + m_optgroup_render->m_on_change = [this](t_config_option_key opt_key, boost::any value) { + if (auto it = m_values.find(opt_key); it != m_values.end()) { + m_values.erase(it); // we shouldn't change value, if some of those parameters were selected, and then deselected + return; + } + m_values[opt_key] = boost::any_cast(value) ? "1" : "0"; + }; + + append_bool_option(m_optgroup_render, "use_environment_map", + L("Use environment map"), + L("If enabled, renders object using the environment map."), + app_config->get("use_environment_map") == "1"); + + activate_options_tab(m_optgroup_render); +#endif // ENABLE_ENVIRONMENT_MAP + +#ifdef _WIN32 + // Add "Dark Mode" tab + m_optgroup_dark_mode = create_options_tab(_L("Dark mode (experimental)"), tabs); + m_optgroup_dark_mode->m_on_change = [this](t_config_option_key opt_key, boost::any value) { + if (auto it = m_values.find(opt_key); it != m_values.end()) { + m_values.erase(it); // we shouldn't change value, if some of those parameters were selected, and then deselected + return; + } + m_values[opt_key] = boost::any_cast(value) ? "1" : "0"; + }; + + append_bool_option(m_optgroup_dark_mode, "dark_color_mode", + L("Enable dark mode"), + L("If enabled, UI will use Dark mode colors. If disabled, old UI will be used."), + app_config->get("dark_color_mode") == "1"); + + if (wxPlatformInfo::Get().GetOSMajorVersion() >= 10) // Use system menu just for Window newer then Windows 10 + // Use menu with ownerdrawn items by default on systems older then Windows 10 + { + append_bool_option(m_optgroup_dark_mode, "sys_menu_enabled", + L("Use system menu for application"), + L("If enabled, application will use the standart Windows system menu,\n" + "but on some combination od display scales it can look ugly. If disabled, old UI will be used."), + app_config->get("sys_menu_enabled") == "1"); + } + + activate_options_tab(m_optgroup_dark_mode); +#endif //_WIN32 + } + + // update alignment of the controls for all tabs + update_ctrls_alignment(); + + auto sizer = new wxBoxSizer(wxVERTICAL); + sizer->Add(tabs, 1, wxEXPAND | wxTOP | wxLEFT | wxRIGHT, 5); + + auto buttons = CreateStdDialogButtonSizer(wxOK | wxCANCEL); + this->Bind(wxEVT_BUTTON, &PreferencesDialog::accept, this, wxID_OK); + this->Bind(wxEVT_BUTTON, &PreferencesDialog::revert, this, wxID_CANCEL); + + for (int id : {wxID_OK, wxID_CANCEL}) + wxGetApp().UpdateDarkUI(static_cast(FindWindowById(id, this))); + + sizer->Add(buttons, 0, wxALIGN_CENTER_HORIZONTAL | wxBOTTOM | wxTOP, 10); + + SetSizer(sizer); + sizer->SetSizeHints(this); + this->CenterOnParent(); +} + +std::vector PreferencesDialog::optgroups() +{ + std::vector out; + out.reserve(4); + for (ConfigOptionsGroup* opt : { m_optgroup_general.get(), m_optgroup_camera.get(), m_optgroup_gui.get() +#ifdef _WIN32 + , m_optgroup_dark_mode.get() +#endif // _WIN32 +#if ENABLE_ENVIRONMENT_MAP + , m_optgroup_render.get() +#endif // ENABLE_ENVIRONMENT_MAP + }) + if (opt) + out.emplace_back(opt); + return out; +} + +void PreferencesDialog::update_ctrls_alignment() +{ + int max_ctrl_width{ 0 }; + for (ConfigOptionsGroup* og : this->optgroups()) + if (int max = og->custom_ctrl->get_max_win_width(); + max_ctrl_width < max) + max_ctrl_width = max; + if (max_ctrl_width) + for (ConfigOptionsGroup* og : this->optgroups()) + og->custom_ctrl->set_max_win_width(max_ctrl_width); +} + +void PreferencesDialog::accept(wxEvent&) +{ + std::vector options_to_recreate_GUI = { "no_defaults", "tabs_as_menu", "sys_menu_enabled" }; + + for (const std::string& option : options_to_recreate_GUI) { + if (m_values.find(option) != m_values.end()) { + wxString title = wxGetApp().is_editor() ? wxString(SLIC3R_APP_NAME) : wxString(GCODEVIEWER_APP_NAME); + title += " - " + _L("Changes for the critical options"); + MessageDialog dialog(nullptr, + _L("Changing some options will trigger application restart.\n" + "You will lose the content of the plater.") + "\n\n" + + _L("Do you want to proceed?"), + title, + wxICON_QUESTION | wxYES | wxNO); + if (dialog.ShowModal() == wxID_YES) { + m_recreate_GUI = true; + } + else { + for (const std::string& option : options_to_recreate_GUI) + m_values.erase(option); + } + break; + } + } + + auto app_config = get_app_config(); + + m_seq_top_layer_only_changed = false; + if (auto it = m_values.find("seq_top_layer_only"); it != m_values.end()) + m_seq_top_layer_only_changed = app_config->get("seq_top_layer_only") != it->second; + + m_settings_layout_changed = false; + for (const std::string& key : { "old_settings_layout_mode", + "new_settings_layout_mode", + "dlg_settings_layout_mode" }) + { + auto it = m_values.find(key); + if (it != m_values.end() && app_config->get(key) != it->second) { + m_settings_layout_changed = true; + break; + } + } + +#if 0 //#ifdef _WIN32 // #ysDarkMSW - Allow it when we deside to support the sustem colors for application + if (m_values.find("always_dark_color_mode") != m_values.end()) + wxGetApp().force_sys_colors_update(); +#endif + + for (std::map::iterator it = m_values.begin(); it != m_values.end(); ++it) + app_config->set(it->first, it->second); + + app_config->save(); + if (wxGetApp().is_editor()) { + wxGetApp().set_label_clr_sys(m_sys_colour->GetColour()); + wxGetApp().set_label_clr_modified(m_mod_colour->GetColour()); + } + + EndModal(wxID_OK); + +#ifdef _WIN32 + if (m_values.find("dark_color_mode") != m_values.end()) + wxGetApp().force_colors_update(); +#ifdef _MSW_DARK_MODE + if (m_values.find("sys_menu_enabled") != m_values.end()) + wxGetApp().force_menu_update(); +#endif //_MSW_DARK_MODE +#endif // _WIN32 + + wxGetApp().update_ui_from_settings(); + clear_cache(); +} + +void PreferencesDialog::revert(wxEvent&) +{ + auto app_config = get_app_config(); + + bool save_app_config = false; + if (m_custom_toolbar_size != atoi(app_config->get("custom_toolbar_size").c_str())) { + app_config->set("custom_toolbar_size", (boost::format("%d") % m_custom_toolbar_size).str()); + m_icon_size_slider->SetValue(m_custom_toolbar_size); + save_app_config |= true; + } + if (m_use_custom_toolbar_size != (get_app_config()->get("use_custom_toolbar_size") == "1")) { + app_config->set("use_custom_toolbar_size", m_use_custom_toolbar_size ? "1" : "0"); + save_app_config |= true; + + m_optgroup_gui->set_value("use_custom_toolbar_size", m_use_custom_toolbar_size); + m_icon_size_sizer->ShowItems(m_use_custom_toolbar_size); + refresh_og(m_optgroup_gui); + } + if (save_app_config) + app_config->save(); + + + for (auto value : m_values) { + const std::string& key = value.first; + + if (key == "default_action_on_dirty_project") { + m_optgroup_general->set_value(key, app_config->get(key).empty()); + continue; + } + if (key == "default_action_on_close_application" || key == "default_action_on_select_preset" || key == "default_action_on_new_project") { + m_optgroup_general->set_value(key, app_config->get(key) == "none"); + continue; + } + if (key == "notify_release") { + m_optgroup_gui->set_value(key, s_keys_map_NotifyReleaseMode.at(app_config->get(key))); + continue; + } + if (key == "old_settings_layout_mode") { + m_rb_old_settings_layout_mode->SetValue(app_config->get(key) == "1"); + m_settings_layout_changed = false; + continue; + } + if (key == "new_settings_layout_mode") { + m_rb_new_settings_layout_mode->SetValue(app_config->get(key) == "1"); + m_settings_layout_changed = false; + continue; + } + if (key == "dlg_settings_layout_mode") { + m_rb_dlg_settings_layout_mode->SetValue(app_config->get(key) == "1"); + m_settings_layout_changed = false; + continue; + } + + for (auto opt_group : { m_optgroup_general, m_optgroup_camera, m_optgroup_gui +#ifdef _WIN32 + , m_optgroup_dark_mode +#endif // _WIN32 +#if ENABLE_ENVIRONMENT_MAP + , m_optgroup_render +#endif // ENABLE_ENVIRONMENT_MAP + }) { + if (opt_group->set_value(key, app_config->get(key) == "1")) + break; + } + if (key == "tabs_as_menu") { + m_rb_new_settings_layout_mode->Show(app_config->get(key) != "1"); + refresh_og(m_optgroup_gui); + continue; + } + } + + clear_cache(); + EndModal(wxID_CANCEL); +} + +void PreferencesDialog::msw_rescale() +{ + for (ConfigOptionsGroup* og : this->optgroups()) + og->msw_rescale(); +#ifdef _WIN32 + m_optgroup_dark_mode->msw_rescale(); +#endif //_WIN32 +#if ENABLE_ENVIRONMENT_MAP + m_optgroup_render->msw_rescale(); +#endif // ENABLE_ENVIRONMENT_MAP + + msw_buttons_rescale(this, em_unit(), { wxID_OK, wxID_CANCEL }); + + layout(); +} + +void PreferencesDialog::on_sys_color_changed() +{ +#ifdef _WIN32 + wxGetApp().UpdateDlgDarkUI(this); +#endif +} + +void PreferencesDialog::layout() +{ + const int em = em_unit(); + + SetMinSize(wxSize(47 * em, 28 * em)); + Fit(); + + Refresh(); +} + +void PreferencesDialog::clear_cache() +{ + m_values.clear(); + m_custom_toolbar_size = -1; +} + +void PreferencesDialog::refresh_og(std::shared_ptr og) +{ + og->parent()->Layout(); + tabs->Layout(); + this->layout(); +} + +void PreferencesDialog::create_icon_size_slider() +{ + const auto app_config = get_app_config(); + + const int em = em_unit(); + + m_icon_size_sizer = new wxBoxSizer(wxHORIZONTAL); + + wxWindow* parent = m_optgroup_gui->parent(); + wxGetApp().UpdateDarkUI(parent); + + if (isOSX) + // For correct rendering of the slider and value label under OSX + // we should use system default background + parent->SetBackgroundStyle(wxBG_STYLE_ERASE); + + auto label = new wxStaticText(parent, wxID_ANY, _L("Icon size in a respect to the default size") + " (%) :"); + + m_icon_size_sizer->Add(label, 0, wxALIGN_CENTER_VERTICAL| wxRIGHT | (isOSX ? 0 : wxLEFT), em); + + const int def_val = atoi(app_config->get("custom_toolbar_size").c_str()); + + long style = wxSL_HORIZONTAL; + if (!isOSX) + style |= wxSL_LABELS | wxSL_AUTOTICKS; + + m_icon_size_slider = new wxSlider(parent, wxID_ANY, def_val, 30, 100, + wxDefaultPosition, wxDefaultSize, style); + + m_icon_size_slider->SetTickFreq(10); + m_icon_size_slider->SetPageSize(10); + m_icon_size_slider->SetToolTip(_L("Select toolbar icon size in respect to the default one.")); + + m_icon_size_sizer->Add(m_icon_size_slider, 1, wxEXPAND); + + wxStaticText* val_label{ nullptr }; + if (isOSX) { + val_label = new wxStaticText(parent, wxID_ANY, wxString::Format("%d", def_val)); + m_icon_size_sizer->Add(val_label, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, em); + } + + m_icon_size_slider->Bind(wxEVT_SLIDER, ([this, val_label, app_config](wxCommandEvent e) { + auto val = m_icon_size_slider->GetValue(); + + app_config->set("custom_toolbar_size", (boost::format("%d") % val).str()); + app_config->save(); + wxGetApp().plater()->get_current_canvas3D()->render(); + + if (val_label) + val_label->SetLabelText(wxString::Format("%d", val)); + }), m_icon_size_slider->GetId()); + + for (wxWindow* win : std::vector{ m_icon_size_slider, label, val_label }) { + if (!win) continue; + win->SetFont(wxGetApp().normal_font()); + + if (isOSX) continue; // under OSX we use wxBG_STYLE_ERASE + win->SetBackgroundStyle(wxBG_STYLE_PAINT); + } + + m_optgroup_gui->sizer->Add(m_icon_size_sizer, 0, wxEXPAND | wxALL, em); +} + +void PreferencesDialog::create_settings_mode_widget() +{ + wxWindow* parent = m_optgroup_gui->parent(); + wxGetApp().UpdateDarkUI(parent); + + wxString title = L("Layout Options"); + wxStaticBox* stb = new wxStaticBox(parent, wxID_ANY, _(title)); + wxGetApp().UpdateDarkUI(stb); + if (!wxOSX) stb->SetBackgroundStyle(wxBG_STYLE_PAINT); + stb->SetFont(wxGetApp().normal_font()); + + wxSizer* stb_sizer = new wxStaticBoxSizer(stb, wxVERTICAL); + + auto app_config = get_app_config(); + std::vector choices = { _L("Old regular layout with the tab bar"), + _L("New layout, access via settings button in the top menu"), + _L("Settings in non-modal window") }; + int id = -1; + auto add_radio = [this, parent, stb_sizer, choices](wxRadioButton** rb, int id, bool select) { + *rb = new wxRadioButton(parent, wxID_ANY, choices[id], wxDefaultPosition, wxDefaultSize, id == 0 ? wxRB_GROUP : 0); + stb_sizer->Add(*rb); + (*rb)->SetValue(select); + (*rb)->Bind(wxEVT_RADIOBUTTON, [this, id](wxCommandEvent&) { + m_values["old_settings_layout_mode"] = (id == 0) ? "1" : "0"; + m_values["new_settings_layout_mode"] = (id == 1) ? "1" : "0"; + m_values["dlg_settings_layout_mode"] = (id == 2) ? "1" : "0"; + }); + }; + + add_radio(&m_rb_old_settings_layout_mode, ++id, app_config->get("old_settings_layout_mode") == "1"); + add_radio(&m_rb_new_settings_layout_mode, ++id, app_config->get("new_settings_layout_mode") == "1"); + add_radio(&m_rb_dlg_settings_layout_mode, ++id, app_config->get("dlg_settings_layout_mode") == "1"); + +#ifdef _MSW_DARK_MODE + if (app_config->get("tabs_as_menu") == "1") { + m_rb_new_settings_layout_mode->Hide(); + if (m_rb_new_settings_layout_mode->GetValue()) { + m_rb_new_settings_layout_mode->SetValue(false); + m_rb_old_settings_layout_mode->SetValue(true); + } + } +#endif + + std::string opt_key = "settings_layout_mode"; + m_blinkers[opt_key] = new BlinkingBitmap(parent); + + auto sizer = new wxBoxSizer(wxHORIZONTAL); + sizer->Add(m_blinkers[opt_key], 0, wxRIGHT, 2); + sizer->Add(stb_sizer, 1, wxALIGN_CENTER_VERTICAL); + m_optgroup_gui->sizer->Add(sizer, 0, wxEXPAND | wxTOP, em_unit()); + + append_preferences_option_to_searcer(m_optgroup_gui, opt_key, title); +} + +void PreferencesDialog::create_settings_text_color_widget() +{ + wxWindow* parent = m_optgroup_gui->parent(); + + wxString title = L("Text colors"); + wxStaticBox* stb = new wxStaticBox(parent, wxID_ANY, _(title)); + wxGetApp().UpdateDarkUI(stb); + if (!wxOSX) stb->SetBackgroundStyle(wxBG_STYLE_PAINT); + + std::string opt_key = "text_colors"; + m_blinkers[opt_key] = new BlinkingBitmap(parent); + + wxSizer* stb_sizer = new wxStaticBoxSizer(stb, wxVERTICAL); + ButtonsDescription::FillSizerWithTextColorDescriptions(stb_sizer, parent, &m_sys_colour, &m_mod_colour); + + auto sizer = new wxBoxSizer(wxHORIZONTAL); + sizer->Add(m_blinkers[opt_key], 0, wxRIGHT, 2); + sizer->Add(stb_sizer, 1, wxALIGN_CENTER_VERTICAL); + + m_optgroup_gui->sizer->Add(sizer, 0, wxEXPAND | wxTOP, em_unit()); + + append_preferences_option_to_searcer(m_optgroup_gui, opt_key, title); +} + +void PreferencesDialog::init_highlighter(const t_config_option_key& opt_key) +{ + if (m_blinkers.find(opt_key) != m_blinkers.end()) + if (BlinkingBitmap* blinker = m_blinkers.at(opt_key); blinker) { + m_highlighter.init(blinker); + return; + } + + for (auto opt_group : { m_optgroup_general, m_optgroup_camera, m_optgroup_gui +#ifdef _WIN32 + , m_optgroup_dark_mode +#endif // _WIN32 +#if ENABLE_ENVIRONMENT_MAP + , m_optgroup_render +#endif // ENABLE_ENVIRONMENT_MAP + }) { + std::pair ctrl = opt_group->get_custom_ctrl_with_blinking_ptr(opt_key, -1); + if (ctrl.first && ctrl.second) { + m_highlighter.init(ctrl); + break; + } + } +} + +} // GUI +} // Slic3r diff --git a/src/slic3r/GUI/PresetComboBoxes.cpp b/src/slic3r/GUI/PresetComboBoxes.cpp index ed4888a87..597c30312 100644 --- a/src/slic3r/GUI/PresetComboBoxes.cpp +++ b/src/slic3r/GUI/PresetComboBoxes.cpp @@ -108,8 +108,8 @@ PresetComboBox::PresetComboBox(wxWindow* parent, Preset::Type preset_type, const default: break; } - m_bitmapCompatible = ScalableBitmap(this, "flag_green"); - m_bitmapIncompatible = ScalableBitmap(this, "flag_red"); + m_bitmapCompatible = get_bmp_bundle("flag_green"); + m_bitmapIncompatible = get_bmp_bundle("flag_red"); // parameters for an icon's drawing fill_width_height(); @@ -242,12 +242,12 @@ void PresetComboBox::update(std::string select_preset_name) const std::deque& presets = m_collection->get_presets(); - std::map> nonsys_presets; - std::map incomp_presets; + std::map> nonsys_presets; + std::map incomp_presets; wxString selected = ""; if (!presets.front().is_visible) - set_label_marker(Append(separator(L("System presets")), wxNullBitmap)); + set_label_marker(Append(separator(L("System presets")), NullBitmapBndl())); for (size_t i = presets.front().is_visible ? 0 : m_collection->num_default_presets(); i < presets.size(); ++i) { @@ -268,7 +268,7 @@ void PresetComboBox::update(std::string select_preset_name) } std::string main_icon_name = m_type == Preset::TYPE_PRINTER && preset.printer_technology() == ptSLA ? "sla_printer" : m_main_bitmap_name; - wxBitmap* bmp = get_bmp(bitmap_key, main_icon_name, "lock_closed", is_enabled, preset.is_compatible, preset.is_system || preset.is_default); + auto bmp = get_bmp(bitmap_key, main_icon_name, "lock_closed", is_enabled, preset.is_compatible, preset.is_system || preset.is_default); assert(bmp); if (!is_enabled) @@ -280,17 +280,17 @@ void PresetComboBox::update(std::string select_preset_name) } else { - nonsys_presets.emplace(get_preset_name(preset), std::pair(bmp, is_enabled)); + nonsys_presets.emplace(get_preset_name(preset), std::pair(bmp, is_enabled)); if (preset.name == select_preset_name || (select_preset_name.empty() && is_enabled)) selected = get_preset_name(preset); } if (i + 1 == m_collection->num_default_presets()) - set_label_marker(Append(separator(L("System presets")), wxNullBitmap)); + set_label_marker(Append(separator(L("System presets")), NullBitmapBndl())); } if (!nonsys_presets.empty()) { - set_label_marker(Append(separator(L("User presets")), wxNullBitmap)); - for (std::map>::iterator it = nonsys_presets.begin(); it != nonsys_presets.end(); ++it) { + set_label_marker(Append(separator(L("User presets")), NullBitmapBndl())); + for (std::map>::iterator it = nonsys_presets.begin(); it != nonsys_presets.end(); ++it) { int item_id = Append(it->first, *it->second.first); bool is_enabled = it->second.second; if (!is_enabled) @@ -300,8 +300,8 @@ void PresetComboBox::update(std::string select_preset_name) } if (!incomp_presets.empty()) { - set_label_marker(Append(separator(L("Incompatible presets")), wxNullBitmap)); - for (std::map::iterator it = incomp_presets.begin(); it != incomp_presets.end(); ++it) { + set_label_marker(Append(separator(L("Incompatible presets")), NullBitmapBndl())); + for (std::map::iterator it = incomp_presets.begin(); it != incomp_presets.end(); ++it) { set_label_marker(Append(it->first, *it->second), LABEL_ITEM_DISABLED); } } @@ -337,7 +337,6 @@ bool PresetComboBox::del_physical_printer(const wxString& note_string/* = wxEmpt msg += note_string + "\n"; msg += format_wxstr(_L("Are you sure you want to delete \"%1%\" printer?"), printer_name); - //if (wxMessageDialog(this, msg, _L("Delete Physical Printer"), wxYES_NO | wxNO_DEFAULT | wxICON_QUESTION).ShowModal() != wxID_YES) if (MessageDialog(this, msg, _L("Delete Physical Printer"), wxYES_NO | wxNO_DEFAULT | wxICON_QUESTION).ShowModal() != wxID_YES) return false; @@ -364,7 +363,8 @@ void PresetComboBox::show_all(bool show_all) void PresetComboBox::update() { - this->update(into_u8(this->GetString(this->GetSelection()))); + int n = this->GetSelection(); + this->update(n < 0 ? "" : into_u8(this->GetString(n))); } void PresetComboBox::update_from_bundle() @@ -375,42 +375,31 @@ void PresetComboBox::update_from_bundle() void PresetComboBox::msw_rescale() { m_em_unit = em_unit(this); +} - m_bitmapIncompatible.msw_rescale(); - m_bitmapCompatible.msw_rescale(); - - // parameters for an icon's drawing - fill_width_height(); +void PresetComboBox::sys_color_changed() +{ + m_bitmapCompatible = get_bmp_bundle("flag_green"); + m_bitmapIncompatible = get_bmp_bundle("flag_red"); + wxGetApp().UpdateDarkUI(this); // update the control to redraw the icons update(); } -void PresetComboBox::sys_color_changed() -{ - wxGetApp().UpdateDarkUI(this); - msw_rescale(); -} - void PresetComboBox::fill_width_height() { - // To avoid asserts, each added bitmap to wxBitmapCombobox should be the same size, so - // set a bitmap's height to m_bitmapCompatible->GetHeight() and norm_icon_width to m_bitmapCompatible->GetWidth() - icon_height = m_bitmapCompatible.GetBmpHeight(); - norm_icon_width = m_bitmapCompatible.GetBmpWidth(); + icon_height = 16; + norm_icon_width = 16; - /* It's supposed that standard size of an icon is 16px*16px for 100% scaled display. - * So set sizes for solid_colored icons used for filament preset - * and scale them in respect to em_unit value - */ - const float scale_f = (float)m_em_unit * 0.1f; - - thin_icon_width = lroundf(8 * scale_f); // analogue to 8px; + thin_icon_width = 8; wide_icon_width = norm_icon_width + thin_icon_width; - space_icon_width = lroundf(2 * scale_f); - thin_space_icon_width = lroundf(4 * scale_f); - wide_space_icon_width = lroundf(6 * scale_f); + null_icon_width = 2 * norm_icon_width; + + space_icon_width = 2; + thin_space_icon_width = 4; + wide_space_icon_width = 6; } wxString PresetComboBox::separator(const std::string& label) @@ -418,7 +407,8 @@ wxString PresetComboBox::separator(const std::string& label) return wxString::FromUTF8(separator_head()) + _(label) + wxString::FromUTF8(separator_tail()); } -wxBitmap* PresetComboBox::get_bmp( std::string bitmap_key, bool wide_icons, const std::string& main_icon_name, + +wxBitmapBundle* PresetComboBox::get_bmp( std::string bitmap_key, bool wide_icons, const std::string& main_icon_name, bool is_compatible/* = true*/, bool is_system/* = false*/, bool is_single_bar/* = false*/, const std::string& filament_rgb/* = ""*/, const std::string& extruder_rgb/* = ""*/, const std::string& material_rgb/* = ""*/) { @@ -434,45 +424,41 @@ wxBitmap* PresetComboBox::get_bmp( std::string bitmap_key, bool wide_icons, con bitmap_key += ",dark"; bitmap_key += material_rgb; - wxBitmap* bmp = bitmap_cache().find(bitmap_key); - if (bmp == nullptr) { + wxBitmapBundle* bmp_bndl = bitmap_cache().find_bndl(bitmap_key); + if (bmp_bndl == nullptr) { // Create the bitmap with color bars. - std::vector bmps; + std::vector bmps; if (wide_icons) // Paint a red flag for incompatible presets. - bmps.emplace_back(is_compatible ? bitmap_cache().mkclear(norm_icon_width, icon_height) : m_bitmapIncompatible.bmp()); + bmps.emplace_back(is_compatible ? get_empty_bmp_bundle(norm_icon_width, icon_height) : m_bitmapIncompatible); if (m_type == Preset::TYPE_FILAMENT && !filament_rgb.empty()) { // Paint the color bars. - ColorRGB color; - decode_color(filament_rgb, color); - bmps.emplace_back(bitmap_cache().mksolid(is_single_bar ? wide_icon_width : norm_icon_width, icon_height, color, false, 1, dark_mode)); - if (!is_single_bar) { - decode_color(extruder_rgb, color); - bmps.emplace_back(bitmap_cache().mksolid(thin_icon_width, icon_height, color, false, 1, dark_mode)); - } + bmps.emplace_back(get_solid_bmp_bundle(is_single_bar ? wide_icon_width : norm_icon_width, icon_height, filament_rgb)); + if (!is_single_bar) + bmps.emplace_back(get_solid_bmp_bundle(thin_icon_width, icon_height, extruder_rgb)); // Paint a lock at the system presets. - bmps.emplace_back(bitmap_cache().mkclear(space_icon_width, icon_height)); + bmps.emplace_back(get_empty_bmp_bundle(space_icon_width, icon_height)); } else { // Paint the color bars. - bmps.emplace_back(bitmap_cache().mkclear(thin_space_icon_width, icon_height)); + bmps.emplace_back(get_empty_bmp_bundle(thin_space_icon_width, icon_height)); if (m_type == Preset::TYPE_SLA_MATERIAL) - bmps.emplace_back(create_scaled_bitmap(main_icon_name, this, 16, false, material_rgb)); + bmps.emplace_back(bitmap_cache().from_svg(main_icon_name, 16, 16, dark_mode, material_rgb)); else - bmps.emplace_back(create_scaled_bitmap(main_icon_name)); + bmps.emplace_back(get_bmp_bundle(main_icon_name)); // Paint a lock at the system presets. - bmps.emplace_back(bitmap_cache().mkclear(wide_space_icon_width, icon_height)); + bmps.emplace_back(get_empty_bmp_bundle(wide_space_icon_width, icon_height)); } - bmps.emplace_back(is_system ? create_scaled_bitmap("lock_closed") : bitmap_cache().mkclear(norm_icon_width, icon_height)); - bmp = bitmap_cache().insert(bitmap_key, bmps); + bmps.emplace_back(is_system ? get_bmp_bundle("lock_closed") : get_empty_bmp_bundle(norm_icon_width, icon_height)); + bmp_bndl = bitmap_cache().insert_bndl(bitmap_key, bmps); } - return bmp; + return bmp_bndl; } -wxBitmap* PresetComboBox::get_bmp( std::string bitmap_key, const std::string& main_icon_name, const std::string& next_icon_name, +wxBitmapBundle* PresetComboBox::get_bmp( std::string bitmap_key, const std::string& main_icon_name, const std::string& next_icon_name, bool is_enabled/* = true*/, bool is_compatible/* = true*/, bool is_system/* = false*/) { bitmap_key += !is_enabled ? "_disabled" : ""; @@ -482,20 +468,26 @@ wxBitmap* PresetComboBox::get_bmp( std::string bitmap_key, const std::string& m if (wxGetApp().dark_mode()) bitmap_key += ",dark"; - wxBitmap* bmp = bitmap_cache().find(bitmap_key); + wxBitmapBundle* bmp = bitmap_cache().find_bndl(bitmap_key); if (bmp == nullptr) { // Create the bitmap with color bars. - std::vector bmps; - bmps.emplace_back(m_type == Preset::TYPE_PRINTER ? create_scaled_bitmap(main_icon_name, this, 16, !is_enabled) : - is_compatible ? m_bitmapCompatible.bmp() : m_bitmapIncompatible.bmp()); + std::vector bmps; + bmps.emplace_back(m_type == Preset::TYPE_PRINTER ? get_bmp_bundle(main_icon_name) : + is_compatible ? m_bitmapCompatible : m_bitmapIncompatible); // Paint a lock at the system presets. - bmps.emplace_back(is_system ? create_scaled_bitmap(next_icon_name, this, 16, !is_enabled) : bitmap_cache().mkclear(norm_icon_width, icon_height)); - bmp = bitmap_cache().insert(bitmap_key, bmps); + bmps.emplace_back(is_system ? get_bmp_bundle(next_icon_name) : get_empty_bmp_bundle(norm_icon_width, icon_height)); + bmp = bitmap_cache().insert_bndl(bitmap_key, bmps); } return bmp; } +wxBitmapBundle PresetComboBox::NullBitmapBndl() +{ + assert(null_icon_width > 0); + return *get_empty_bmp_bundle(null_icon_width, icon_height); +} + bool PresetComboBox::is_selected_physical_printer() { auto selected_item = this->GetSelection(); @@ -782,14 +774,16 @@ void PlaterPresetComboBox::update() // and draw a red flag in front of the selected preset. bool wide_icons = selected_preset && !selected_preset->is_compatible; - std::map nonsys_presets; + null_icon_width = (wide_icons ? 3 : 2) * norm_icon_width + thin_space_icon_width + wide_space_icon_width; + + std::map nonsys_presets; wxString selected_user_preset; wxString tooltip; const std::deque& presets = m_collection->get_presets(); if (!presets.front().is_visible) - this->set_label_marker(this->Append(separator(L("System presets")), wxNullBitmap)); + this->set_label_marker(this->Append(separator(L("System presets")), NullBitmapBndl())); for (size_t i = presets.front().is_visible ? 0 : m_collection->num_default_presets(); i < presets.size(); ++i) { @@ -823,7 +817,7 @@ void PlaterPresetComboBox::update() material_rgb = print_config_def.get("material_colour")->get_default_value()->value; } - wxBitmap* bmp = get_bmp(bitmap_key, wide_icons, bitmap_type_name, + auto bmp = get_bmp(bitmap_key, wide_icons, bitmap_type_name, preset.is_compatible, preset.is_system || preset.is_default, single_bar, filament_rgb, extruder_rgb, material_rgb); assert(bmp); @@ -844,12 +838,12 @@ void PlaterPresetComboBox::update() } } if (i + 1 == m_collection->num_default_presets()) - set_label_marker(Append(separator(L("System presets")), wxNullBitmap)); + set_label_marker(Append(separator(L("System presets")), NullBitmapBndl())); } if (!nonsys_presets.empty()) { - set_label_marker(Append(separator(L("User presets")), wxNullBitmap)); - for (std::map::iterator it = nonsys_presets.begin(); it != nonsys_presets.end(); ++it) { + set_label_marker(Append(separator(L("User presets")), NullBitmapBndl())); + for (std::map::iterator it = nonsys_presets.begin(); it != nonsys_presets.end(); ++it) { Append(it->first, *it->second); validate_selection(it->first == selected_user_preset); } @@ -859,7 +853,7 @@ void PlaterPresetComboBox::update() { // add Physical printers, if any exists if (!m_preset_bundle->physical_printers.empty()) { - set_label_marker(Append(separator(L("Physical printers")), wxNullBitmap)); + set_label_marker(Append(separator(L("Physical printers")), NullBitmapBndl())); const PhysicalPrinterCollection& ph_printers = m_preset_bundle->physical_printers; for (PhysicalPrinterCollection::ConstIterator it = ph_printers.begin(); it != ph_printers.end(); ++it) { @@ -868,7 +862,7 @@ void PlaterPresetComboBox::update() if (!preset || !preset->is_visible) continue; std::string main_icon_name, bitmap_key = main_icon_name = preset->printer_technology() == ptSLA ? "sla_printer" : m_main_bitmap_name; - wxBitmap* bmp = get_bmp(main_icon_name, wide_icons, main_icon_name); + auto bmp = get_bmp(main_icon_name, wide_icons, main_icon_name); assert(bmp); set_label_marker(Append(from_u8(it->get_full_name(preset_name) + suffix(preset)), *bmp), LABEL_ITEM_PHYSICAL_PRINTER); @@ -879,7 +873,7 @@ void PlaterPresetComboBox::update() } if (m_type == Preset::TYPE_PRINTER || m_type == Preset::TYPE_FILAMENT || m_type == Preset::TYPE_SLA_MATERIAL) { - wxBitmap* bmp = get_bmp("edit_preset_list", wide_icons, "edit_uni"); + auto bmp = get_bmp("edit_preset_list", wide_icons, "edit_uni"); assert(bmp); if (m_type == Preset::TYPE_FILAMENT) @@ -916,9 +910,20 @@ void PlaterPresetComboBox::update() void PlaterPresetComboBox::msw_rescale() { PresetComboBox::msw_rescale(); - edit_btn->msw_rescale(); +#ifdef __WXMSW__ + // Use this part of code just on Windows to avoid of some layout issues on Linux + // see https://github.com/prusa3d/PrusaSlicer/issues/5163 and https://github.com/prusa3d/PrusaSlicer/issues/5505 + // Update control min size after rescale (changed Display DPI under MSW) + if (GetMinWidth() != 20 * m_em_unit) + SetMinSize(wxSize(20 * m_em_unit, GetSize().GetHeight())); +#endif //__WXMSW__ } +void PlaterPresetComboBox::sys_color_changed() +{ + PresetComboBox::sys_color_changed(); + edit_btn->sys_color_changed(); +} // --------------------------------- // *** TabPresetComboBox *** @@ -981,10 +986,10 @@ void TabPresetComboBox::update() const std::deque& presets = m_collection->get_presets(); - std::map> nonsys_presets; + std::map> nonsys_presets; wxString selected = ""; if (!presets.front().is_visible) - set_label_marker(Append(separator(L("System presets")), wxNullBitmap)); + set_label_marker(Append(separator(L("System presets")), NullBitmapBndl())); size_t idx_selected = m_collection->get_selected_idx(); if (m_type == Preset::TYPE_PRINTER && m_preset_bundle->physical_printers.has_selection()) { @@ -1011,7 +1016,7 @@ void TabPresetComboBox::update() } std::string main_icon_name = m_type == Preset::TYPE_PRINTER && preset.printer_technology() == ptSLA ? "sla_printer" : m_main_bitmap_name; - wxBitmap* bmp = get_bmp(bitmap_key, main_icon_name, "lock_closed", is_enabled, preset.is_compatible, preset.is_system || preset.is_default); + auto bmp = get_bmp(bitmap_key, main_icon_name, "lock_closed", is_enabled, preset.is_compatible, preset.is_system || preset.is_default); assert(bmp); if (preset.is_default || preset.is_system) { @@ -1022,18 +1027,18 @@ void TabPresetComboBox::update() } else { - std::pair pair(bmp, is_enabled); - nonsys_presets.emplace(get_preset_name(preset), std::pair(bmp, is_enabled)); + std::pair pair(bmp, is_enabled); + nonsys_presets.emplace(get_preset_name(preset), std::pair(bmp, is_enabled)); if (i == idx_selected) selected = get_preset_name(preset); } if (i + 1 == m_collection->num_default_presets()) - set_label_marker(Append(separator(L("System presets")), wxNullBitmap)); + set_label_marker(Append(separator(L("System presets")), NullBitmapBndl())); } if (!nonsys_presets.empty()) { - set_label_marker(Append(separator(L("User presets")), wxNullBitmap)); - for (std::map>::iterator it = nonsys_presets.begin(); it != nonsys_presets.end(); ++it) { + set_label_marker(Append(separator(L("User presets")), NullBitmapBndl())); + for (std::map>::iterator it = nonsys_presets.begin(); it != nonsys_presets.end(); ++it) { int item_id = Append(it->first, *it->second.first); bool is_enabled = it->second.second; if (!is_enabled) @@ -1046,7 +1051,7 @@ void TabPresetComboBox::update() { // add Physical printers, if any exists if (!m_preset_bundle->physical_printers.empty()) { - set_label_marker(Append(separator(L("Physical printers")), wxNullBitmap)); + set_label_marker(Append(separator(L("Physical printers")), NullBitmapBndl())); const PhysicalPrinterCollection& ph_printers = m_preset_bundle->physical_printers; for (PhysicalPrinterCollection::ConstIterator it = ph_printers.begin(); it != ph_printers.end(); ++it) { @@ -1056,7 +1061,7 @@ void TabPresetComboBox::update() continue; std::string main_icon_name = preset->printer_technology() == ptSLA ? "sla_printer" : m_main_bitmap_name; - wxBitmap* bmp = get_bmp(main_icon_name, main_icon_name, "", true, true, false); + auto bmp = get_bmp(main_icon_name, main_icon_name, "", true, true, false); assert(bmp); set_label_marker(Append(from_u8(it->get_full_name(preset_name) + suffix(preset)), *bmp), LABEL_ITEM_PHYSICAL_PRINTER); @@ -1067,7 +1072,7 @@ void TabPresetComboBox::update() // add "Add/Remove printers" item std::string icon_name = "edit_uni"; - wxBitmap* bmp = get_bmp("edit_preset_list, tab,", icon_name, ""); + auto bmp = get_bmp("edit_preset_list, tab,", icon_name, ""); assert(bmp); set_label_marker(Append(separator(L("Add/Remove printers")), *bmp), LABEL_ITEM_WIZARD_PRINTERS); diff --git a/src/slic3r/GUI/PresetComboBoxes.hpp b/src/slic3r/GUI/PresetComboBoxes.hpp index 40a0cfc28..9fe75c126 100644 --- a/src/slic3r/GUI/PresetComboBoxes.hpp +++ b/src/slic3r/GUI/PresetComboBoxes.hpp @@ -1,7 +1,7 @@ #ifndef slic3r_PresetComboBoxes_hpp_ #define slic3r_PresetComboBoxes_hpp_ -//#include +#include #include #include "libslic3r/Preset.hpp" @@ -88,9 +88,9 @@ protected: static BitmapCache& bitmap_cache(); // Indicator, that the preset is compatible with the selected printer. - ScalableBitmap m_bitmapCompatible; + wxBitmapBundle* m_bitmapCompatible; // Indicator, that the preset is NOT compatible with the selected printer. - ScalableBitmap m_bitmapIncompatible; + wxBitmapBundle* m_bitmapIncompatible; int m_last_selected; int m_em_unit; @@ -99,6 +99,7 @@ protected: // parameters for an icon's drawing int icon_height; int norm_icon_width; + int null_icon_width; int thin_icon_width; int wide_icon_width; int space_icon_width; @@ -120,13 +121,15 @@ protected: #endif // __linux__ static wxString separator(const std::string& label); - wxBitmap* get_bmp( std::string bitmap_key, bool wide_icons, const std::string& main_icon_name, + wxBitmapBundle* get_bmp( std::string bitmap_key, bool wide_icons, const std::string& main_icon_name, bool is_compatible = true, bool is_system = false, bool is_single_bar = false, const std::string& filament_rgb = "", const std::string& extruder_rgb = "", const std::string& material_rgb = ""); - wxBitmap* get_bmp( std::string bitmap_key, const std::string& main_icon_name, const std::string& next_icon_name, + wxBitmapBundle* get_bmp( std::string bitmap_key, const std::string& main_icon_name, const std::string& next_icon_name, bool is_enabled = true, bool is_compatible = true, bool is_system = false); + wxBitmapBundle NullBitmapBndl(); + private: void fill_width_height(); }; @@ -155,6 +158,7 @@ public: wxString get_preset_name(const Preset& preset) override; void update() override; void msw_rescale() override; + void sys_color_changed() override; void OnSelect(wxCommandEvent& evt) override; private: diff --git a/src/slic3r/GUI/SavePresetDialog.cpp b/src/slic3r/GUI/SavePresetDialog.cpp index 460b30126..2a2e3bb10 100644 --- a/src/slic3r/GUI/SavePresetDialog.cpp +++ b/src/slic3r/GUI/SavePresetDialog.cpp @@ -56,7 +56,7 @@ SavePresetDialog::Item::Item(Preset::Type type, const std::string& suffix, wxBox wxStaticText* label_top = new wxStaticText(m_parent, wxID_ANY, from_u8((boost::format(_utf8(L("Save %s as:"))) % into_u8(tab->title())).str())); - m_valid_bmp = new wxStaticBitmap(m_parent, wxID_ANY, create_scaled_bitmap("tick_mark", m_parent)); + m_valid_bmp = new wxStaticBitmap(m_parent, wxID_ANY, *get_bmp_bundle("tick_mark")); m_combo = new wxComboBox(m_parent, wxID_ANY, from_u8(preset_name), wxDefaultPosition, wxSize(35 * wxGetApp().em_unit(), -1)); for (const std::string& value : values) @@ -173,7 +173,7 @@ void SavePresetDialog::Item::update_valid_bmp() { std::string bmp_name = m_valid_type == Warning ? "exclamation" : m_valid_type == NoValid ? "cross" : "tick_mark" ; - m_valid_bmp->SetBitmap(create_scaled_bitmap(bmp_name, m_parent)); + m_valid_bmp->SetBitmap(*get_bmp_bundle(bmp_name)); } void SavePresetDialog::Item::accept() diff --git a/src/slic3r/GUI/Search.cpp b/src/slic3r/GUI/Search.cpp index d8c4622ec..52e58193c 100644 --- a/src/slic3r/GUI/Search.cpp +++ b/src/slic3r/GUI/Search.cpp @@ -311,6 +311,9 @@ void OptionsSearcher::check_and_update(PrinterTechnology pt_in, ConfigOptionMode for (auto i : input_values) append_options(i.config, i.type); + + options.insert(options.end(), preferences_options.begin(), preferences_options.end()); + sort_options(); search(search_line, true); @@ -717,12 +720,9 @@ void SearchDialog::msw_rescale() { const int& em = em_unit(); - search_list_model->msw_rescale(); search_list->GetColumn(SearchListModel::colIcon )->SetWidth(3 * em); search_list->GetColumn(SearchListModel::colMarkedText)->SetWidth(45 * em); - msw_buttons_rescale(this, em, { wxID_CANCEL }); - const wxSize& size = wxSize(40 * em, 30 * em); SetMinSize(size); @@ -740,7 +740,7 @@ void SearchDialog::on_sys_color_changed() #endif // msw_rescale updates just icons, so use it - search_list_model->msw_rescale(); + search_list_model->sys_color_changed(); Refresh(); } @@ -773,10 +773,10 @@ void SearchListModel::Prepend(const std::string& label) RowPrepended(); } -void SearchListModel::msw_rescale() +void SearchListModel::sys_color_changed() { for (ScalableBitmap& bmp : m_icon) - bmp.msw_rescale(); + bmp.sys_color_changed(); } wxString SearchListModel::GetColumnType(unsigned int col) const @@ -792,7 +792,7 @@ void SearchListModel::GetValueByRow(wxVariant& variant, switch (col) { case colIcon: - variant << m_icon[m_values[row].second].bmp(); + variant << m_icon[m_values[row].second].bmp().GetBitmapFor(m_icon[m_values[row].second].parent()); break; case colMarkedText: variant = m_values[row].first; diff --git a/src/slic3r/GUI/Search.hpp b/src/slic3r/GUI/Search.hpp index 49866d066..0d7851132 100644 --- a/src/slic3r/GUI/Search.hpp +++ b/src/slic3r/GUI/Search.hpp @@ -213,7 +213,7 @@ public: void Clear(); void Prepend(const std::string& text); - void msw_rescale(); + void sys_color_changed(); // implementation of base class virtuals to define model diff --git a/src/slic3r/GUI/Selection.cpp b/src/slic3r/GUI/Selection.cpp index 7ccc6fc25..271435c99 100644 --- a/src/slic3r/GUI/Selection.cpp +++ b/src/slic3r/GUI/Selection.cpp @@ -1,2765 +1,3370 @@ -#include "libslic3r/libslic3r.h" -#include "Selection.hpp" - -#include "3DScene.hpp" -#include "GLCanvas3D.hpp" -#include "GUI_App.hpp" -#include "GUI.hpp" -#include "GUI_ObjectManipulation.hpp" -#include "GUI_ObjectList.hpp" -#include "Gizmos/GLGizmoBase.hpp" -#include "Camera.hpp" -#include "Plater.hpp" -#include "slic3r/Utils/UndoRedo.hpp" - -#include "libslic3r/LocalesUtils.hpp" -#include "libslic3r/Model.hpp" -#include "libslic3r/PresetBundle.hpp" -#include "libslic3r/BuildVolume.hpp" - -#include - -#include -#include - -static const Slic3r::ColorRGBA UNIFORM_SCALE_COLOR = Slic3r::ColorRGBA::ORANGE(); -static const Slic3r::ColorRGBA SOLID_PLANE_COLOR = Slic3r::ColorRGBA::ORANGE(); -static const Slic3r::ColorRGBA TRANSPARENT_PLANE_COLOR = { 0.8f, 0.8f, 0.8f, 0.5f }; - -namespace Slic3r { -namespace GUI { - -Selection::VolumeCache::TransformCache::TransformCache() - : position(Vec3d::Zero()) - , rotation(Vec3d::Zero()) - , scaling_factor(Vec3d::Ones()) - , mirror(Vec3d::Ones()) - , rotation_matrix(Transform3d::Identity()) - , scale_matrix(Transform3d::Identity()) - , mirror_matrix(Transform3d::Identity()) - , full_matrix(Transform3d::Identity()) -{ -} - -Selection::VolumeCache::TransformCache::TransformCache(const Geometry::Transformation& transform) - : position(transform.get_offset()) - , rotation(transform.get_rotation()) - , scaling_factor(transform.get_scaling_factor()) - , mirror(transform.get_mirror()) - , full_matrix(transform.get_matrix()) -{ - rotation_matrix = Geometry::assemble_transform(Vec3d::Zero(), rotation); - scale_matrix = Geometry::assemble_transform(Vec3d::Zero(), Vec3d::Zero(), scaling_factor); - mirror_matrix = Geometry::assemble_transform(Vec3d::Zero(), Vec3d::Zero(), Vec3d::Ones(), mirror); -} - -Selection::VolumeCache::VolumeCache(const Geometry::Transformation& volume_transform, const Geometry::Transformation& instance_transform) - : m_volume(volume_transform) - , m_instance(instance_transform) -{ -} - -bool Selection::Clipboard::is_sla_compliant() const -{ - if (m_mode == Selection::Volume) - return false; - - for (const ModelObject* o : m_model->objects) { - if (o->is_multiparts()) - return false; - - for (const ModelVolume* v : o->volumes) { - if (v->is_modifier()) - return false; - } - } - - return true; -} - -Selection::Clipboard::Clipboard() -{ - m_model.reset(new Model); -} - -void Selection::Clipboard::reset() -{ - m_model->clear_objects(); -} - -bool Selection::Clipboard::is_empty() const -{ - return m_model->objects.empty(); -} - -ModelObject* Selection::Clipboard::add_object() -{ - return m_model->add_object(); -} - -ModelObject* Selection::Clipboard::get_object(unsigned int id) -{ - return (id < (unsigned int)m_model->objects.size()) ? m_model->objects[id] : nullptr; -} - -const ModelObjectPtrs& Selection::Clipboard::get_objects() const -{ - return m_model->objects; -} - -Selection::Selection() - : m_volumes(nullptr) - , m_model(nullptr) - , m_enabled(false) - , m_mode(Instance) - , m_type(Empty) - , m_valid(false) - , m_scale_factor(1.0f) -{ - this->set_bounding_boxes_dirty(); -} - - -void Selection::set_volumes(GLVolumePtrs* volumes) -{ - m_volumes = volumes; - update_valid(); -} - -// Init shall be called from the OpenGL render function, so that the OpenGL context is initialized! -bool Selection::init() -{ - m_arrow.init_from(straight_arrow(10.0f, 5.0f, 5.0f, 10.0f, 1.0f)); - m_curved_arrow.init_from(circular_arrow(16, 10.0f, 5.0f, 10.0f, 5.0f, 1.0f)); -#if ENABLE_RENDER_SELECTION_CENTER - m_vbo_sphere.init_from(its_make_sphere(0.75, PI / 12.0)); -#endif // ENABLE_RENDER_SELECTION_CENTER - - return true; -} - -void Selection::set_model(Model* model) -{ - m_model = model; - update_valid(); -} - -void Selection::add(unsigned int volume_idx, bool as_single_selection, bool check_for_already_contained) -{ - if (!m_valid || (unsigned int)m_volumes->size() <= volume_idx) - return; - - const GLVolume* volume = (*m_volumes)[volume_idx]; - // wipe tower is already selected - if (is_wipe_tower() && volume->is_wipe_tower) - return; - - bool keep_instance_mode = (m_mode == Instance) && !as_single_selection; - bool already_contained = check_for_already_contained && contains_volume(volume_idx); - - // resets the current list if needed - bool needs_reset = as_single_selection && !already_contained; - needs_reset |= volume->is_wipe_tower; - needs_reset |= is_wipe_tower() && !volume->is_wipe_tower; - needs_reset |= as_single_selection && !is_any_modifier() && volume->is_modifier; - needs_reset |= is_any_modifier() && !volume->is_modifier; - - if (!already_contained || needs_reset) { - wxGetApp().plater()->take_snapshot(_L("Selection-Add"), UndoRedo::SnapshotType::Selection); - - if (needs_reset) - clear(); - - if (!keep_instance_mode) - m_mode = volume->is_modifier ? Volume : Instance; - } - else - // keep current mode - return; - - switch (m_mode) - { - case Volume: - { - if (volume->volume_idx() >= 0 && (is_empty() || volume->instance_idx() == get_instance_idx())) - do_add_volume(volume_idx); - - break; - } - case Instance: - { - Plater::SuppressSnapshots suppress(wxGetApp().plater()); - add_instance(volume->object_idx(), volume->instance_idx(), as_single_selection); - break; - } - } - - update_type(); - this->set_bounding_boxes_dirty(); -} - -void Selection::remove(unsigned int volume_idx) -{ - if (!m_valid || (unsigned int)m_volumes->size() <= volume_idx) - return; - - if (!contains_volume(volume_idx)) - return; - - wxGetApp().plater()->take_snapshot(_L("Selection-Remove"), UndoRedo::SnapshotType::Selection); - - GLVolume* volume = (*m_volumes)[volume_idx]; - - switch (m_mode) - { - case Volume: - { - do_remove_volume(volume_idx); - break; - } - case Instance: - { - do_remove_instance(volume->object_idx(), volume->instance_idx()); - break; - } - } - - update_type(); - this->set_bounding_boxes_dirty(); -} - -void Selection::add_object(unsigned int object_idx, bool as_single_selection) -{ - if (!m_valid) - return; - - std::vector volume_idxs = get_volume_idxs_from_object(object_idx); - if ((!as_single_selection && contains_all_volumes(volume_idxs)) || - (as_single_selection && matches(volume_idxs))) - return; - - wxGetApp().plater()->take_snapshot(_L("Selection-Add Object"), UndoRedo::SnapshotType::Selection); - - // resets the current list if needed - if (as_single_selection) - clear(); - - m_mode = Instance; - - do_add_volumes(volume_idxs); - - update_type(); - this->set_bounding_boxes_dirty(); -} - -void Selection::remove_object(unsigned int object_idx) -{ - if (!m_valid) - return; - - wxGetApp().plater()->take_snapshot(_L("Selection-Remove Object"), UndoRedo::SnapshotType::Selection); - - do_remove_object(object_idx); - - update_type(); - this->set_bounding_boxes_dirty(); -} - -void Selection::add_instance(unsigned int object_idx, unsigned int instance_idx, bool as_single_selection) -{ - if (!m_valid) - return; - - const std::vector volume_idxs = get_volume_idxs_from_instance(object_idx, instance_idx); - if ((!as_single_selection && contains_all_volumes(volume_idxs)) || - (as_single_selection && matches(volume_idxs))) - return; - - wxGetApp().plater()->take_snapshot(_L("Selection-Add Instance"), UndoRedo::SnapshotType::Selection); - - // resets the current list if needed - if (as_single_selection) - clear(); - - m_mode = Instance; - - do_add_volumes(volume_idxs); - - update_type(); - this->set_bounding_boxes_dirty(); -} - -void Selection::remove_instance(unsigned int object_idx, unsigned int instance_idx) -{ - if (!m_valid) - return; - - wxGetApp().plater()->take_snapshot(_L("Selection-Remove Instance"), UndoRedo::SnapshotType::Selection); - - do_remove_instance(object_idx, instance_idx); - - update_type(); - this->set_bounding_boxes_dirty(); -} - -void Selection::add_volume(unsigned int object_idx, unsigned int volume_idx, int instance_idx, bool as_single_selection) -{ - if (!m_valid) - return; - - std::vector volume_idxs = get_volume_idxs_from_volume(object_idx, instance_idx, volume_idx); - if ((!as_single_selection && contains_all_volumes(volume_idxs)) || - (as_single_selection && matches(volume_idxs))) - return; - - // resets the current list if needed - if (as_single_selection) - clear(); - - m_mode = Volume; - - do_add_volumes(volume_idxs); - - update_type(); - this->set_bounding_boxes_dirty(); -} - -void Selection::remove_volume(unsigned int object_idx, unsigned int volume_idx) -{ - if (!m_valid) - return; - - for (unsigned int i = 0; i < (unsigned int)m_volumes->size(); ++i) { - GLVolume* v = (*m_volumes)[i]; - if (v->object_idx() == (int)object_idx && v->volume_idx() == (int)volume_idx) - do_remove_volume(i); - } - - update_type(); - this->set_bounding_boxes_dirty(); -} - -void Selection::add_volumes(EMode mode, const std::vector& volume_idxs, bool as_single_selection) -{ - if (!m_valid) - return; - - if ((!as_single_selection && contains_all_volumes(volume_idxs)) || - (as_single_selection && matches(volume_idxs))) - return; - - // resets the current list if needed - if (as_single_selection) - clear(); - - m_mode = mode; - for (unsigned int i : volume_idxs) { - if (i < (unsigned int)m_volumes->size()) - do_add_volume(i); - } - - update_type(); - this->set_bounding_boxes_dirty(); -} - -void Selection::remove_volumes(EMode mode, const std::vector& volume_idxs) -{ - if (!m_valid) - return; - - m_mode = mode; - for (unsigned int i : volume_idxs) { - if (i < (unsigned int)m_volumes->size()) - do_remove_volume(i); - } - - update_type(); - this->set_bounding_boxes_dirty(); -} - -void Selection::add_all() -{ - if (!m_valid) - return; - - unsigned int count = 0; - for (unsigned int i = 0; i < (unsigned int)m_volumes->size(); ++i) { - if (!(*m_volumes)[i]->is_wipe_tower) - ++count; - } - - if ((unsigned int)m_list.size() == count) - return; - - wxGetApp().plater()->take_snapshot(_(L("Selection-Add All")), UndoRedo::SnapshotType::Selection); - - m_mode = Instance; - clear(); - - for (unsigned int i = 0; i < (unsigned int)m_volumes->size(); ++i) { - if (!(*m_volumes)[i]->is_wipe_tower) - do_add_volume(i); - } - - update_type(); - this->set_bounding_boxes_dirty(); -} - -void Selection::remove_all() -{ - if (!m_valid) - return; - - if (is_empty()) - return; - -// Not taking the snapshot with non-empty Redo stack will likely be more confusing than losing the Redo stack. -// Let's wait for user feedback. -// if (!wxGetApp().plater()->can_redo()) - wxGetApp().plater()->take_snapshot(_L("Selection-Remove All"), UndoRedo::SnapshotType::Selection); - - m_mode = Instance; - clear(); -} - -void Selection::set_deserialized(EMode mode, const std::vector> &volumes_and_instances) -{ - if (! m_valid) - return; - - m_mode = mode; - for (unsigned int i : m_list) - (*m_volumes)[i]->selected = false; - m_list.clear(); - for (unsigned int i = 0; i < (unsigned int)m_volumes->size(); ++ i) - if (std::binary_search(volumes_and_instances.begin(), volumes_and_instances.end(), (*m_volumes)[i]->geometry_id)) - do_add_volume(i); - update_type(); - set_bounding_boxes_dirty(); -} - -void Selection::clear() -{ - if (!m_valid) - return; - - if (m_list.empty()) - return; - - // ensure that the volumes get the proper color before next call to render (expecially needed for transparent volumes) - for (unsigned int i : m_list) { - GLVolume& volume = *(*m_volumes)[i]; - volume.selected = false; - volume.set_render_color(volume.color.is_transparent()); - } - - m_list.clear(); - - update_type(); - set_bounding_boxes_dirty(); - - // this happens while the application is closing - if (wxGetApp().obj_manipul() == nullptr) - return; - - // resets the cache in the sidebar - wxGetApp().obj_manipul()->reset_cache(); - - // #et_FIXME fake KillFocus from sidebar - wxGetApp().plater()->canvas3D()->handle_sidebar_focus_event("", false); -} - -// Update the selection based on the new instance IDs. -void Selection::instances_changed(const std::vector &instance_ids_selected) -{ - assert(m_valid); - assert(m_mode == Instance); - m_list.clear(); - for (unsigned int volume_idx = 0; volume_idx < (unsigned int)m_volumes->size(); ++ volume_idx) { - const GLVolume *volume = (*m_volumes)[volume_idx]; - auto it = std::lower_bound(instance_ids_selected.begin(), instance_ids_selected.end(), volume->geometry_id.second); - if (it != instance_ids_selected.end() && *it == volume->geometry_id.second) - this->do_add_volume(volume_idx); - } - update_type(); - this->set_bounding_boxes_dirty(); -} - -// Update the selection based on the map from old indices to new indices after m_volumes changed. -// If the current selection is by instance, this call may select newly added volumes, if they belong to already selected instances. -void Selection::volumes_changed(const std::vector &map_volume_old_to_new) -{ - assert(m_valid); - assert(m_mode == Volume); - IndicesList list_new; - for (unsigned int idx : m_list) - if (map_volume_old_to_new[idx] != size_t(-1)) { - unsigned int new_idx = (unsigned int)map_volume_old_to_new[idx]; - (*m_volumes)[new_idx]->selected = true; - list_new.insert(new_idx); - } - m_list = std::move(list_new); - update_type(); - this->set_bounding_boxes_dirty(); -} - -bool Selection::is_single_full_instance() const -{ - if (m_type == SingleFullInstance) - return true; - - if (m_type == SingleFullObject) - return get_instance_idx() != -1; - - if (m_list.empty() || m_volumes->empty()) - return false; - - int object_idx = m_valid ? get_object_idx() : -1; - if (object_idx < 0 || (int)m_model->objects.size() <= object_idx) - return false; - - int instance_idx = (*m_volumes)[*m_list.begin()]->instance_idx(); - - std::set volumes_idxs; - for (unsigned int i : m_list) { - const GLVolume* v = (*m_volumes)[i]; - if (object_idx != v->object_idx() || instance_idx != v->instance_idx()) - return false; - - int volume_idx = v->volume_idx(); - if (volume_idx >= 0) - volumes_idxs.insert(volume_idx); - } - - return m_model->objects[object_idx]->volumes.size() == volumes_idxs.size(); -} - -bool Selection::is_from_single_object() const -{ - const int idx = get_object_idx(); -#if ENABLE_WIPETOWER_OBJECTID_1000_REMOVAL - return 0 <= idx && idx < int(m_model->objects.size()); -#else - return 0 <= idx && idx < 1000; -#endif // ENABLE_WIPETOWER_OBJECTID_1000_REMOVAL -} - -bool Selection::is_sla_compliant() const -{ - if (m_mode == Volume) - return false; - - for (unsigned int i : m_list) { - if ((*m_volumes)[i]->is_modifier) - return false; - } - - return true; -} - -bool Selection::contains_all_volumes(const std::vector& volume_idxs) const -{ - for (unsigned int i : volume_idxs) { - if (m_list.find(i) == m_list.end()) - return false; - } - - return true; -} - -bool Selection::contains_any_volume(const std::vector& volume_idxs) const -{ - for (unsigned int i : volume_idxs) { - if (m_list.find(i) != m_list.end()) - return true; - } - - return false; -} - -bool Selection::matches(const std::vector& volume_idxs) const -{ - unsigned int count = 0; - - for (unsigned int i : volume_idxs) { - if (m_list.find(i) != m_list.end()) - ++count; - else - return false; - } - - return count == (unsigned int)m_list.size(); -} - -bool Selection::requires_uniform_scale() const -{ - if (is_single_full_instance() || is_single_modifier() || is_single_volume()) - return false; - - return true; -} - -int Selection::get_object_idx() const -{ - return (m_cache.content.size() == 1) ? m_cache.content.begin()->first : -1; -} - -int Selection::get_instance_idx() const -{ - if (m_cache.content.size() == 1) { - const InstanceIdxsList& idxs = m_cache.content.begin()->second; - if (idxs.size() == 1) - return *idxs.begin(); - } - - return -1; -} - -const Selection::InstanceIdxsList& Selection::get_instance_idxs() const -{ - assert(m_cache.content.size() == 1); - return m_cache.content.begin()->second; -} - -const GLVolume* Selection::get_volume(unsigned int volume_idx) const -{ - return (m_valid && (volume_idx < (unsigned int)m_volumes->size())) ? (*m_volumes)[volume_idx] : nullptr; -} - -const BoundingBoxf3& Selection::get_bounding_box() const -{ - if (!m_bounding_box.has_value()) { - std::optional* bbox = const_cast*>(&m_bounding_box); - *bbox = BoundingBoxf3(); - if (m_valid) { - for (unsigned int i : m_list) { - (*bbox)->merge((*m_volumes)[i]->transformed_convex_hull_bounding_box()); - } - } - } - return *m_bounding_box; -} - -const BoundingBoxf3& Selection::get_unscaled_instance_bounding_box() const -{ - if (!m_unscaled_instance_bounding_box.has_value()) { - std::optional* bbox = const_cast*>(&m_unscaled_instance_bounding_box); - *bbox = BoundingBoxf3(); - if (m_valid) { - for (unsigned int i : m_list) { - const GLVolume& volume = *(*m_volumes)[i]; - if (volume.is_modifier) - continue; - Transform3d trafo = volume.get_instance_transformation().get_matrix(false, false, true, false) * volume.get_volume_transformation().get_matrix(); - trafo.translation().z() += volume.get_sla_shift_z(); - (*bbox)->merge(volume.transformed_convex_hull_bounding_box(trafo)); - } - } - } - return *m_unscaled_instance_bounding_box; -} - -const BoundingBoxf3& Selection::get_scaled_instance_bounding_box() const -{ - if (!m_scaled_instance_bounding_box.has_value()) { - std::optional* bbox = const_cast*>(&m_scaled_instance_bounding_box); - *bbox = BoundingBoxf3(); - if (m_valid) { - for (unsigned int i : m_list) { - const GLVolume& volume = *(*m_volumes)[i]; - if (volume.is_modifier) - continue; - Transform3d trafo = volume.get_instance_transformation().get_matrix(false, false, false, false) * volume.get_volume_transformation().get_matrix(); - trafo.translation().z() += volume.get_sla_shift_z(); - (*bbox)->merge(volume.transformed_convex_hull_bounding_box(trafo)); - } - } - } - return *m_scaled_instance_bounding_box; -} - -void Selection::setup_cache() -{ - if (!m_valid) - return; - - set_caches(); -} - -void Selection::translate(const Vec3d& displacement, bool local) -{ - if (!m_valid) - return; - - EMode translation_type = m_mode; - - for (unsigned int i : m_list) { - GLVolume& v = *(*m_volumes)[i]; - if (m_mode == Volume || v.is_wipe_tower) { - if (local) - v.set_volume_offset(m_cache.volumes_data[i].get_volume_position() + displacement); - else { - const Vec3d local_displacement = (m_cache.volumes_data[i].get_instance_rotation_matrix() * m_cache.volumes_data[i].get_instance_scale_matrix() * m_cache.volumes_data[i].get_instance_mirror_matrix()).inverse() * displacement; - v.set_volume_offset(m_cache.volumes_data[i].get_volume_position() + local_displacement); - } - } - else if (m_mode == Instance) { - if (is_from_fully_selected_instance(i)) - v.set_instance_offset(m_cache.volumes_data[i].get_instance_position() + displacement); - else { - const Vec3d local_displacement = (m_cache.volumes_data[i].get_instance_rotation_matrix() * m_cache.volumes_data[i].get_instance_scale_matrix() * m_cache.volumes_data[i].get_instance_mirror_matrix()).inverse() * displacement; - v.set_volume_offset(m_cache.volumes_data[i].get_volume_position() + local_displacement); - translation_type = Volume; - } - } - } - -#if !DISABLE_INSTANCES_SYNCH - if (translation_type == Instance) - synchronize_unselected_instances(SYNC_ROTATION_NONE); - else if (translation_type == Volume) - synchronize_unselected_volumes(); -#endif // !DISABLE_INSTANCES_SYNCH - - ensure_not_below_bed(); - set_bounding_boxes_dirty(); - wxGetApp().plater()->canvas3D()->requires_check_outside_state(); -} - -// Rotate an object around one of the axes. Only one rotation component is expected to be changing. -void Selection::rotate(const Vec3d& rotation, TransformationType transformation_type) -{ - if (!m_valid) - return; - - // Only relative rotation values are allowed in the world coordinate system. - assert(!transformation_type.world() || transformation_type.relative()); - - if (!is_wipe_tower()) { - int rot_axis_max = 0; - if (rotation.isApprox(Vec3d::Zero())) { - for (unsigned int i : m_list) { - GLVolume &v = *(*m_volumes)[i]; - if (m_mode == Instance) { - v.set_instance_rotation(m_cache.volumes_data[i].get_instance_rotation()); - v.set_instance_offset(m_cache.volumes_data[i].get_instance_position()); - } - else if (m_mode == Volume) { - v.set_volume_rotation(m_cache.volumes_data[i].get_volume_rotation()); - v.set_volume_offset(m_cache.volumes_data[i].get_volume_position()); - } - } - } - else { // this is not the wipe tower - //FIXME this does not work for absolute rotations (transformation_type.absolute() is true) - rotation.cwiseAbs().maxCoeff(&rot_axis_max); - -// if ( single instance or single volume ) - // Rotate around center , if only a single object or volume -// transformation_type.set_independent(); - - // For generic rotation, we want to rotate the first volume in selection, and then to synchronize the other volumes with it. - std::vector object_instance_first(m_model->objects.size(), -1); - auto rotate_instance = [this, &rotation, &object_instance_first, rot_axis_max, transformation_type](GLVolume &volume, int i) { - const int first_volume_idx = object_instance_first[volume.object_idx()]; - if (rot_axis_max != 2 && first_volume_idx != -1) { - // Generic rotation, but no rotation around the Z axis. - // Always do a local rotation (do not consider the selection to be a rigid body). - assert(is_approx(rotation.z(), 0.0)); - const GLVolume &first_volume = *(*m_volumes)[first_volume_idx]; - const Vec3d &rotation = first_volume.get_instance_rotation(); - const double z_diff = Geometry::rotation_diff_z(m_cache.volumes_data[first_volume_idx].get_instance_rotation(), m_cache.volumes_data[i].get_instance_rotation()); - volume.set_instance_rotation(Vec3d(rotation(0), rotation(1), rotation(2) + z_diff)); - } - else { - // extracts rotations from the composed transformation - Vec3d new_rotation = transformation_type.world() ? - Geometry::extract_euler_angles(Geometry::assemble_transform(Vec3d::Zero(), rotation) * m_cache.volumes_data[i].get_instance_rotation_matrix()) : - transformation_type.absolute() ? rotation : rotation + m_cache.volumes_data[i].get_instance_rotation(); - if (rot_axis_max == 2 && transformation_type.joint()) { - // Only allow rotation of multiple instances as a single rigid body when rotating around the Z axis. - const double z_diff = Geometry::rotation_diff_z(m_cache.volumes_data[i].get_instance_rotation(), new_rotation); - volume.set_instance_offset(m_cache.dragging_center + Eigen::AngleAxisd(z_diff, Vec3d::UnitZ()) * (m_cache.volumes_data[i].get_instance_position() - m_cache.dragging_center)); - } - volume.set_instance_rotation(new_rotation); - object_instance_first[volume.object_idx()] = i; - } - }; - - for (unsigned int i : m_list) { - GLVolume &v = *(*m_volumes)[i]; - if (is_single_full_instance()) - rotate_instance(v, i); - else if (is_single_volume() || is_single_modifier()) { - if (transformation_type.independent()) - v.set_volume_rotation(m_cache.volumes_data[i].get_volume_rotation() + rotation); - else { - const Transform3d m = Geometry::assemble_transform(Vec3d::Zero(), rotation); - const Vec3d new_rotation = Geometry::extract_euler_angles(m * m_cache.volumes_data[i].get_volume_rotation_matrix()); - v.set_volume_rotation(new_rotation); - } - } - else - { - if (m_mode == Instance) - rotate_instance(v, i); - else if (m_mode == Volume) { - // extracts rotations from the composed transformation - Transform3d m = Geometry::assemble_transform(Vec3d::Zero(), rotation); - Vec3d new_rotation = Geometry::extract_euler_angles(m * m_cache.volumes_data[i].get_volume_rotation_matrix()); - if (transformation_type.joint()) { - const Vec3d local_pivot = m_cache.volumes_data[i].get_instance_full_matrix().inverse() * m_cache.dragging_center; - const Vec3d offset = m * (m_cache.volumes_data[i].get_volume_position() - local_pivot); - v.set_volume_offset(local_pivot + offset); - } - v.set_volume_rotation(new_rotation); - } - } - } - } - - #if !DISABLE_INSTANCES_SYNCH - if (m_mode == Instance) - synchronize_unselected_instances((rot_axis_max == 2) ? SYNC_ROTATION_NONE : SYNC_ROTATION_GENERAL); - else if (m_mode == Volume) - synchronize_unselected_volumes(); - #endif // !DISABLE_INSTANCES_SYNCH - } - else { // it's the wipe tower that's selected and being rotated - GLVolume& volume = *((*m_volumes)[*m_list.begin()]); // the wipe tower is always alone in the selection - - // make sure the wipe tower rotates around its center, not origin - // we can assume that only Z rotation changes - const Vec3d center_local = volume.transformed_bounding_box().center() - volume.get_volume_offset(); - const Vec3d center_local_new = Eigen::AngleAxisd(rotation(2)-volume.get_volume_rotation()(2), Vec3d(0.0, 0.0, 1.0)) * center_local; - volume.set_volume_rotation(rotation); - volume.set_volume_offset(volume.get_volume_offset() + center_local - center_local_new); - } - - set_bounding_boxes_dirty(); - wxGetApp().plater()->canvas3D()->requires_check_outside_state(); -} - -void Selection::flattening_rotate(const Vec3d& normal) -{ - // We get the normal in untransformed coordinates. We must transform it using the instance matrix, find out - // how to rotate the instance so it faces downwards and do the rotation. All that for all selected instances. - // The function assumes that is_from_single_object() holds. - assert(Slic3r::is_approx(normal.norm(), 1.)); - - if (!m_valid) - return; - - for (unsigned int i : m_list) { - GLVolume& v = *(*m_volumes)[i]; - // Normal transformed from the object coordinate space to the world coordinate space. - const auto &voldata = m_cache.volumes_data[i]; - Vec3d tnormal = (Geometry::assemble_transform( - Vec3d::Zero(), voldata.get_instance_rotation(), - voldata.get_instance_scaling_factor().cwiseInverse(), voldata.get_instance_mirror()) * normal).normalized(); - // Additional rotation to align tnormal with the down vector in the world coordinate space. - auto extra_rotation = Eigen::Quaterniond().setFromTwoVectors(tnormal, - Vec3d::UnitZ()); - v.set_instance_rotation(Geometry::extract_euler_angles(extra_rotation.toRotationMatrix() * m_cache.volumes_data[i].get_instance_rotation_matrix())); - } - -#if !DISABLE_INSTANCES_SYNCH - // Apply the same transformation also to other instances, - // but respect their possibly diffrent z-rotation. - if (m_mode == Instance) - synchronize_unselected_instances(SYNC_ROTATION_GENERAL); -#endif // !DISABLE_INSTANCES_SYNCH - - this->set_bounding_boxes_dirty(); -} - -void Selection::scale(const Vec3d& scale, TransformationType transformation_type) -{ - if (!m_valid) - return; - - for (unsigned int i : m_list) { - GLVolume &v = *(*m_volumes)[i]; - if (is_single_full_instance()) { - if (transformation_type.relative()) { - Transform3d m = Geometry::assemble_transform(Vec3d::Zero(), Vec3d::Zero(), scale); - Eigen::Matrix new_matrix = (m * m_cache.volumes_data[i].get_instance_scale_matrix()).matrix().block(0, 0, 3, 3); - // extracts scaling factors from the composed transformation - Vec3d new_scale(new_matrix.col(0).norm(), new_matrix.col(1).norm(), new_matrix.col(2).norm()); - if (transformation_type.joint()) - v.set_instance_offset(m_cache.dragging_center + m * (m_cache.volumes_data[i].get_instance_position() - m_cache.dragging_center)); - - v.set_instance_scaling_factor(new_scale); - } - else { - if (transformation_type.world() && (std::abs(scale.x() - scale.y()) > EPSILON || std::abs(scale.x() - scale.z()) > EPSILON)) { - // Non-uniform scaling. Transform the scaling factors into the local coordinate system. - // This is only possible, if the instance rotation is mulitples of ninety degrees. - assert(Geometry::is_rotation_ninety_degrees(v.get_instance_rotation())); - v.set_instance_scaling_factor((v.get_instance_transformation().get_matrix(true, false, true, true).matrix().block<3, 3>(0, 0).transpose() * scale).cwiseAbs()); - } - else - v.set_instance_scaling_factor(scale); - } - } - else if (is_single_volume() || is_single_modifier()) - v.set_volume_scaling_factor(scale); - else { - Transform3d m = Geometry::assemble_transform(Vec3d::Zero(), Vec3d::Zero(), scale); - if (m_mode == Instance) { - Eigen::Matrix new_matrix = (m * m_cache.volumes_data[i].get_instance_scale_matrix()).matrix().block(0, 0, 3, 3); - // extracts scaling factors from the composed transformation - Vec3d new_scale(new_matrix.col(0).norm(), new_matrix.col(1).norm(), new_matrix.col(2).norm()); - if (transformation_type.joint()) - v.set_instance_offset(m_cache.dragging_center + m * (m_cache.volumes_data[i].get_instance_position() - m_cache.dragging_center)); - - v.set_instance_scaling_factor(new_scale); - } - else if (m_mode == Volume) { - Eigen::Matrix new_matrix = (m * m_cache.volumes_data[i].get_volume_scale_matrix()).matrix().block(0, 0, 3, 3); - // extracts scaling factors from the composed transformation - Vec3d new_scale(new_matrix.col(0).norm(), new_matrix.col(1).norm(), new_matrix.col(2).norm()); - if (transformation_type.joint()) { - Vec3d offset = m * (m_cache.volumes_data[i].get_volume_position() + m_cache.volumes_data[i].get_instance_position() - m_cache.dragging_center); - v.set_volume_offset(m_cache.dragging_center - m_cache.volumes_data[i].get_instance_position() + offset); - } - v.set_volume_scaling_factor(new_scale); - } - } - } - -#if !DISABLE_INSTANCES_SYNCH - if (m_mode == Instance) - synchronize_unselected_instances(SYNC_ROTATION_NONE); - else if (m_mode == Volume) - synchronize_unselected_volumes(); -#endif // !DISABLE_INSTANCES_SYNCH - - ensure_on_bed(); - set_bounding_boxes_dirty(); - wxGetApp().plater()->canvas3D()->requires_check_outside_state(); -} - -void Selection::scale_to_fit_print_volume(const BuildVolume& volume) -{ - auto fit = [this](double s, Vec3d offset) { - if (s <= 0.0 || s == 1.0) - return; - - wxGetApp().plater()->take_snapshot(_L("Scale To Fit")); - - TransformationType type; - type.set_world(); - type.set_relative(); - type.set_joint(); - - // apply scale - setup_cache(); - scale(s * Vec3d::Ones(), type); - wxGetApp().plater()->canvas3D()->do_scale(""); // avoid storing another snapshot - - // center selection on print bed - setup_cache(); - offset.z() = -get_bounding_box().min.z(); - translate(offset); - wxGetApp().plater()->canvas3D()->do_move(""); // avoid storing another snapshot - - wxGetApp().obj_manipul()->set_dirty(); - }; - - auto fit_rectangle = [this, fit](const BuildVolume& volume) { - const BoundingBoxf3 print_volume = volume.bounding_volume(); - const Vec3d print_volume_size = print_volume.size(); - - // adds 1/100th of a mm on all sides to avoid false out of print volume detections due to floating-point roundings - const Vec3d box_size = get_bounding_box().size() + 0.02 * Vec3d::Ones(); - - const double sx = (box_size.x() != 0.0) ? print_volume_size.x() / box_size.x() : 0.0; - const double sy = (box_size.y() != 0.0) ? print_volume_size.y() / box_size.y() : 0.0; - const double sz = (box_size.z() != 0.0) ? print_volume_size.z() / box_size.z() : 0.0; - - if (sx != 0.0 && sy != 0.0 && sz != 0.0) - fit(std::min(sx, std::min(sy, sz)), print_volume.center() - get_bounding_box().center()); - }; - - auto fit_circle = [this, fit](const BuildVolume& volume) { - const Geometry::Circled& print_circle = volume.circle(); - double print_circle_radius = unscale(print_circle.radius); - - if (print_circle_radius == 0.0) - return; - - Points points; - double max_z = 0.0; - for (unsigned int i : m_list) { - const GLVolume& v = *(*m_volumes)[i]; - TriangleMesh hull_3d = *v.convex_hull(); - hull_3d.transform(v.world_matrix()); - max_z = std::max(max_z, hull_3d.bounding_box().size().z()); - const Polygon hull_2d = hull_3d.convex_hull(); - points.insert(points.end(), hull_2d.begin(), hull_2d.end()); - } - - if (points.empty()) - return; - - const Geometry::Circled circle = Geometry::smallest_enclosing_circle_welzl(points); - // adds 1/100th of a mm on all sides to avoid false out of print volume detections due to floating-point roundings - const double circle_radius = unscale(circle.radius) + 0.01; - - if (circle_radius == 0.0 || max_z == 0.0) - return; - - const double s = std::min(print_circle_radius / circle_radius, volume.max_print_height() / max_z); - const Vec3d sel_center = get_bounding_box().center(); - const Vec3d offset = s * (Vec3d(unscale(circle.center.x()), unscale(circle.center.y()), 0.5 * max_z) - sel_center); - const Vec3d print_center = { unscale(print_circle.center.x()), unscale(print_circle.center.y()), 0.5 * volume.max_print_height() }; - fit(s, print_center - (sel_center + offset)); - }; - - if (is_empty() || m_mode == Volume) - return; - - switch (volume.type()) - { - case BuildVolume::Type::Rectangle: { fit_rectangle(volume); break; } - case BuildVolume::Type::Circle: { fit_circle(volume); break; } - default: { break; } - } -} - -void Selection::mirror(Axis axis) -{ - if (!m_valid) - return; - - for (unsigned int i : m_list) { - GLVolume& v = *(*m_volumes)[i]; - if (is_single_full_instance()) - v.set_instance_mirror(axis, -v.get_instance_mirror(axis)); - else if (m_mode == Volume) - v.set_volume_mirror(axis, -v.get_volume_mirror(axis)); - } - -#if !DISABLE_INSTANCES_SYNCH - if (m_mode == Instance) - synchronize_unselected_instances(SYNC_ROTATION_NONE); - else if (m_mode == Volume) - synchronize_unselected_volumes(); -#endif // !DISABLE_INSTANCES_SYNCH - - set_bounding_boxes_dirty(); -} - -void Selection::translate(unsigned int object_idx, const Vec3d& displacement) -{ - if (!m_valid) - return; - - for (unsigned int i : m_list) { - GLVolume& v = *(*m_volumes)[i]; - if (v.object_idx() == (int)object_idx) - v.set_instance_offset(v.get_instance_offset() + displacement); - } - - std::set done; // prevent processing volumes twice - done.insert(m_list.begin(), m_list.end()); - - for (unsigned int i : m_list) { - if (done.size() == m_volumes->size()) - break; - -#if ENABLE_WIPETOWER_OBJECTID_1000_REMOVAL - if ((*m_volumes)[i]->is_wipe_tower) - continue; - - int object_idx = (*m_volumes)[i]->object_idx(); -#else - int object_idx = (*m_volumes)[i]->object_idx(); - if (object_idx >= 1000) - continue; -#endif // ENABLE_WIPETOWER_OBJECTID_1000_REMOVAL - - // Process unselected volumes of the object. - for (unsigned int j = 0; j < (unsigned int)m_volumes->size(); ++j) { - if (done.size() == m_volumes->size()) - break; - - if (done.find(j) != done.end()) - continue; - - GLVolume& v = *(*m_volumes)[j]; - if (v.object_idx() != object_idx) - continue; - - v.set_instance_offset(v.get_instance_offset() + displacement); - done.insert(j); - } - } - - this->set_bounding_boxes_dirty(); -} - -void Selection::translate(unsigned int object_idx, unsigned int instance_idx, const Vec3d& displacement) -{ - if (!m_valid) - return; - - for (unsigned int i : m_list) { - GLVolume& v = *(*m_volumes)[i]; - if (v.object_idx() == (int)object_idx && v.instance_idx() == (int)instance_idx) - v.set_instance_offset(v.get_instance_offset() + displacement); - } - - std::set done; // prevent processing volumes twice - done.insert(m_list.begin(), m_list.end()); - - for (unsigned int i : m_list) { - if (done.size() == m_volumes->size()) - break; - -#if ENABLE_WIPETOWER_OBJECTID_1000_REMOVAL - if ((*m_volumes)[i]->is_wipe_tower) - continue; - - int object_idx = (*m_volumes)[i]->object_idx(); -#else - int object_idx = (*m_volumes)[i]->object_idx(); - if (object_idx >= 1000) - continue; -#endif // ENABLE_WIPETOWER_OBJECTID_1000_REMOVAL - - // Process unselected volumes of the object. - for (unsigned int j = 0; j < (unsigned int)m_volumes->size(); ++j) { - if (done.size() == m_volumes->size()) - break; - - if (done.find(j) != done.end()) - continue; - - GLVolume& v = *(*m_volumes)[j]; - if (v.object_idx() != object_idx || v.instance_idx() != (int)instance_idx) - continue; - - v.set_instance_offset(v.get_instance_offset() + displacement); - done.insert(j); - } - } - - this->set_bounding_boxes_dirty(); -} - -void Selection::erase() -{ - if (!m_valid) - return; - - if (is_single_full_object()) - wxGetApp().obj_list()->delete_from_model_and_list(ItemType::itObject, get_object_idx(), 0); - else if (is_multiple_full_object()) { - std::vector items; - items.reserve(m_cache.content.size()); - for (ObjectIdxsToInstanceIdxsMap::iterator it = m_cache.content.begin(); it != m_cache.content.end(); ++it) { - items.emplace_back(ItemType::itObject, it->first, 0); - } - wxGetApp().obj_list()->delete_from_model_and_list(items); - } - else if (is_multiple_full_instance()) { - std::set> instances_idxs; - for (ObjectIdxsToInstanceIdxsMap::iterator obj_it = m_cache.content.begin(); obj_it != m_cache.content.end(); ++obj_it) { - for (InstanceIdxsList::reverse_iterator inst_it = obj_it->second.rbegin(); inst_it != obj_it->second.rend(); ++inst_it) { - instances_idxs.insert(std::make_pair(obj_it->first, *inst_it)); - } - } - - std::vector items; - items.reserve(instances_idxs.size()); - for (const std::pair& i : instances_idxs) { - items.emplace_back(ItemType::itInstance, i.first, i.second); - } - wxGetApp().obj_list()->delete_from_model_and_list(items); - } - else if (is_single_full_instance()) - wxGetApp().obj_list()->delete_from_model_and_list(ItemType::itInstance, get_object_idx(), get_instance_idx()); - else if (is_mixed()) { - std::set items_set; - std::map volumes_in_obj; - - for (auto i : m_list) { - const auto gl_vol = (*m_volumes)[i]; - const auto glv_obj_idx = gl_vol->object_idx(); - const auto model_object = m_model->objects[glv_obj_idx]; - - if (model_object->instances.size() == 1) { - if (model_object->volumes.size() == 1) - items_set.insert(ItemForDelete(ItemType::itObject, glv_obj_idx, -1)); - else { - items_set.insert(ItemForDelete(ItemType::itVolume, glv_obj_idx, gl_vol->volume_idx())); - int idx = (volumes_in_obj.find(glv_obj_idx) == volumes_in_obj.end()) ? 0 : volumes_in_obj.at(glv_obj_idx); - volumes_in_obj[glv_obj_idx] = ++idx; - } - continue; - } - - const auto glv_ins_idx = gl_vol->instance_idx(); - - for (auto obj_ins : m_cache.content) { - if (obj_ins.first == glv_obj_idx) { - if (obj_ins.second.find(glv_ins_idx) != obj_ins.second.end()) { - if (obj_ins.second.size() == model_object->instances.size()) - items_set.insert(ItemForDelete(ItemType::itObject, glv_obj_idx, -1)); - else - items_set.insert(ItemForDelete(ItemType::itInstance, glv_obj_idx, glv_ins_idx)); - - break; - } - } - } - } - - std::vector items; - items.reserve(items_set.size()); - for (const ItemForDelete& i : items_set) { - if (i.type == ItemType::itVolume) { - const int vol_in_obj_cnt = volumes_in_obj.find(i.obj_idx) == volumes_in_obj.end() ? 0 : volumes_in_obj.at(i.obj_idx); - if (vol_in_obj_cnt == (int)m_model->objects[i.obj_idx]->volumes.size()) { - if (i.sub_obj_idx == vol_in_obj_cnt - 1) - items.emplace_back(ItemType::itObject, i.obj_idx, 0); - continue; - } - } - items.emplace_back(i.type, i.obj_idx, i.sub_obj_idx); - } - - wxGetApp().obj_list()->delete_from_model_and_list(items); - } - else { - std::set> volumes_idxs; - for (unsigned int i : m_list) { - const GLVolume* v = (*m_volumes)[i]; - // Only remove volumes associated with ModelVolumes from the object list. - // Temporary meshes (SLA supports or pads) are not managed by the object list. - if (v->volume_idx() >= 0) - volumes_idxs.insert(std::make_pair(v->object_idx(), v->volume_idx())); - } - - std::vector items; - items.reserve(volumes_idxs.size()); - for (const std::pair& v : volumes_idxs) { - items.emplace_back(ItemType::itVolume, v.first, v.second); - } - - wxGetApp().obj_list()->delete_from_model_and_list(items); - ensure_not_below_bed(); - } -} - -void Selection::render(float scale_factor) -{ - if (!m_valid || is_empty()) - return; - - m_scale_factor = scale_factor; - // render cumulative bounding box of selected volumes -#if ENABLE_LEGACY_OPENGL_REMOVAL - render_bounding_box(get_bounding_box(), ColorRGB::WHITE()); -#else - render_selected_volumes(); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - render_synchronized_volumes(); -} - -#if ENABLE_RENDER_SELECTION_CENTER -void Selection::render_center(bool gizmo_is_dragging) -{ - if (!m_valid || is_empty()) - return; - -#if ENABLE_LEGACY_OPENGL_REMOVAL - GLShaderProgram* shader = wxGetApp().get_shader("flat"); - if (shader == nullptr) - return; - - shader->start_using(); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - - const Vec3d center = gizmo_is_dragging ? m_cache.dragging_center : get_bounding_box().center(); - - glsafe(::glDisable(GL_DEPTH_TEST)); - -#if ENABLE_GL_SHADERS_ATTRIBUTES - const Camera& camera = wxGetApp().plater()->get_camera(); - Transform3d view_model_matrix = camera.get_view_matrix() * Geometry::assemble_transform(center); - - shader->set_uniform("view_model_matrix", view_model_matrix); - shader->set_uniform("projection_matrix", camera.get_projection_matrix()); -#else - glsafe(::glPushMatrix()); - glsafe(::glTranslated(center.x(), center.y(), center.z())); -#endif // ENABLE_GL_SHADERS_ATTRIBUTES - -#if ENABLE_LEGACY_OPENGL_REMOVAL - m_vbo_sphere.set_color(ColorRGBA::WHITE()); -#else - m_vbo_sphere.set_color(-1, ColorRGBA::WHITE()); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - m_vbo_sphere.render(); - -#if !ENABLE_GL_SHADERS_ATTRIBUTES - glsafe(::glPopMatrix()); -#endif // !ENABLE_GL_SHADERS_ATTRIBUTES - -#if ENABLE_LEGACY_OPENGL_REMOVAL - shader->stop_using(); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL -} -#endif // ENABLE_RENDER_SELECTION_CENTER - -void Selection::render_sidebar_hints(const std::string& sidebar_field) -{ - if (sidebar_field.empty()) - return; - -#if ENABLE_LEGACY_OPENGL_REMOVAL - GLShaderProgram* shader = wxGetApp().get_shader(boost::starts_with(sidebar_field, "layer") ? "flat" : "gouraud_light"); - if (shader == nullptr) - return; - - shader->start_using(); -#else - GLShaderProgram* shader = nullptr; - - if (!boost::starts_with(sidebar_field, "layer")) { - shader = wxGetApp().get_shader("gouraud_light"); - if (shader == nullptr) - return; - - shader->start_using(); - glsafe(::glClear(GL_DEPTH_BUFFER_BIT)); - } -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - - glsafe(::glEnable(GL_DEPTH_TEST)); - -#if ENABLE_GL_SHADERS_ATTRIBUTES - const Transform3d base_matrix = Geometry::assemble_transform(get_bounding_box().center()); - Transform3d orient_matrix = Transform3d::Identity(); -#else - glsafe(::glPushMatrix()); -#endif // ENABLE_GL_SHADERS_ATTRIBUTES - - if (!boost::starts_with(sidebar_field, "layer")) { -#if ENABLE_GL_SHADERS_ATTRIBUTES - shader->set_uniform("emission_factor", 0.05f); -#else - const Vec3d& center = get_bounding_box().center(); -#endif // ENABLE_GL_SHADERS_ATTRIBUTES - if (is_single_full_instance() && !wxGetApp().obj_manipul()->get_world_coordinates()) { -#if !ENABLE_GL_SHADERS_ATTRIBUTES - glsafe(::glTranslated(center.x(), center.y(), center.z())); -#endif // !ENABLE_GL_SHADERS_ATTRIBUTES - if (!boost::starts_with(sidebar_field, "position")) { -#if !ENABLE_GL_SHADERS_ATTRIBUTES - Transform3d orient_matrix = Transform3d::Identity(); -#endif // !ENABLE_GL_SHADERS_ATTRIBUTES - if (boost::starts_with(sidebar_field, "scale")) - orient_matrix = (*m_volumes)[*m_list.begin()]->get_instance_transformation().get_matrix(true, false, true, true); - else if (boost::starts_with(sidebar_field, "rotation")) { - if (boost::ends_with(sidebar_field, "x")) - orient_matrix = (*m_volumes)[*m_list.begin()]->get_instance_transformation().get_matrix(true, false, true, true); - else if (boost::ends_with(sidebar_field, "y")) { - const Vec3d& rotation = (*m_volumes)[*m_list.begin()]->get_instance_transformation().get_rotation(); - if (rotation.x() == 0.0) - orient_matrix = (*m_volumes)[*m_list.begin()]->get_instance_transformation().get_matrix(true, false, true, true); - else - orient_matrix.rotate(Eigen::AngleAxisd(rotation.z(), Vec3d::UnitZ())); - } - } -#if !ENABLE_GL_SHADERS_ATTRIBUTES - glsafe(::glMultMatrixd(orient_matrix.data())); -#endif // !ENABLE_GL_SHADERS_ATTRIBUTES - } - } - else if (is_single_volume() || is_single_modifier()) { -#if ENABLE_GL_SHADERS_ATTRIBUTES - orient_matrix = (*m_volumes)[*m_list.begin()]->get_instance_transformation().get_matrix(true, false, true, true); -#else - glsafe(::glTranslated(center.x(), center.y(), center.z())); - Transform3d orient_matrix = (*m_volumes)[*m_list.begin()]->get_instance_transformation().get_matrix(true, false, true, true); -#endif // ENABLE_GL_SHADERS_ATTRIBUTES - if (!boost::starts_with(sidebar_field, "position")) - orient_matrix = orient_matrix * (*m_volumes)[*m_list.begin()]->get_volume_transformation().get_matrix(true, false, true, true); - -#if !ENABLE_GL_SHADERS_ATTRIBUTES - glsafe(::glMultMatrixd(orient_matrix.data())); -#endif // !ENABLE_GL_SHADERS_ATTRIBUTES - } - else { -#if ENABLE_GL_SHADERS_ATTRIBUTES - if (requires_local_axes()) - orient_matrix = (*m_volumes)[*m_list.begin()]->get_instance_transformation().get_matrix(true, false, true, true); -#else - glsafe(::glTranslated(center.x(), center.y(), center.z())); - if (requires_local_axes()) { - const Transform3d orient_matrix = (*m_volumes)[*m_list.begin()]->get_instance_transformation().get_matrix(true, false, true, true); - glsafe(::glMultMatrixd(orient_matrix.data())); - } -#endif // ENABLE_GL_SHADERS_ATTRIBUTES - } - } - -#if ENABLE_LEGACY_OPENGL_REMOVAL - if (!boost::starts_with(sidebar_field, "layer")) - glsafe(::glClear(GL_DEPTH_BUFFER_BIT)); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - -#if ENABLE_GL_SHADERS_ATTRIBUTES - if (boost::starts_with(sidebar_field, "position")) - render_sidebar_position_hints(sidebar_field, *shader, base_matrix * orient_matrix); - else if (boost::starts_with(sidebar_field, "rotation")) - render_sidebar_rotation_hints(sidebar_field, *shader, base_matrix * orient_matrix); - else if (boost::starts_with(sidebar_field, "scale") || boost::starts_with(sidebar_field, "size")) - render_sidebar_scale_hints(sidebar_field, *shader, base_matrix * orient_matrix); - else if (boost::starts_with(sidebar_field, "layer")) - render_sidebar_layers_hints(sidebar_field, *shader); -#else - if (boost::starts_with(sidebar_field, "position")) - render_sidebar_position_hints(sidebar_field); - else if (boost::starts_with(sidebar_field, "rotation")) - render_sidebar_rotation_hints(sidebar_field); - else if (boost::starts_with(sidebar_field, "scale") || boost::starts_with(sidebar_field, "size")) - render_sidebar_scale_hints(sidebar_field); - else if (boost::starts_with(sidebar_field, "layer")) - render_sidebar_layers_hints(sidebar_field); - - glsafe(::glPopMatrix()); -#endif // ENABLE_GL_SHADERS_ATTRIBUTES - -#if !ENABLE_LEGACY_OPENGL_REMOVAL - if (!boost::starts_with(sidebar_field, "layer")) -#endif // !ENABLE_LEGACY_OPENGL_REMOVAL - shader->stop_using(); -} - -bool Selection::requires_local_axes() const -{ - return m_mode == Volume && is_from_single_instance(); -} - -void Selection::copy_to_clipboard() -{ - if (!m_valid) - return; - - m_clipboard.reset(); - - for (const ObjectIdxsToInstanceIdxsMap::value_type& object : m_cache.content) { - ModelObject* src_object = m_model->objects[object.first]; - ModelObject* dst_object = m_clipboard.add_object(); - dst_object->name = src_object->name; - dst_object->input_file = src_object->input_file; - dst_object->config.assign_config(src_object->config); - dst_object->sla_support_points = src_object->sla_support_points; - dst_object->sla_points_status = src_object->sla_points_status; - dst_object->sla_drain_holes = src_object->sla_drain_holes; - dst_object->layer_config_ranges = src_object->layer_config_ranges; // #ys_FIXME_experiment - dst_object->layer_height_profile.assign(src_object->layer_height_profile); - dst_object->origin_translation = src_object->origin_translation; - - for (int i : object.second) { - dst_object->add_instance(*src_object->instances[i]); - } - - for (unsigned int i : m_list) { - // Copy the ModelVolumes only for the selected GLVolumes of the 1st selected instance. - const GLVolume* volume = (*m_volumes)[i]; - if (volume->object_idx() == object.first && volume->instance_idx() == *object.second.begin()) { - int volume_idx = volume->volume_idx(); - if (0 <= volume_idx && volume_idx < (int)src_object->volumes.size()) { - ModelVolume* src_volume = src_object->volumes[volume_idx]; - ModelVolume* dst_volume = dst_object->add_volume(*src_volume); - dst_volume->set_new_unique_id(); - } - else - assert(false); - } - } - } - - m_clipboard.set_mode(m_mode); -} - -void Selection::paste_from_clipboard() -{ - if (!m_valid || m_clipboard.is_empty()) - return; - - switch (m_clipboard.get_mode()) - { - case Volume: - { - if (is_from_single_instance()) - paste_volumes_from_clipboard(); - - break; - } - case Instance: - { - if (m_mode == Instance) - paste_objects_from_clipboard(); - - break; - } - } -} - -std::vector Selection::get_volume_idxs_from_object(unsigned int object_idx) const -{ - std::vector idxs; - - for (unsigned int i = 0; i < (unsigned int)m_volumes->size(); ++i) - { - if ((*m_volumes)[i]->object_idx() == (int)object_idx) - idxs.push_back(i); - } - - return idxs; -} - -std::vector Selection::get_volume_idxs_from_instance(unsigned int object_idx, unsigned int instance_idx) const -{ - std::vector idxs; - - for (unsigned int i = 0; i < (unsigned int)m_volumes->size(); ++i) - { - const GLVolume* v = (*m_volumes)[i]; - if ((v->object_idx() == (int)object_idx) && (v->instance_idx() == (int)instance_idx)) - idxs.push_back(i); - } - - return idxs; -} - -std::vector Selection::get_volume_idxs_from_volume(unsigned int object_idx, unsigned int instance_idx, unsigned int volume_idx) const -{ - std::vector idxs; - - for (unsigned int i = 0; i < (unsigned int)m_volumes->size(); ++i) - { - const GLVolume* v = (*m_volumes)[i]; - if ((v->object_idx() == (int)object_idx) && (v->volume_idx() == (int)volume_idx)) - { - if (((int)instance_idx != -1) && (v->instance_idx() == (int)instance_idx)) - idxs.push_back(i); - } - } - - return idxs; -} - -std::vector Selection::get_missing_volume_idxs_from(const std::vector& volume_idxs) const -{ - std::vector idxs; - - for (unsigned int i : m_list) - { - std::vector::const_iterator it = std::find(volume_idxs.begin(), volume_idxs.end(), i); - if (it == volume_idxs.end()) - idxs.push_back(i); - } - - return idxs; -} - -std::vector Selection::get_unselected_volume_idxs_from(const std::vector& volume_idxs) const -{ - std::vector idxs; - - for (unsigned int i : volume_idxs) - { - if (m_list.find(i) == m_list.end()) - idxs.push_back(i); - } - - return idxs; -} - -void Selection::update_valid() -{ - m_valid = (m_volumes != nullptr) && (m_model != nullptr); -} - -void Selection::update_type() -{ - m_cache.content.clear(); - m_type = Mixed; - - for (unsigned int i : m_list) - { - const GLVolume* volume = (*m_volumes)[i]; - int obj_idx = volume->object_idx(); - int inst_idx = volume->instance_idx(); - ObjectIdxsToInstanceIdxsMap::iterator obj_it = m_cache.content.find(obj_idx); - if (obj_it == m_cache.content.end()) - obj_it = m_cache.content.insert(ObjectIdxsToInstanceIdxsMap::value_type(obj_idx, InstanceIdxsList())).first; - - obj_it->second.insert(inst_idx); - } - - bool requires_disable = false; - - if (!m_valid) - m_type = Invalid; - else - { - if (m_list.empty()) - m_type = Empty; - else if (m_list.size() == 1) - { - const GLVolume* first = (*m_volumes)[*m_list.begin()]; - if (first->is_wipe_tower) - m_type = WipeTower; - else if (first->is_modifier) - { - m_type = SingleModifier; - requires_disable = true; - } - else - { - const ModelObject* model_object = m_model->objects[first->object_idx()]; - unsigned int volumes_count = (unsigned int)model_object->volumes.size(); - unsigned int instances_count = (unsigned int)model_object->instances.size(); - if (volumes_count * instances_count == 1) - { - m_type = SingleFullObject; - // ensures the correct mode is selected - m_mode = Instance; - } - else if (volumes_count == 1) // instances_count > 1 - { - m_type = SingleFullInstance; - // ensures the correct mode is selected - m_mode = Instance; - } - else - { - m_type = SingleVolume; - requires_disable = true; - } - } - } - else - { - unsigned int sla_volumes_count = 0; - // Note: sla_volumes_count is a count of the selected sla_volumes per object instead of per instance, like a model_volumes_count is - for (unsigned int i : m_list) { - if ((*m_volumes)[i]->volume_idx() < 0) - ++sla_volumes_count; - } - - if (m_cache.content.size() == 1) // single object - { - const ModelObject* model_object = m_model->objects[m_cache.content.begin()->first]; - unsigned int model_volumes_count = (unsigned int)model_object->volumes.size(); - - unsigned int instances_count = (unsigned int)model_object->instances.size(); - unsigned int selected_instances_count = (unsigned int)m_cache.content.begin()->second.size(); - if (model_volumes_count * instances_count + sla_volumes_count == (unsigned int)m_list.size()) - { - m_type = SingleFullObject; - // ensures the correct mode is selected - m_mode = Instance; - } - else if (selected_instances_count == 1) - { - if (model_volumes_count + sla_volumes_count == (unsigned int)m_list.size()) - { - m_type = SingleFullInstance; - // ensures the correct mode is selected - m_mode = Instance; - } - else - { - unsigned int modifiers_count = 0; - for (unsigned int i : m_list) - { - if ((*m_volumes)[i]->is_modifier) - ++modifiers_count; - } - - if (modifiers_count == 0) - m_type = MultipleVolume; - else if (modifiers_count == (unsigned int)m_list.size()) - m_type = MultipleModifier; - - requires_disable = true; - } - } - else if ((selected_instances_count > 1) && (selected_instances_count * model_volumes_count + sla_volumes_count == (unsigned int)m_list.size())) - { - m_type = MultipleFullInstance; - // ensures the correct mode is selected - m_mode = Instance; - } - } - else - { - unsigned int sels_cntr = 0; - for (ObjectIdxsToInstanceIdxsMap::iterator it = m_cache.content.begin(); it != m_cache.content.end(); ++it) - { - const ModelObject* model_object = m_model->objects[it->first]; - unsigned int volumes_count = (unsigned int)model_object->volumes.size(); - unsigned int instances_count = (unsigned int)model_object->instances.size(); - sels_cntr += volumes_count * instances_count; - } - if (sels_cntr + sla_volumes_count == (unsigned int)m_list.size()) - { - m_type = MultipleFullObject; - // ensures the correct mode is selected - m_mode = Instance; - } - } - } - } - - int object_idx = get_object_idx(); - int instance_idx = get_instance_idx(); - for (GLVolume* v : *m_volumes) - { - v->disabled = requires_disable ? (v->object_idx() != object_idx) || (v->instance_idx() != instance_idx) : false; - } - -#if ENABLE_SELECTION_DEBUG_OUTPUT - std::cout << "Selection: "; - std::cout << "mode: "; - switch (m_mode) - { - case Volume: - { - std::cout << "Volume"; - break; - } - case Instance: - { - std::cout << "Instance"; - break; - } - } - - std::cout << " - type: "; - - switch (m_type) - { - case Invalid: - { - std::cout << "Invalid" << std::endl; - break; - } - case Empty: - { - std::cout << "Empty" << std::endl; - break; - } - case WipeTower: - { - std::cout << "WipeTower" << std::endl; - break; - } - case SingleModifier: - { - std::cout << "SingleModifier" << std::endl; - break; - } - case MultipleModifier: - { - std::cout << "MultipleModifier" << std::endl; - break; - } - case SingleVolume: - { - std::cout << "SingleVolume" << std::endl; - break; - } - case MultipleVolume: - { - std::cout << "MultipleVolume" << std::endl; - break; - } - case SingleFullObject: - { - std::cout << "SingleFullObject" << std::endl; - break; - } - case MultipleFullObject: - { - std::cout << "MultipleFullObject" << std::endl; - break; - } - case SingleFullInstance: - { - std::cout << "SingleFullInstance" << std::endl; - break; - } - case MultipleFullInstance: - { - std::cout << "MultipleFullInstance" << std::endl; - break; - } - case Mixed: - { - std::cout << "Mixed" << std::endl; - break; - } - } -#endif // ENABLE_SELECTION_DEBUG_OUTPUT -} - -void Selection::set_caches() -{ - m_cache.volumes_data.clear(); - m_cache.sinking_volumes.clear(); - for (unsigned int i = 0; i < (unsigned int)m_volumes->size(); ++i) { - const GLVolume& v = *(*m_volumes)[i]; - m_cache.volumes_data.emplace(i, VolumeCache(v.get_volume_transformation(), v.get_instance_transformation())); - if (v.is_sinking()) - m_cache.sinking_volumes.push_back(i); - } - m_cache.dragging_center = get_bounding_box().center(); -} - -void Selection::do_add_volume(unsigned int volume_idx) -{ - m_list.insert(volume_idx); - GLVolume* v = (*m_volumes)[volume_idx]; - v->selected = true; - if (v->hover == GLVolume::HS_Select || v->hover == GLVolume::HS_Deselect) - v->hover = GLVolume::HS_Hover; -} - -void Selection::do_add_volumes(const std::vector& volume_idxs) -{ - for (unsigned int i : volume_idxs) - { - if (i < (unsigned int)m_volumes->size()) - do_add_volume(i); - } -} - -void Selection::do_remove_volume(unsigned int volume_idx) -{ - IndicesList::iterator v_it = m_list.find(volume_idx); - if (v_it == m_list.end()) - return; - - m_list.erase(v_it); - - (*m_volumes)[volume_idx]->selected = false; -} - -void Selection::do_remove_instance(unsigned int object_idx, unsigned int instance_idx) -{ - for (unsigned int i = 0; i < (unsigned int)m_volumes->size(); ++i) { - GLVolume* v = (*m_volumes)[i]; - if (v->object_idx() == (int)object_idx && v->instance_idx() == (int)instance_idx) - do_remove_volume(i); - } -} - -void Selection::do_remove_object(unsigned int object_idx) -{ - for (unsigned int i = 0; i < (unsigned int)m_volumes->size(); ++i) { - GLVolume* v = (*m_volumes)[i]; - if (v->object_idx() == (int)object_idx) - do_remove_volume(i); - } -} - -#if !ENABLE_LEGACY_OPENGL_REMOVAL -void Selection::render_selected_volumes() const -{ - float color[3] = { 1.0f, 1.0f, 1.0f }; - render_bounding_box(get_bounding_box(), color); -} -#endif // !ENABLE_LEGACY_OPENGL_REMOVAL - -void Selection::render_synchronized_volumes() -{ - if (m_mode == Instance) - return; - -#if !ENABLE_LEGACY_OPENGL_REMOVAL - float color[3] = { 1.0f, 1.0f, 0.0f }; -#endif // !ENABLE_LEGACY_OPENGL_REMOVAL - - for (unsigned int i : m_list) { - const GLVolume& volume = *(*m_volumes)[i]; - int object_idx = volume.object_idx(); - int volume_idx = volume.volume_idx(); - for (unsigned int j = 0; j < (unsigned int)m_volumes->size(); ++j) { - if (i == j) - continue; - - const GLVolume& v = *(*m_volumes)[j]; - if (v.object_idx() != object_idx || v.volume_idx() != volume_idx) - continue; - -#if ENABLE_LEGACY_OPENGL_REMOVAL - render_bounding_box(v.transformed_convex_hull_bounding_box(), ColorRGB::YELLOW()); -#else - render_bounding_box(v.transformed_convex_hull_bounding_box(), color); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - } - } -} - -#if ENABLE_LEGACY_OPENGL_REMOVAL -void Selection::render_bounding_box(const BoundingBoxf3& box, const ColorRGB& color) -{ -#else -void Selection::render_bounding_box(const BoundingBoxf3 & box, float* color) const -{ - if (color == nullptr) - return; - - const Vec3f b_min = box.min.cast(); - const Vec3f b_max = box.max.cast(); - const Vec3f size = 0.2f * box.size().cast(); - - glsafe(::glEnable(GL_DEPTH_TEST)); - glsafe(::glColor3fv(color)); - glsafe(::glLineWidth(2.0f * m_scale_factor)); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - -#if ENABLE_LEGACY_OPENGL_REMOVAL - const BoundingBoxf3& curr_box = m_box.get_bounding_box(); - if (!m_box.is_initialized() || !is_approx(box.min, curr_box.min) || !is_approx(box.max, curr_box.max)) { - m_box.reset(); - - const Vec3f b_min = box.min.cast(); - const Vec3f b_max = box.max.cast(); - const Vec3f size = 0.2f * box.size().cast(); - - GLModel::Geometry init_data; - init_data.format = { GLModel::Geometry::EPrimitiveType::Lines, GLModel::Geometry::EVertexLayout::P3 }; - init_data.reserve_vertices(48); - init_data.reserve_indices(48); - - // vertices - init_data.add_vertex(Vec3f(b_min.x(), b_min.y(), b_min.z())); - init_data.add_vertex(Vec3f(b_min.x() + size.x(), b_min.y(), b_min.z())); - init_data.add_vertex(Vec3f(b_min.x(), b_min.y(), b_min.z())); - init_data.add_vertex(Vec3f(b_min.x(), b_min.y() + size.y(), b_min.z())); - init_data.add_vertex(Vec3f(b_min.x(), b_min.y(), b_min.z())); - init_data.add_vertex(Vec3f(b_min.x(), b_min.y(), b_min.z() + size.z())); - - init_data.add_vertex(Vec3f(b_max.x(), b_min.y(), b_min.z())); - init_data.add_vertex(Vec3f(b_max.x() - size.x(), b_min.y(), b_min.z())); - init_data.add_vertex(Vec3f(b_max.x(), b_min.y(), b_min.z())); - init_data.add_vertex(Vec3f(b_max.x(), b_min.y() + size.y(), b_min.z())); - init_data.add_vertex(Vec3f(b_max.x(), b_min.y(), b_min.z())); - init_data.add_vertex(Vec3f(b_max.x(), b_min.y(), b_min.z() + size.z())); - - init_data.add_vertex(Vec3f(b_max.x(), b_max.y(), b_min.z())); - init_data.add_vertex(Vec3f(b_max.x() - size.x(), b_max.y(), b_min.z())); - init_data.add_vertex(Vec3f(b_max.x(), b_max.y(), b_min.z())); - init_data.add_vertex(Vec3f(b_max.x(), b_max.y() - size.y(), b_min.z())); - init_data.add_vertex(Vec3f(b_max.x(), b_max.y(), b_min.z())); - init_data.add_vertex(Vec3f(b_max.x(), b_max.y(), b_min.z() + size.z())); - - init_data.add_vertex(Vec3f(b_min.x(), b_max.y(), b_min.z())); - init_data.add_vertex(Vec3f(b_min.x() + size.x(), b_max.y(), b_min.z())); - init_data.add_vertex(Vec3f(b_min.x(), b_max.y(), b_min.z())); - init_data.add_vertex(Vec3f(b_min.x(), b_max.y() - size.y(), b_min.z())); - init_data.add_vertex(Vec3f(b_min.x(), b_max.y(), b_min.z())); - init_data.add_vertex(Vec3f(b_min.x(), b_max.y(), b_min.z() + size.z())); - - init_data.add_vertex(Vec3f(b_min.x(), b_min.y(), b_max.z())); - init_data.add_vertex(Vec3f(b_min.x() + size.x(), b_min.y(), b_max.z())); - init_data.add_vertex(Vec3f(b_min.x(), b_min.y(), b_max.z())); - init_data.add_vertex(Vec3f(b_min.x(), b_min.y() + size.y(), b_max.z())); - init_data.add_vertex(Vec3f(b_min.x(), b_min.y(), b_max.z())); - init_data.add_vertex(Vec3f(b_min.x(), b_min.y(), b_max.z() - size.z())); - - init_data.add_vertex(Vec3f(b_max.x(), b_min.y(), b_max.z())); - init_data.add_vertex(Vec3f(b_max.x() - size.x(), b_min.y(), b_max.z())); - init_data.add_vertex(Vec3f(b_max.x(), b_min.y(), b_max.z())); - init_data.add_vertex(Vec3f(b_max.x(), b_min.y() + size.y(), b_max.z())); - init_data.add_vertex(Vec3f(b_max.x(), b_min.y(), b_max.z())); - init_data.add_vertex(Vec3f(b_max.x(), b_min.y(), b_max.z() - size.z())); - - init_data.add_vertex(Vec3f(b_max.x(), b_max.y(), b_max.z())); - init_data.add_vertex(Vec3f(b_max.x() - size.x(), b_max.y(), b_max.z())); - init_data.add_vertex(Vec3f(b_max.x(), b_max.y(), b_max.z())); - init_data.add_vertex(Vec3f(b_max.x(), b_max.y() - size.y(), b_max.z())); - init_data.add_vertex(Vec3f(b_max.x(), b_max.y(), b_max.z())); - init_data.add_vertex(Vec3f(b_max.x(), b_max.y(), b_max.z() - size.z())); - - init_data.add_vertex(Vec3f(b_min.x(), b_max.y(), b_max.z())); - init_data.add_vertex(Vec3f(b_min.x() + size.x(), b_max.y(), b_max.z())); - init_data.add_vertex(Vec3f(b_min.x(), b_max.y(), b_max.z())); - init_data.add_vertex(Vec3f(b_min.x(), b_max.y() - size.y(), b_max.z())); - init_data.add_vertex(Vec3f(b_min.x(), b_max.y(), b_max.z())); - init_data.add_vertex(Vec3f(b_min.x(), b_max.y(), b_max.z() - size.z())); - - // indices - for (unsigned int i = 0; i < 48; ++i) { - init_data.add_index(i); - } - - m_box.init_from(std::move(init_data)); - } - - glsafe(::glEnable(GL_DEPTH_TEST)); - - glsafe(::glLineWidth(2.0f * m_scale_factor)); - - GLShaderProgram* shader = wxGetApp().get_shader("flat"); - if (shader == nullptr) - return; - - shader->start_using(); -#if ENABLE_GL_SHADERS_ATTRIBUTES - const Camera& camera = wxGetApp().plater()->get_camera(); - shader->set_uniform("view_model_matrix", camera.get_view_matrix()); - shader->set_uniform("projection_matrix", camera.get_projection_matrix()); -#endif // ENABLE_GL_SHADERS_ATTRIBUTES - m_box.set_color(to_rgba(color)); - m_box.render(); - shader->stop_using(); -#else - ::glBegin(GL_LINES); - - ::glVertex3f(b_min(0), b_min(1), b_min(2)); ::glVertex3f(b_min(0) + size(0), b_min(1), b_min(2)); - ::glVertex3f(b_min(0), b_min(1), b_min(2)); ::glVertex3f(b_min(0), b_min(1) + size(1), b_min(2)); - ::glVertex3f(b_min(0), b_min(1), b_min(2)); ::glVertex3f(b_min(0), b_min(1), b_min(2) + size(2)); - - ::glVertex3f(b_max(0), b_min(1), b_min(2)); ::glVertex3f(b_max(0) - size(0), b_min(1), b_min(2)); - ::glVertex3f(b_max(0), b_min(1), b_min(2)); ::glVertex3f(b_max(0), b_min(1) + size(1), b_min(2)); - ::glVertex3f(b_max(0), b_min(1), b_min(2)); ::glVertex3f(b_max(0), b_min(1), b_min(2) + size(2)); - - ::glVertex3f(b_max(0), b_max(1), b_min(2)); ::glVertex3f(b_max(0) - size(0), b_max(1), b_min(2)); - ::glVertex3f(b_max(0), b_max(1), b_min(2)); ::glVertex3f(b_max(0), b_max(1) - size(1), b_min(2)); - ::glVertex3f(b_max(0), b_max(1), b_min(2)); ::glVertex3f(b_max(0), b_max(1), b_min(2) + size(2)); - - ::glVertex3f(b_min(0), b_max(1), b_min(2)); ::glVertex3f(b_min(0) + size(0), b_max(1), b_min(2)); - ::glVertex3f(b_min(0), b_max(1), b_min(2)); ::glVertex3f(b_min(0), b_max(1) - size(1), b_min(2)); - ::glVertex3f(b_min(0), b_max(1), b_min(2)); ::glVertex3f(b_min(0), b_max(1), b_min(2) + size(2)); - - ::glVertex3f(b_min(0), b_min(1), b_max(2)); ::glVertex3f(b_min(0) + size(0), b_min(1), b_max(2)); - ::glVertex3f(b_min(0), b_min(1), b_max(2)); ::glVertex3f(b_min(0), b_min(1) + size(1), b_max(2)); - ::glVertex3f(b_min(0), b_min(1), b_max(2)); ::glVertex3f(b_min(0), b_min(1), b_max(2) - size(2)); - - ::glVertex3f(b_max(0), b_min(1), b_max(2)); ::glVertex3f(b_max(0) - size(0), b_min(1), b_max(2)); - ::glVertex3f(b_max(0), b_min(1), b_max(2)); ::glVertex3f(b_max(0), b_min(1) + size(1), b_max(2)); - ::glVertex3f(b_max(0), b_min(1), b_max(2)); ::glVertex3f(b_max(0), b_min(1), b_max(2) - size(2)); - - ::glVertex3f(b_max(0), b_max(1), b_max(2)); ::glVertex3f(b_max(0) - size(0), b_max(1), b_max(2)); - ::glVertex3f(b_max(0), b_max(1), b_max(2)); ::glVertex3f(b_max(0), b_max(1) - size(1), b_max(2)); - ::glVertex3f(b_max(0), b_max(1), b_max(2)); ::glVertex3f(b_max(0), b_max(1), b_max(2) - size(2)); - - ::glVertex3f(b_min(0), b_max(1), b_max(2)); ::glVertex3f(b_min(0) + size(0), b_max(1), b_max(2)); - ::glVertex3f(b_min(0), b_max(1), b_max(2)); ::glVertex3f(b_min(0), b_max(1) - size(1), b_max(2)); - ::glVertex3f(b_min(0), b_max(1), b_max(2)); ::glVertex3f(b_min(0), b_max(1), b_max(2) - size(2)); - - glsafe(::glEnd()); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL -} - -static ColorRGBA get_color(Axis axis) -{ - return AXES_COLOR[axis]; -} - -#if ENABLE_GL_SHADERS_ATTRIBUTES -void Selection::render_sidebar_position_hints(const std::string& sidebar_field, GLShaderProgram& shader, const Transform3d& matrix) -#else -void Selection::render_sidebar_position_hints(const std::string& sidebar_field) -#endif // ENABLE_GL_SHADERS_ATTRIBUTES -{ -#if ENABLE_LEGACY_OPENGL_REMOVAL -#if ENABLE_GL_SHADERS_ATTRIBUTES - const Camera& camera = wxGetApp().plater()->get_camera(); - const Transform3d view_matrix = camera.get_view_matrix() * matrix; - shader.set_uniform("projection_matrix", camera.get_projection_matrix()); -#endif // ENABLE_GL_SHADERS_ATTRIBUTES - - if (boost::ends_with(sidebar_field, "x")) { -#if ENABLE_GL_SHADERS_ATTRIBUTES - const Transform3d view_model_matrix = view_matrix * Geometry::assemble_transform(Vec3d::Zero(), -0.5 * PI * Vec3d::UnitZ()); - shader.set_uniform("view_model_matrix", view_model_matrix); - shader.set_uniform("normal_matrix", (Matrix3d)view_model_matrix.matrix().block(0, 0, 3, 3).inverse().transpose()); -#else - glsafe(::glRotated(-90.0, 0.0, 0.0, 1.0)); -#endif // ENABLE_GL_SHADERS_ATTRIBUTES - m_arrow.set_color(get_color(X)); - m_arrow.render(); - } - else if (boost::ends_with(sidebar_field, "y")) { -#if ENABLE_GL_SHADERS_ATTRIBUTES - shader.set_uniform("view_model_matrix", view_matrix); - shader.set_uniform("normal_matrix", (Matrix3d)view_matrix.matrix().block(0, 0, 3, 3).inverse().transpose()); -#endif // ENABLE_GL_SHADERS_ATTRIBUTES - m_arrow.set_color(get_color(Y)); - m_arrow.render(); - } - else if (boost::ends_with(sidebar_field, "z")) { -#if ENABLE_GL_SHADERS_ATTRIBUTES - const Transform3d view_model_matrix = view_matrix * Geometry::assemble_transform(Vec3d::Zero(), 0.5 * PI * Vec3d::UnitX()); - shader.set_uniform("view_model_matrix", view_model_matrix); - shader.set_uniform("normal_matrix", (Matrix3d)view_model_matrix.matrix().block(0, 0, 3, 3).inverse().transpose()); -#else - glsafe(::glRotated(90.0, 1.0, 0.0, 0.0)); -#endif // ENABLE_GL_SHADERS_ATTRIBUTES - m_arrow.set_color(get_color(Z)); - m_arrow.render(); - } -#else - if (boost::ends_with(sidebar_field, "x")) { - glsafe(::glRotated(-90.0, 0.0, 0.0, 1.0)); - m_arrow.set_color(-1, get_color(X)); - m_arrow.render(); - } - else if (boost::ends_with(sidebar_field, "y")) { - m_arrow.set_color(-1, get_color(Y)); - m_arrow.render(); - } - else if (boost::ends_with(sidebar_field, "z")) { - glsafe(::glRotated(90.0, 1.0, 0.0, 0.0)); - m_arrow.set_color(-1, get_color(Z)); - m_arrow.render(); - } -#endif // ENABLE_LEGACY_OPENGL_REMOVAL -} - -#if ENABLE_GL_SHADERS_ATTRIBUTES -void Selection::render_sidebar_rotation_hints(const std::string& sidebar_field, GLShaderProgram& shader, const Transform3d& matrix) -#else -void Selection::render_sidebar_rotation_hints(const std::string& sidebar_field) -#endif // ENABLE_GL_SHADERS_ATTRIBUTES -{ -#if ENABLE_LEGACY_OPENGL_REMOVAL -#if ENABLE_GL_SHADERS_ATTRIBUTES - auto render_sidebar_rotation_hint = [this](GLShaderProgram& shader, const Transform3d& matrix) { - Transform3d view_model_matrix = matrix; - shader.set_uniform("view_model_matrix", view_model_matrix); - shader.set_uniform("normal_matrix", (Matrix3d)view_model_matrix.matrix().block(0, 0, 3, 3).inverse().transpose()); - m_curved_arrow.render(); - view_model_matrix = matrix * Geometry::assemble_transform(Vec3d::Zero(), PI * Vec3d::UnitZ()); - shader.set_uniform("view_model_matrix", view_model_matrix); - shader.set_uniform("normal_matrix", (Matrix3d)view_model_matrix.matrix().block(0, 0, 3, 3).inverse().transpose()); - m_curved_arrow.render(); - }; - - const Camera& camera = wxGetApp().plater()->get_camera(); - const Transform3d view_matrix = camera.get_view_matrix() * matrix; - shader.set_uniform("projection_matrix", camera.get_projection_matrix()); -#else - auto render_sidebar_rotation_hint = [this]() { - m_curved_arrow.render(); - glsafe(::glRotated(180.0, 0.0, 0.0, 1.0)); - m_curved_arrow.render(); - }; -#endif // ENABLE_GL_SHADERS_ATTRIBUTES - - if (boost::ends_with(sidebar_field, "x")) { -#if !ENABLE_GL_SHADERS_ATTRIBUTES - glsafe(::glRotated(90.0, 0.0, 1.0, 0.0)); -#endif // !ENABLE_GL_SHADERS_ATTRIBUTES - m_curved_arrow.set_color(get_color(X)); -#if ENABLE_GL_SHADERS_ATTRIBUTES - render_sidebar_rotation_hint(shader, view_matrix * Geometry::assemble_transform(Vec3d::Zero(), 0.5 * PI * Vec3d::UnitY())); -#else - render_sidebar_rotation_hint(); -#endif // ENABLE_GL_SHADERS_ATTRIBUTES - } - else if (boost::ends_with(sidebar_field, "y")) { -#if !ENABLE_GL_SHADERS_ATTRIBUTES - glsafe(::glRotated(-90.0, 1.0, 0.0, 0.0)); -#endif // !ENABLE_GL_SHADERS_ATTRIBUTES - m_curved_arrow.set_color(get_color(Y)); -#if ENABLE_GL_SHADERS_ATTRIBUTES - render_sidebar_rotation_hint(shader, view_matrix * Geometry::assemble_transform(Vec3d::Zero(), -0.5 * PI * Vec3d::UnitX())); -#else - render_sidebar_rotation_hint(); -#endif // ENABLE_GL_SHADERS_ATTRIBUTES - } - else if (boost::ends_with(sidebar_field, "z")) { - m_curved_arrow.set_color(get_color(Z)); -#if ENABLE_GL_SHADERS_ATTRIBUTES - render_sidebar_rotation_hint(shader, view_matrix); -#else - render_sidebar_rotation_hint(); -#endif // ENABLE_GL_SHADERS_ATTRIBUTES - } -#else - auto render_sidebar_rotation_hint = [this]() { - m_curved_arrow.render(); - glsafe(::glRotated(180.0, 0.0, 0.0, 1.0)); - m_curved_arrow.render(); - }; - - if (boost::ends_with(sidebar_field, "x")) { - glsafe(::glRotated(90.0, 0.0, 1.0, 0.0)); - m_curved_arrow.set_color(-1, get_color(X)); - render_sidebar_rotation_hint(); - } - else if (boost::ends_with(sidebar_field, "y")) { - glsafe(::glRotated(-90.0, 1.0, 0.0, 0.0)); - m_curved_arrow.set_color(-1, get_color(Y)); - render_sidebar_rotation_hint(); - } - else if (boost::ends_with(sidebar_field, "z")) { - m_curved_arrow.set_color(-1, get_color(Z)); - render_sidebar_rotation_hint(); - } -#endif // ENABLE_LEGACY_OPENGL_REMOVAL -} - -#if ENABLE_GL_SHADERS_ATTRIBUTES -void Selection::render_sidebar_scale_hints(const std::string& sidebar_field, GLShaderProgram& shader, const Transform3d& matrix) -#else -void Selection::render_sidebar_scale_hints(const std::string& sidebar_field) -#endif // ENABLE_GL_SHADERS_ATTRIBUTES -{ - const bool uniform_scale = requires_uniform_scale() || wxGetApp().obj_manipul()->get_uniform_scaling(); - -#if ENABLE_GL_SHADERS_ATTRIBUTES - auto render_sidebar_scale_hint = [this, uniform_scale](Axis axis, GLShaderProgram& shader, const Transform3d& matrix) { -#else - auto render_sidebar_scale_hint = [this, uniform_scale](Axis axis) { -#endif // ENABLE_GL_SHADERS_ATTRIBUTES -#if ENABLE_LEGACY_OPENGL_REMOVAL - m_arrow.set_color(uniform_scale ? UNIFORM_SCALE_COLOR : get_color(axis)); -#else - m_arrow.set_color(-1, uniform_scale ? UNIFORM_SCALE_COLOR : get_color(axis)); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - -#if ENABLE_GL_SHADERS_ATTRIBUTES - Transform3d view_model_matrix = matrix * Geometry::assemble_transform(5.0 * Vec3d::UnitY()); - shader.set_uniform("view_model_matrix", view_model_matrix); - shader.set_uniform("normal_matrix", (Matrix3d)view_model_matrix.matrix().block(0, 0, 3, 3).inverse().transpose()); -#else - GLShaderProgram* shader = wxGetApp().get_current_shader(); - if (shader != nullptr) - shader->set_uniform("emission_factor", 0.0f); - - glsafe(::glTranslated(0.0, 5.0, 0.0)); -#endif // ENABLE_GL_SHADERS_ATTRIBUTES - m_arrow.render(); - -#if ENABLE_GL_SHADERS_ATTRIBUTES - view_model_matrix = matrix * Geometry::assemble_transform(-5.0 * Vec3d::UnitY(), PI * Vec3d::UnitZ()); - shader.set_uniform("view_model_matrix", view_model_matrix); - shader.set_uniform("normal_matrix", (Matrix3d)view_model_matrix.matrix().block(0, 0, 3, 3).inverse().transpose()); -#else - glsafe(::glTranslated(0.0, -10.0, 0.0)); - glsafe(::glRotated(180.0, 0.0, 0.0, 1.0)); -#endif // ENABLE_GL_SHADERS_ATTRIBUTES - m_arrow.render(); - }; - -#if ENABLE_GL_SHADERS_ATTRIBUTES - const Camera& camera = wxGetApp().plater()->get_camera(); - const Transform3d view_matrix = camera.get_view_matrix() * matrix; - shader.set_uniform("projection_matrix", camera.get_projection_matrix()); -#endif // ENABLE_GL_SHADERS_ATTRIBUTES - - if (boost::ends_with(sidebar_field, "x") || uniform_scale) { -#if ENABLE_GL_SHADERS_ATTRIBUTES - render_sidebar_scale_hint(X, shader, view_matrix * Geometry::assemble_transform(Vec3d::Zero(), -0.5 * PI * Vec3d::UnitZ())); -#else - glsafe(::glPushMatrix()); - glsafe(::glRotated(-90.0, 0.0, 0.0, 1.0)); - render_sidebar_scale_hint(X); - glsafe(::glPopMatrix()); -#endif // ENABLE_GL_SHADERS_ATTRIBUTES - } - - if (boost::ends_with(sidebar_field, "y") || uniform_scale) { -#if ENABLE_GL_SHADERS_ATTRIBUTES - render_sidebar_scale_hint(Y, shader, view_matrix); -#else - glsafe(::glPushMatrix()); - render_sidebar_scale_hint(Y); - glsafe(::glPopMatrix()); -#endif // ENABLE_GL_SHADERS_ATTRIBUTES - } - - if (boost::ends_with(sidebar_field, "z") || uniform_scale) { -#if ENABLE_GL_SHADERS_ATTRIBUTES - render_sidebar_scale_hint(Z, shader, view_matrix * Geometry::assemble_transform(Vec3d::Zero(), 0.5 * PI * Vec3d::UnitX())); -#else - glsafe(::glPushMatrix()); - glsafe(::glRotated(90.0, 1.0, 0.0, 0.0)); - render_sidebar_scale_hint(Z); - glsafe(::glPopMatrix()); -#endif // ENABLE_GL_SHADERS_ATTRIBUTES - } -} - -#if ENABLE_GL_SHADERS_ATTRIBUTES -void Selection::render_sidebar_layers_hints(const std::string& sidebar_field, GLShaderProgram& shader) -#else -void Selection::render_sidebar_layers_hints(const std::string& sidebar_field) -#endif // ENABLE_GL_SHADERS_ATTRIBUTES -{ - static const float Margin = 10.0f; - - std::string field = sidebar_field; - - // extract max_z - std::string::size_type pos = field.rfind("_"); - if (pos == std::string::npos) - return; - - const float max_z = float(string_to_double_decimal_point(field.substr(pos + 1))); - - // extract min_z - field = field.substr(0, pos); - pos = field.rfind("_"); - if (pos == std::string::npos) - return; - - const float min_z = float(string_to_double_decimal_point(field.substr(pos + 1))); - - // extract type - field = field.substr(0, pos); - pos = field.rfind("_"); - if (pos == std::string::npos) - return; - - const int type = std::stoi(field.substr(pos + 1)); - - const BoundingBoxf3& box = get_bounding_box(); - -#if !ENABLE_LEGACY_OPENGL_REMOVAL - const float min_x = float(box.min.x()) - Margin; - const float max_x = float(box.max.x()) + Margin; - const float min_y = float(box.min.y()) - Margin; - const float max_y = float(box.max.y()) + Margin; -#endif // !ENABLE_LEGACY_OPENGL_REMOVAL - - // view dependend order of rendering to keep correct transparency - const bool camera_on_top = wxGetApp().plater()->get_camera().is_looking_downward(); - const float z1 = camera_on_top ? min_z : max_z; - const float z2 = camera_on_top ? max_z : min_z; - -#if ENABLE_LEGACY_OPENGL_REMOVAL - const Vec3f p1 = { float(box.min.x()) - Margin, float(box.min.y()) - Margin, z1 }; - const Vec3f p2 = { float(box.max.x()) + Margin, float(box.max.y()) + Margin, z2 }; -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - - glsafe(::glEnable(GL_DEPTH_TEST)); - glsafe(::glDisable(GL_CULL_FACE)); - glsafe(::glEnable(GL_BLEND)); - glsafe(::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)); - -#if ENABLE_LEGACY_OPENGL_REMOVAL - if (!m_planes.models[0].is_initialized() || !is_approx(m_planes.check_points[0], p1)) { - m_planes.check_points[0] = p1; - m_planes.models[0].reset(); - - GLModel::Geometry init_data; - init_data.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3 }; - init_data.reserve_vertices(4); - init_data.reserve_indices(6); - - // vertices - init_data.add_vertex(Vec3f(p1.x(), p1.y(), z1)); - init_data.add_vertex(Vec3f(p2.x(), p1.y(), z1)); - init_data.add_vertex(Vec3f(p2.x(), p2.y(), z1)); - init_data.add_vertex(Vec3f(p1.x(), p2.y(), z1)); - - // indices - init_data.add_triangle(0, 1, 2); - init_data.add_triangle(2, 3, 0); - - m_planes.models[0].init_from(std::move(init_data)); - } - - if (!m_planes.models[1].is_initialized() || !is_approx(m_planes.check_points[1], p2)) { - m_planes.check_points[1] = p2; - m_planes.models[1].reset(); - - GLModel::Geometry init_data; - init_data.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3 }; - init_data.reserve_vertices(4); - init_data.reserve_indices(6); - - // vertices - init_data.add_vertex(Vec3f(p1.x(), p1.y(), z2)); - init_data.add_vertex(Vec3f(p2.x(), p1.y(), z2)); - init_data.add_vertex(Vec3f(p2.x(), p2.y(), z2)); - init_data.add_vertex(Vec3f(p1.x(), p2.y(), z2)); - - // indices - init_data.add_triangle(0, 1, 2); - init_data.add_triangle(2, 3, 0); - - m_planes.models[1].init_from(std::move(init_data)); - } - -#if ENABLE_GL_SHADERS_ATTRIBUTES - const Camera& camera = wxGetApp().plater()->get_camera(); - shader.set_uniform("view_model_matrix", camera.get_view_matrix()); - shader.set_uniform("projection_matrix", camera.get_projection_matrix()); -#endif // ENABLE_GL_SHADERS_ATTRIBUTES - - m_planes.models[0].set_color((camera_on_top && type == 1) || (!camera_on_top && type == 2) ? SOLID_PLANE_COLOR : TRANSPARENT_PLANE_COLOR); - m_planes.models[0].render(); - m_planes.models[1].set_color((camera_on_top && type == 2) || (!camera_on_top && type == 1) ? SOLID_PLANE_COLOR : TRANSPARENT_PLANE_COLOR); - m_planes.models[1].render(); -#else - ::glBegin(GL_QUADS); - ::glColor4fv((camera_on_top && type == 1) || (!camera_on_top && type == 2) ? SOLID_PLANE_COLOR.data() : TRANSPARENT_PLANE_COLOR.data()); - ::glVertex3f(min_x, min_y, z1); - ::glVertex3f(max_x, min_y, z1); - ::glVertex3f(max_x, max_y, z1); - ::glVertex3f(min_x, max_y, z1); - glsafe(::glEnd()); - - ::glBegin(GL_QUADS); - ::glColor4fv((camera_on_top && type == 2) || (!camera_on_top && type == 1) ? SOLID_PLANE_COLOR.data() : TRANSPARENT_PLANE_COLOR.data()); - ::glVertex3f(min_x, min_y, z2); - ::glVertex3f(max_x, min_y, z2); - ::glVertex3f(max_x, max_y, z2); - ::glVertex3f(min_x, max_y, z2); - glsafe(::glEnd()); -#endif // ENABLE_LEGACY_OPENGL_REMOVAL - - glsafe(::glEnable(GL_CULL_FACE)); - glsafe(::glDisable(GL_BLEND)); -} - -#ifndef NDEBUG -static bool is_rotation_xy_synchronized(const Vec3d &rot_xyz_from, const Vec3d &rot_xyz_to) -{ - const Eigen::AngleAxisd angle_axis(Geometry::rotation_xyz_diff(rot_xyz_from, rot_xyz_to)); - const Vec3d axis = angle_axis.axis(); - const double angle = angle_axis.angle(); - if (std::abs(angle) < 1e-8) - return true; - assert(std::abs(axis.x()) < 1e-8); - assert(std::abs(axis.y()) < 1e-8); - assert(std::abs(std::abs(axis.z()) - 1.) < 1e-8); - return std::abs(axis.x()) < 1e-8 && std::abs(axis.y()) < 1e-8 && std::abs(std::abs(axis.z()) - 1.) < 1e-8; -} - -static void verify_instances_rotation_synchronized(const Model &model, const GLVolumePtrs &volumes) -{ - for (int idx_object = 0; idx_object < int(model.objects.size()); ++idx_object) { - int idx_volume_first = -1; - for (int i = 0; i < (int)volumes.size(); ++i) { - if (volumes[i]->object_idx() == idx_object) { - idx_volume_first = i; - break; - } - } - assert(idx_volume_first != -1); // object without instances? - if (idx_volume_first == -1) - continue; - const Vec3d &rotation0 = volumes[idx_volume_first]->get_instance_rotation(); - for (int i = idx_volume_first + 1; i < (int)volumes.size(); ++i) - if (volumes[i]->object_idx() == idx_object) { - const Vec3d &rotation = volumes[i]->get_instance_rotation(); - assert(is_rotation_xy_synchronized(rotation, rotation0)); - } - } -} -#endif /* NDEBUG */ - -void Selection::synchronize_unselected_instances(SyncRotationType sync_rotation_type) -{ - std::set done; // prevent processing volumes twice - done.insert(m_list.begin(), m_list.end()); - - for (unsigned int i : m_list) { - if (done.size() == m_volumes->size()) - break; - - const GLVolume* volume = (*m_volumes)[i]; -#if ENABLE_WIPETOWER_OBJECTID_1000_REMOVAL - if (volume->is_wipe_tower) - continue; - - const int object_idx = volume->object_idx(); -#else - const int object_idx = volume->object_idx(); - if (object_idx >= 1000) - continue; -#endif // ENABLE_WIPETOWER_OBJECTID_1000_REMOVAL - - const int instance_idx = volume->instance_idx(); - const Vec3d& rotation = volume->get_instance_rotation(); - const Vec3d& scaling_factor = volume->get_instance_scaling_factor(); - const Vec3d& mirror = volume->get_instance_mirror(); - - // Process unselected instances. - for (unsigned int j = 0; j < (unsigned int)m_volumes->size(); ++j) { - if (done.size() == m_volumes->size()) - break; - - if (done.find(j) != done.end()) - continue; - - GLVolume* v = (*m_volumes)[j]; - if (v->object_idx() != object_idx || v->instance_idx() == instance_idx) - continue; - - assert(is_rotation_xy_synchronized(m_cache.volumes_data[i].get_instance_rotation(), m_cache.volumes_data[j].get_instance_rotation())); - switch (sync_rotation_type) { - case SYNC_ROTATION_NONE: { - // z only rotation -> synch instance z - // The X,Y rotations should be synchronized from start to end of the rotation. - assert(is_rotation_xy_synchronized(rotation, v->get_instance_rotation())); - if (wxGetApp().preset_bundle->printers.get_edited_preset().printer_technology() != ptSLA) - v->set_instance_offset(Z, volume->get_instance_offset().z()); - break; - } - case SYNC_ROTATION_GENERAL: - // generic rotation -> update instance z with the delta of the rotation. - const double z_diff = Geometry::rotation_diff_z(m_cache.volumes_data[i].get_instance_rotation(), m_cache.volumes_data[j].get_instance_rotation()); - v->set_instance_rotation({ rotation.x(), rotation.y(), rotation.z() + z_diff }); - break; - } - - v->set_instance_scaling_factor(scaling_factor); - v->set_instance_mirror(mirror); - - done.insert(j); - } - } - -#ifndef NDEBUG - verify_instances_rotation_synchronized(*m_model, *m_volumes); -#endif /* NDEBUG */ -} - -void Selection::synchronize_unselected_volumes() -{ - for (unsigned int i : m_list) { - const GLVolume* volume = (*m_volumes)[i]; -#if ENABLE_WIPETOWER_OBJECTID_1000_REMOVAL - if (volume->is_wipe_tower) - continue; - - const int object_idx = volume->object_idx(); -#else - const int object_idx = volume->object_idx(); - if (object_idx >= 1000) - continue; -#endif // ENABLE_WIPETOWER_OBJECTID_1000_REMOVAL - - const int volume_idx = volume->volume_idx(); - const Vec3d& offset = volume->get_volume_offset(); - const Vec3d& rotation = volume->get_volume_rotation(); - const Vec3d& scaling_factor = volume->get_volume_scaling_factor(); - const Vec3d& mirror = volume->get_volume_mirror(); - - // Process unselected volumes. - for (unsigned int j = 0; j < (unsigned int)m_volumes->size(); ++j) { - if (j == i) - continue; - - GLVolume* v = (*m_volumes)[j]; - if (v->object_idx() != object_idx || v->volume_idx() != volume_idx) - continue; - - v->set_volume_offset(offset); - v->set_volume_rotation(rotation); - v->set_volume_scaling_factor(scaling_factor); - v->set_volume_mirror(mirror); - } - } -} - -void Selection::ensure_on_bed() -{ - typedef std::map, double> InstancesToZMap; - InstancesToZMap instances_min_z; - - for (size_t i = 0; i < m_volumes->size(); ++i) { - GLVolume* volume = (*m_volumes)[i]; - if (!volume->is_wipe_tower && !volume->is_modifier && - std::find(m_cache.sinking_volumes.begin(), m_cache.sinking_volumes.end(), i) == m_cache.sinking_volumes.end()) { - const double min_z = volume->transformed_convex_hull_bounding_box().min.z(); - std::pair instance = std::make_pair(volume->object_idx(), volume->instance_idx()); - InstancesToZMap::iterator it = instances_min_z.find(instance); - if (it == instances_min_z.end()) - it = instances_min_z.insert(InstancesToZMap::value_type(instance, DBL_MAX)).first; - - it->second = std::min(it->second, min_z); - } - } - - for (GLVolume* volume : *m_volumes) { - std::pair instance = std::make_pair(volume->object_idx(), volume->instance_idx()); - InstancesToZMap::iterator it = instances_min_z.find(instance); - if (it != instances_min_z.end()) - volume->set_instance_offset(Z, volume->get_instance_offset(Z) - it->second); - } -} - -void Selection::ensure_not_below_bed() -{ - typedef std::map, double> InstancesToZMap; - InstancesToZMap instances_max_z; - - for (size_t i = 0; i < m_volumes->size(); ++i) { - GLVolume* volume = (*m_volumes)[i]; - if (!volume->is_wipe_tower && !volume->is_modifier) { - const double max_z = volume->transformed_convex_hull_bounding_box().max.z(); - const std::pair instance = std::make_pair(volume->object_idx(), volume->instance_idx()); - InstancesToZMap::iterator it = instances_max_z.find(instance); - if (it == instances_max_z.end()) - it = instances_max_z.insert({ instance, -DBL_MAX }).first; - - it->second = std::max(it->second, max_z); - } - } - - if (is_any_volume()) { - for (unsigned int i : m_list) { - GLVolume& volume = *(*m_volumes)[i]; - const std::pair instance = std::make_pair(volume.object_idx(), volume.instance_idx()); - InstancesToZMap::const_iterator it = instances_max_z.find(instance); - const double z_shift = SINKING_MIN_Z_THRESHOLD - it->second; - if (it != instances_max_z.end() && z_shift > 0.0) - volume.set_volume_offset(Z, volume.get_volume_offset(Z) + z_shift); - } - } - else { - for (GLVolume* volume : *m_volumes) { - const std::pair instance = std::make_pair(volume->object_idx(), volume->instance_idx()); - InstancesToZMap::const_iterator it = instances_max_z.find(instance); - if (it != instances_max_z.end() && it->second < SINKING_MIN_Z_THRESHOLD) - volume->set_instance_offset(Z, volume->get_instance_offset(Z) + SINKING_MIN_Z_THRESHOLD - it->second); - } - } -} - -bool Selection::is_from_fully_selected_instance(unsigned int volume_idx) const -{ - struct SameInstance - { - int obj_idx; - int inst_idx; - GLVolumePtrs& volumes; - - SameInstance(int obj_idx, int inst_idx, GLVolumePtrs& volumes) : obj_idx(obj_idx), inst_idx(inst_idx), volumes(volumes) {} - bool operator () (unsigned int i) { return (volumes[i]->volume_idx() >= 0) && (volumes[i]->object_idx() == obj_idx) && (volumes[i]->instance_idx() == inst_idx); } - }; - - if ((unsigned int)m_volumes->size() <= volume_idx) - return false; - - GLVolume* volume = (*m_volumes)[volume_idx]; - int object_idx = volume->object_idx(); - if ((int)m_model->objects.size() <= object_idx) - return false; - - unsigned int count = (unsigned int)std::count_if(m_list.begin(), m_list.end(), SameInstance(object_idx, volume->instance_idx(), *m_volumes)); - return count == (unsigned int)m_model->objects[object_idx]->volumes.size(); -} - -void Selection::paste_volumes_from_clipboard() -{ -#ifdef _DEBUG - check_model_ids_validity(*m_model); -#endif /* _DEBUG */ - - int dst_obj_idx = get_object_idx(); - if ((dst_obj_idx < 0) || ((int)m_model->objects.size() <= dst_obj_idx)) - return; - - ModelObject* dst_object = m_model->objects[dst_obj_idx]; - - int dst_inst_idx = get_instance_idx(); - if ((dst_inst_idx < 0) || ((int)dst_object->instances.size() <= dst_inst_idx)) - return; - - ModelObject* src_object = m_clipboard.get_object(0); - if (src_object != nullptr) - { - ModelInstance* dst_instance = dst_object->instances[dst_inst_idx]; - BoundingBoxf3 dst_instance_bb = dst_object->instance_bounding_box(dst_inst_idx); - Transform3d src_matrix = src_object->instances[0]->get_transformation().get_matrix(true); - Transform3d dst_matrix = dst_instance->get_transformation().get_matrix(true); - bool from_same_object = (src_object->input_file == dst_object->input_file) && src_matrix.isApprox(dst_matrix); - - // used to keep relative position of multivolume selections when pasting from another object - BoundingBoxf3 total_bb; - - ModelVolumePtrs volumes; - for (ModelVolume* src_volume : src_object->volumes) - { - ModelVolume* dst_volume = dst_object->add_volume(*src_volume); - dst_volume->set_new_unique_id(); - if (from_same_object) - { -// // if the volume comes from the same object, apply the offset in world system -// double offset = wxGetApp().plater()->canvas3D()->get_size_proportional_to_max_bed_size(0.05); -// dst_volume->translate(dst_matrix.inverse() * Vec3d(offset, offset, 0.0)); - } - else - { - // if the volume comes from another object, apply the offset as done when adding modifiers - // see ObjectList::load_generic_subobject() - total_bb.merge(dst_volume->mesh().bounding_box().transformed(src_volume->get_matrix())); - } - - volumes.push_back(dst_volume); -#ifdef _DEBUG - check_model_ids_validity(*m_model); -#endif /* _DEBUG */ - } - - // keeps relative position of multivolume selections - if (!from_same_object) - { - for (ModelVolume* v : volumes) - { - v->set_offset((v->get_offset() - total_bb.center()) + dst_matrix.inverse() * (Vec3d(dst_instance_bb.max(0), dst_instance_bb.min(1), dst_instance_bb.min(2)) + 0.5 * total_bb.size() - dst_instance->get_transformation().get_offset())); - } - } - - wxGetApp().obj_list()->paste_volumes_into_list(dst_obj_idx, volumes); - } - -#ifdef _DEBUG - check_model_ids_validity(*m_model); -#endif /* _DEBUG */ -} - -void Selection::paste_objects_from_clipboard() -{ -#ifdef _DEBUG - check_model_ids_validity(*m_model); -#endif /* _DEBUG */ - - std::vector object_idxs; - const ModelObjectPtrs& src_objects = m_clipboard.get_objects(); - for (const ModelObject* src_object : src_objects) - { - ModelObject* dst_object = m_model->add_object(*src_object); - double offset = wxGetApp().plater()->canvas3D()->get_size_proportional_to_max_bed_size(0.05); - Vec3d displacement(offset, offset, 0.0); - for (ModelInstance* inst : dst_object->instances) - { - inst->set_offset(inst->get_offset() + displacement); - } - - object_idxs.push_back(m_model->objects.size() - 1); -#ifdef _DEBUG - check_model_ids_validity(*m_model); -#endif /* _DEBUG */ - } - - wxGetApp().obj_list()->paste_objects_into_list(object_idxs); - -#ifdef _DEBUG - check_model_ids_validity(*m_model); -#endif /* _DEBUG */ -} - -} // namespace GUI -} // namespace Slic3r +#include "libslic3r/libslic3r.h" +#include "Selection.hpp" + +#include "3DScene.hpp" +#include "GLCanvas3D.hpp" +#include "GUI_App.hpp" +#include "GUI.hpp" +#include "GUI_ObjectManipulation.hpp" +#include "GUI_ObjectList.hpp" +#include "Camera.hpp" +#include "Plater.hpp" +#if ENABLE_WORLD_COORDINATE +#include "MsgDialog.hpp" +#endif // ENABLE_WORLD_COORDINATE + +#include "Gizmos/GLGizmoBase.hpp" + +#include "slic3r/Utils/UndoRedo.hpp" + +#include "libslic3r/LocalesUtils.hpp" +#include "libslic3r/Model.hpp" +#include "libslic3r/PresetBundle.hpp" +#include "libslic3r/BuildVolume.hpp" + +#include + +#include +#include + +static const Slic3r::ColorRGBA UNIFORM_SCALE_COLOR = Slic3r::ColorRGBA::ORANGE(); +static const Slic3r::ColorRGBA SOLID_PLANE_COLOR = Slic3r::ColorRGBA::ORANGE(); +static const Slic3r::ColorRGBA TRANSPARENT_PLANE_COLOR = { 0.8f, 0.8f, 0.8f, 0.5f }; + +namespace Slic3r { +namespace GUI { + +Selection::VolumeCache::TransformCache::TransformCache(const Geometry::Transformation& transform) + : position(transform.get_offset()) + , rotation(transform.get_rotation()) + , scaling_factor(transform.get_scaling_factor()) + , mirror(transform.get_mirror()) + , full_matrix(transform.get_matrix()) +#if ENABLE_WORLD_COORDINATE + , transform(transform) + , rotation_matrix(transform.get_rotation_matrix()) + , scale_matrix(transform.get_scaling_factor_matrix()) + , mirror_matrix(transform.get_mirror_matrix()) +#endif // ENABLE_WORLD_COORDINATE +{ +#if !ENABLE_WORLD_COORDINATE + rotation_matrix = Geometry::assemble_transform(Vec3d::Zero(), rotation); + scale_matrix = Geometry::assemble_transform(Vec3d::Zero(), Vec3d::Zero(), scaling_factor); + mirror_matrix = Geometry::assemble_transform(Vec3d::Zero(), Vec3d::Zero(), Vec3d::Ones(), mirror); +#endif // !ENABLE_WORLD_COORDINATE +} + +Selection::VolumeCache::VolumeCache(const Geometry::Transformation& volume_transform, const Geometry::Transformation& instance_transform) + : m_volume(volume_transform) + , m_instance(instance_transform) +{ +} + +bool Selection::Clipboard::is_sla_compliant() const +{ + if (m_mode == Selection::Volume) + return false; + + for (const ModelObject* o : m_model->objects) { + if (o->is_multiparts()) + return false; + + for (const ModelVolume* v : o->volumes) { + if (v->is_modifier()) + return false; + } + } + + return true; +} + +Selection::Clipboard::Clipboard() +{ + m_model.reset(new Model); +} + +void Selection::Clipboard::reset() +{ + m_model->clear_objects(); +} + +bool Selection::Clipboard::is_empty() const +{ + return m_model->objects.empty(); +} + +ModelObject* Selection::Clipboard::add_object() +{ + return m_model->add_object(); +} + +ModelObject* Selection::Clipboard::get_object(unsigned int id) +{ + return (id < (unsigned int)m_model->objects.size()) ? m_model->objects[id] : nullptr; +} + +const ModelObjectPtrs& Selection::Clipboard::get_objects() const +{ + return m_model->objects; +} + +Selection::Selection() + : m_volumes(nullptr) + , m_model(nullptr) + , m_enabled(false) + , m_mode(Instance) + , m_type(Empty) + , m_valid(false) + , m_scale_factor(1.0f) +{ + this->set_bounding_boxes_dirty(); +#if ENABLE_WORLD_COORDINATE + m_axes.set_stem_radius(0.15f); + m_axes.set_stem_length(3.0f); + m_axes.set_tip_radius(0.45f); + m_axes.set_tip_length(1.5f); +#endif // ENABLE_WORLD_COORDINATE +} + + +void Selection::set_volumes(GLVolumePtrs* volumes) +{ + m_volumes = volumes; + update_valid(); +} + +// Init shall be called from the OpenGL render function, so that the OpenGL context is initialized! +bool Selection::init() +{ + m_arrow.init_from(straight_arrow(10.0f, 5.0f, 5.0f, 10.0f, 1.0f)); + m_curved_arrow.init_from(circular_arrow(16, 10.0f, 5.0f, 10.0f, 5.0f, 1.0f)); +#if ENABLE_RENDER_SELECTION_CENTER + m_vbo_sphere.init_from(its_make_sphere(0.75, PI / 12.0)); +#endif // ENABLE_RENDER_SELECTION_CENTER + + return true; +} + +void Selection::set_model(Model* model) +{ + m_model = model; + update_valid(); +} + +void Selection::add(unsigned int volume_idx, bool as_single_selection, bool check_for_already_contained) +{ + if (!m_valid || (unsigned int)m_volumes->size() <= volume_idx) + return; + + const GLVolume* volume = (*m_volumes)[volume_idx]; + // wipe tower is already selected + if (is_wipe_tower() && volume->is_wipe_tower) + return; + + bool keep_instance_mode = (m_mode == Instance) && !as_single_selection; + bool already_contained = check_for_already_contained && contains_volume(volume_idx); + + // resets the current list if needed + bool needs_reset = as_single_selection && !already_contained; + needs_reset |= volume->is_wipe_tower; + needs_reset |= is_wipe_tower() && !volume->is_wipe_tower; + needs_reset |= as_single_selection && !is_any_modifier() && volume->is_modifier; + needs_reset |= is_any_modifier() && !volume->is_modifier; + + if (!already_contained || needs_reset) { + wxGetApp().plater()->take_snapshot(_L("Selection-Add"), UndoRedo::SnapshotType::Selection); + + if (needs_reset) + clear(); + + if (!keep_instance_mode) + m_mode = volume->is_modifier ? Volume : Instance; + } + else + // keep current mode + return; + + switch (m_mode) + { + case Volume: + { + if (volume->volume_idx() >= 0 && (is_empty() || volume->instance_idx() == get_instance_idx())) + do_add_volume(volume_idx); + + break; + } + case Instance: + { + Plater::SuppressSnapshots suppress(wxGetApp().plater()); + add_instance(volume->object_idx(), volume->instance_idx(), as_single_selection); + break; + } + } + + update_type(); + this->set_bounding_boxes_dirty(); +} + +void Selection::remove(unsigned int volume_idx) +{ + if (!m_valid || (unsigned int)m_volumes->size() <= volume_idx) + return; + + if (!contains_volume(volume_idx)) + return; + + wxGetApp().plater()->take_snapshot(_L("Selection-Remove"), UndoRedo::SnapshotType::Selection); + + GLVolume* volume = (*m_volumes)[volume_idx]; + + switch (m_mode) + { + case Volume: + { + do_remove_volume(volume_idx); + break; + } + case Instance: + { + do_remove_instance(volume->object_idx(), volume->instance_idx()); + break; + } + } + + update_type(); + this->set_bounding_boxes_dirty(); +} + +void Selection::add_object(unsigned int object_idx, bool as_single_selection) +{ + if (!m_valid) + return; + + std::vector volume_idxs = get_volume_idxs_from_object(object_idx); + if ((!as_single_selection && contains_all_volumes(volume_idxs)) || + (as_single_selection && matches(volume_idxs))) + return; + + wxGetApp().plater()->take_snapshot(_L("Selection-Add Object"), UndoRedo::SnapshotType::Selection); + + // resets the current list if needed + if (as_single_selection) + clear(); + + m_mode = Instance; + + do_add_volumes(volume_idxs); + + update_type(); + this->set_bounding_boxes_dirty(); +} + +void Selection::remove_object(unsigned int object_idx) +{ + if (!m_valid) + return; + + wxGetApp().plater()->take_snapshot(_L("Selection-Remove Object"), UndoRedo::SnapshotType::Selection); + + do_remove_object(object_idx); + + update_type(); + this->set_bounding_boxes_dirty(); +} + +void Selection::add_instance(unsigned int object_idx, unsigned int instance_idx, bool as_single_selection) +{ + if (!m_valid) + return; + + const std::vector volume_idxs = get_volume_idxs_from_instance(object_idx, instance_idx); + if ((!as_single_selection && contains_all_volumes(volume_idxs)) || + (as_single_selection && matches(volume_idxs))) + return; + + wxGetApp().plater()->take_snapshot(_L("Selection-Add Instance"), UndoRedo::SnapshotType::Selection); + + // resets the current list if needed + if (as_single_selection) + clear(); + + m_mode = Instance; + + do_add_volumes(volume_idxs); + + update_type(); + this->set_bounding_boxes_dirty(); +} + +void Selection::remove_instance(unsigned int object_idx, unsigned int instance_idx) +{ + if (!m_valid) + return; + + wxGetApp().plater()->take_snapshot(_L("Selection-Remove Instance"), UndoRedo::SnapshotType::Selection); + + do_remove_instance(object_idx, instance_idx); + + update_type(); + this->set_bounding_boxes_dirty(); +} + +void Selection::add_volume(unsigned int object_idx, unsigned int volume_idx, int instance_idx, bool as_single_selection) +{ + if (!m_valid) + return; + + std::vector volume_idxs = get_volume_idxs_from_volume(object_idx, instance_idx, volume_idx); + if ((!as_single_selection && contains_all_volumes(volume_idxs)) || + (as_single_selection && matches(volume_idxs))) + return; + + // resets the current list if needed + if (as_single_selection) + clear(); + + m_mode = Volume; + + do_add_volumes(volume_idxs); + + update_type(); + this->set_bounding_boxes_dirty(); +} + +void Selection::remove_volume(unsigned int object_idx, unsigned int volume_idx) +{ + if (!m_valid) + return; + + for (unsigned int i = 0; i < (unsigned int)m_volumes->size(); ++i) { + GLVolume* v = (*m_volumes)[i]; + if (v->object_idx() == (int)object_idx && v->volume_idx() == (int)volume_idx) + do_remove_volume(i); + } + + update_type(); + this->set_bounding_boxes_dirty(); +} + +void Selection::add_volumes(EMode mode, const std::vector& volume_idxs, bool as_single_selection) +{ + if (!m_valid) + return; + + if ((!as_single_selection && contains_all_volumes(volume_idxs)) || + (as_single_selection && matches(volume_idxs))) + return; + + // resets the current list if needed + if (as_single_selection) + clear(); + + m_mode = mode; + for (unsigned int i : volume_idxs) { + if (i < (unsigned int)m_volumes->size()) + do_add_volume(i); + } + + update_type(); + this->set_bounding_boxes_dirty(); +} + +void Selection::remove_volumes(EMode mode, const std::vector& volume_idxs) +{ + if (!m_valid) + return; + + m_mode = mode; + for (unsigned int i : volume_idxs) { + if (i < (unsigned int)m_volumes->size()) + do_remove_volume(i); + } + + update_type(); + this->set_bounding_boxes_dirty(); +} + +void Selection::add_all() +{ + if (!m_valid) + return; + + unsigned int count = 0; + for (unsigned int i = 0; i < (unsigned int)m_volumes->size(); ++i) { + if (!(*m_volumes)[i]->is_wipe_tower) + ++count; + } + + if ((unsigned int)m_list.size() == count) + return; + + wxGetApp().plater()->take_snapshot(_(L("Selection-Add All")), UndoRedo::SnapshotType::Selection); + + m_mode = Instance; + clear(); + + for (unsigned int i = 0; i < (unsigned int)m_volumes->size(); ++i) { + if (!(*m_volumes)[i]->is_wipe_tower) + do_add_volume(i); + } + + update_type(); + this->set_bounding_boxes_dirty(); +} + +void Selection::remove_all() +{ + if (!m_valid) + return; + + if (is_empty()) + return; + +// Not taking the snapshot with non-empty Redo stack will likely be more confusing than losing the Redo stack. +// Let's wait for user feedback. +// if (!wxGetApp().plater()->can_redo()) + wxGetApp().plater()->take_snapshot(_L("Selection-Remove All"), UndoRedo::SnapshotType::Selection); + + m_mode = Instance; + clear(); +} + +void Selection::set_deserialized(EMode mode, const std::vector> &volumes_and_instances) +{ + if (! m_valid) + return; + + m_mode = mode; + for (unsigned int i : m_list) + (*m_volumes)[i]->selected = false; + m_list.clear(); + for (unsigned int i = 0; i < (unsigned int)m_volumes->size(); ++ i) + if (std::binary_search(volumes_and_instances.begin(), volumes_and_instances.end(), (*m_volumes)[i]->geometry_id)) + do_add_volume(i); + update_type(); + set_bounding_boxes_dirty(); +} + +void Selection::clear() +{ + if (!m_valid) + return; + + if (m_list.empty()) + return; + + // ensure that the volumes get the proper color before next call to render (expecially needed for transparent volumes) + for (unsigned int i : m_list) { + GLVolume& volume = *(*m_volumes)[i]; + volume.selected = false; + volume.set_render_color(volume.color.is_transparent()); + } + + m_list.clear(); + + update_type(); + set_bounding_boxes_dirty(); + + // this happens while the application is closing + if (wxGetApp().obj_manipul() == nullptr) + return; + + // resets the cache in the sidebar + wxGetApp().obj_manipul()->reset_cache(); + + // #et_FIXME fake KillFocus from sidebar + wxGetApp().plater()->canvas3D()->handle_sidebar_focus_event("", false); +} + +// Update the selection based on the new instance IDs. +void Selection::instances_changed(const std::vector &instance_ids_selected) +{ + assert(m_valid); + assert(m_mode == Instance); + m_list.clear(); + for (unsigned int volume_idx = 0; volume_idx < (unsigned int)m_volumes->size(); ++ volume_idx) { + const GLVolume *volume = (*m_volumes)[volume_idx]; + auto it = std::lower_bound(instance_ids_selected.begin(), instance_ids_selected.end(), volume->geometry_id.second); + if (it != instance_ids_selected.end() && *it == volume->geometry_id.second) + this->do_add_volume(volume_idx); + } + update_type(); + this->set_bounding_boxes_dirty(); +} + +// Update the selection based on the map from old indices to new indices after m_volumes changed. +// If the current selection is by instance, this call may select newly added volumes, if they belong to already selected instances. +void Selection::volumes_changed(const std::vector &map_volume_old_to_new) +{ + assert(m_valid); + assert(m_mode == Volume); + IndicesList list_new; + for (unsigned int idx : m_list) + if (map_volume_old_to_new[idx] != size_t(-1)) { + unsigned int new_idx = (unsigned int)map_volume_old_to_new[idx]; + (*m_volumes)[new_idx]->selected = true; + list_new.insert(new_idx); + } + m_list = std::move(list_new); + update_type(); + this->set_bounding_boxes_dirty(); +} + +bool Selection::is_single_full_instance() const +{ + if (m_type == SingleFullInstance) + return true; + + if (m_type == SingleFullObject) + return get_instance_idx() != -1; + + if (m_list.empty() || m_volumes->empty()) + return false; + + int object_idx = m_valid ? get_object_idx() : -1; + if (object_idx < 0 || (int)m_model->objects.size() <= object_idx) + return false; + + int instance_idx = (*m_volumes)[*m_list.begin()]->instance_idx(); + + std::set volumes_idxs; + for (unsigned int i : m_list) { + const GLVolume* v = (*m_volumes)[i]; + if (object_idx != v->object_idx() || instance_idx != v->instance_idx()) + return false; + + int volume_idx = v->volume_idx(); + if (volume_idx >= 0) + volumes_idxs.insert(volume_idx); + } + + return m_model->objects[object_idx]->volumes.size() == volumes_idxs.size(); +} + +bool Selection::is_from_single_object() const +{ + const int idx = get_object_idx(); +#if ENABLE_WIPETOWER_OBJECTID_1000_REMOVAL + return 0 <= idx && idx < int(m_model->objects.size()); +#else + return 0 <= idx && idx < 1000; +#endif // ENABLE_WIPETOWER_OBJECTID_1000_REMOVAL +} + +bool Selection::is_sla_compliant() const +{ + if (m_mode == Volume) + return false; + + for (unsigned int i : m_list) { + if ((*m_volumes)[i]->is_modifier) + return false; + } + + return true; +} + +bool Selection::contains_all_volumes(const std::vector& volume_idxs) const +{ + for (unsigned int i : volume_idxs) { + if (m_list.find(i) == m_list.end()) + return false; + } + + return true; +} + +bool Selection::contains_any_volume(const std::vector& volume_idxs) const +{ + for (unsigned int i : volume_idxs) { + if (m_list.find(i) != m_list.end()) + return true; + } + + return false; +} + +bool Selection::matches(const std::vector& volume_idxs) const +{ + unsigned int count = 0; + + for (unsigned int i : volume_idxs) { + if (m_list.find(i) != m_list.end()) + ++count; + else + return false; + } + + return count == (unsigned int)m_list.size(); +} + +#if !ENABLE_WORLD_COORDINATE +bool Selection::requires_uniform_scale() const +{ + if (is_single_full_instance() || is_single_modifier() || is_single_volume()) + return false; + + return true; +} +#endif // !ENABLE_WORLD_COORDINATE + +int Selection::get_object_idx() const +{ + return (m_cache.content.size() == 1) ? m_cache.content.begin()->first : -1; +} + +int Selection::get_instance_idx() const +{ + if (m_cache.content.size() == 1) { + const InstanceIdxsList& idxs = m_cache.content.begin()->second; + if (idxs.size() == 1) + return *idxs.begin(); + } + + return -1; +} + +const Selection::InstanceIdxsList& Selection::get_instance_idxs() const +{ + assert(m_cache.content.size() == 1); + return m_cache.content.begin()->second; +} + +const GLVolume* Selection::get_volume(unsigned int volume_idx) const +{ + return (m_valid && (volume_idx < (unsigned int)m_volumes->size())) ? (*m_volumes)[volume_idx] : nullptr; +} + +const BoundingBoxf3& Selection::get_bounding_box() const +{ + if (!m_bounding_box.has_value()) { + std::optional* bbox = const_cast*>(&m_bounding_box); + *bbox = BoundingBoxf3(); + if (m_valid) { + for (unsigned int i : m_list) { + (*bbox)->merge((*m_volumes)[i]->transformed_convex_hull_bounding_box()); + } + } + } + return *m_bounding_box; +} + +const BoundingBoxf3& Selection::get_unscaled_instance_bounding_box() const +{ + assert(is_single_full_instance()); + + if (!m_unscaled_instance_bounding_box.has_value()) { + std::optional* bbox = const_cast*>(&m_unscaled_instance_bounding_box); + *bbox = BoundingBoxf3(); + if (m_valid) { + for (unsigned int i : m_list) { + const GLVolume& volume = *(*m_volumes)[i]; + if (volume.is_modifier) + continue; +#if ENABLE_WORLD_COORDINATE + Transform3d trafo = volume.get_instance_transformation().get_matrix_no_scaling_factor() * volume.get_volume_transformation().get_matrix(); +#else + Transform3d trafo = volume.get_instance_transformation().get_matrix(false, false, true, false) * volume.get_volume_transformation().get_matrix(); +#endif // ENABLE_WORLD_COORDINATE + trafo.translation().z() += volume.get_sla_shift_z(); + (*bbox)->merge(volume.transformed_convex_hull_bounding_box(trafo)); + } + } + } + return *m_unscaled_instance_bounding_box; +} + +const BoundingBoxf3& Selection::get_scaled_instance_bounding_box() const +{ + assert(is_single_full_instance()); + + if (!m_scaled_instance_bounding_box.has_value()) { + std::optional* bbox = const_cast*>(&m_scaled_instance_bounding_box); + *bbox = BoundingBoxf3(); + if (m_valid) { + for (unsigned int i : m_list) { + const GLVolume& volume = *(*m_volumes)[i]; + if (volume.is_modifier) + continue; + Transform3d trafo = volume.get_instance_transformation().get_matrix() * volume.get_volume_transformation().get_matrix(); + trafo.translation().z() += volume.get_sla_shift_z(); + (*bbox)->merge(volume.transformed_convex_hull_bounding_box(trafo)); + } + } + } + return *m_scaled_instance_bounding_box; +} + +#if ENABLE_WORLD_COORDINATE +const BoundingBoxf3& Selection::get_full_unscaled_instance_bounding_box() const +{ + assert(is_single_full_instance()); + + if (!m_full_unscaled_instance_bounding_box.has_value()) { + std::optional* bbox = const_cast*>(&m_full_unscaled_instance_bounding_box); + *bbox = BoundingBoxf3(); + if (m_valid) { + for (unsigned int i : m_list) { + const GLVolume& volume = *(*m_volumes)[i]; + Transform3d trafo = volume.get_instance_transformation().get_matrix_no_scaling_factor() * volume.get_volume_transformation().get_matrix(); + trafo.translation().z() += volume.get_sla_shift_z(); + (*bbox)->merge(volume.transformed_convex_hull_bounding_box(trafo)); + } + } + } + return *m_full_unscaled_instance_bounding_box; +} + +const BoundingBoxf3& Selection::get_full_scaled_instance_bounding_box() const +{ + assert(is_single_full_instance()); + + if (!m_full_scaled_instance_bounding_box.has_value()) { + std::optional* bbox = const_cast*>(&m_full_scaled_instance_bounding_box); + *bbox = BoundingBoxf3(); + if (m_valid) { + for (unsigned int i : m_list) { + const GLVolume& volume = *(*m_volumes)[i]; + Transform3d trafo = volume.get_instance_transformation().get_matrix() * volume.get_volume_transformation().get_matrix(); + trafo.translation().z() += volume.get_sla_shift_z(); + (*bbox)->merge(volume.transformed_convex_hull_bounding_box(trafo)); + } + } + } + return *m_full_scaled_instance_bounding_box; +} + +const BoundingBoxf3& Selection::get_full_unscaled_instance_local_bounding_box() const +{ + assert(is_single_full_instance()); + + if (!m_full_unscaled_instance_local_bounding_box.has_value()) { + std::optional* bbox = const_cast*>(&m_full_unscaled_instance_local_bounding_box); + *bbox = BoundingBoxf3(); + if (m_valid) { + for (unsigned int i : m_list) { + const GLVolume& volume = *(*m_volumes)[i]; + Transform3d trafo = volume.get_volume_transformation().get_matrix(); + trafo.translation().z() += volume.get_sla_shift_z(); + (*bbox)->merge(volume.transformed_convex_hull_bounding_box(trafo)); + } + } + } + return *m_full_unscaled_instance_local_bounding_box; +} +#endif // ENABLE_WORLD_COORDINATE + +void Selection::setup_cache() +{ + if (!m_valid) + return; + + set_caches(); +} + +#if ENABLE_WORLD_COORDINATE +void Selection::translate(const Vec3d& displacement, TransformationType transformation_type) +{ + if (!m_valid) + return; + + assert(transformation_type.relative()); + + for (unsigned int i : m_list) { + GLVolume& v = *(*m_volumes)[i]; + const VolumeCache& volume_data = m_cache.volumes_data[i]; + if (m_mode == Instance && !is_wipe_tower()) { + assert(is_from_fully_selected_instance(i)); + if (transformation_type.world()) + v.set_instance_transformation(Geometry::translation_transform(displacement) * volume_data.get_instance_full_matrix()); + else if (transformation_type.local()) { + const Vec3d world_displacement = volume_data.get_instance_rotation_matrix() * displacement; + v.set_instance_transformation(Geometry::translation_transform(world_displacement) * volume_data.get_instance_full_matrix()); + } + else + assert(false); + } + else { + const Vec3d offset = transformation_type.local() ? + (Vec3d)(volume_data.get_volume_transform().get_rotation_matrix() * displacement) : displacement; + transform_volume_relative(v, volume_data, transformation_type, Geometry::translation_transform(offset)); + } + } + +#if !DISABLE_INSTANCES_SYNCH + if (m_mode == Instance) + synchronize_unselected_instances(SyncRotationType::NONE); + else if (m_mode == Volume) + synchronize_unselected_volumes(); +#endif // !DISABLE_INSTANCES_SYNCH + + ensure_not_below_bed(); + set_bounding_boxes_dirty(); + wxGetApp().plater()->canvas3D()->requires_check_outside_state(); +} +#else +void Selection::translate(const Vec3d& displacement, bool local) +{ + if (!m_valid) + return; + + EMode translation_type = m_mode; + + for (unsigned int i : m_list) { + GLVolume& v = *(*m_volumes)[i]; + if (m_mode == Volume || v.is_wipe_tower) { + if (local) + v.set_volume_offset(m_cache.volumes_data[i].get_volume_position() + displacement); + else { + const Vec3d local_displacement = (m_cache.volumes_data[i].get_instance_rotation_matrix() * m_cache.volumes_data[i].get_instance_scale_matrix() * m_cache.volumes_data[i].get_instance_mirror_matrix()).inverse() * displacement; + v.set_volume_offset(m_cache.volumes_data[i].get_volume_position() + local_displacement); + } + } + else if (m_mode == Instance) { + if (is_from_fully_selected_instance(i)) + v.set_instance_offset(m_cache.volumes_data[i].get_instance_position() + displacement); + else { + const Vec3d local_displacement = (m_cache.volumes_data[i].get_instance_rotation_matrix() * m_cache.volumes_data[i].get_instance_scale_matrix() * m_cache.volumes_data[i].get_instance_mirror_matrix()).inverse() * displacement; + v.set_volume_offset(m_cache.volumes_data[i].get_volume_position() + local_displacement); + translation_type = Volume; + } + } + } + +#if !DISABLE_INSTANCES_SYNCH + if (translation_type == Instance) + synchronize_unselected_instances(SyncRotationType::NONE); + else if (translation_type == Volume) + synchronize_unselected_volumes(); +#endif // !DISABLE_INSTANCES_SYNCH + + ensure_not_below_bed(); + set_bounding_boxes_dirty(); + wxGetApp().plater()->canvas3D()->requires_check_outside_state(); +} +#endif // ENABLE_WORLD_COORDINATE + +// Rotate an object around one of the axes. Only one rotation component is expected to be changing. +#if ENABLE_WORLD_COORDINATE +void Selection::rotate(const Vec3d& rotation, TransformationType transformation_type) +{ + if (!m_valid) + return; + + assert(transformation_type.relative() || (transformation_type.absolute() && transformation_type.local())); + + const Transform3d rotation_matrix = Geometry::rotation_transform(rotation); + + for (unsigned int i : m_list) { + GLVolume& v = *(*m_volumes)[i]; + const VolumeCache& volume_data = m_cache.volumes_data[i]; + const Geometry::Transformation& inst_trafo = volume_data.get_instance_transform(); + if (m_mode == Instance && !is_wipe_tower()) { + assert(is_from_fully_selected_instance(i)); + Transform3d new_rotation_matrix = Transform3d::Identity(); + if (transformation_type.absolute()) + new_rotation_matrix = rotation_matrix; + else { + if (transformation_type.world()) + new_rotation_matrix = rotation_matrix * inst_trafo.get_rotation_matrix(); + else if (transformation_type.local()) + new_rotation_matrix = inst_trafo.get_rotation_matrix() * rotation_matrix; + else + assert(false); + } + + const Vec3d new_offset = transformation_type.independent() ? inst_trafo.get_offset() : + m_cache.dragging_center + new_rotation_matrix * inst_trafo.get_rotation_matrix().inverse() * + (inst_trafo.get_offset() - m_cache.dragging_center); + v.set_instance_transformation(Geometry::assemble_transform(Geometry::translation_transform(new_offset), new_rotation_matrix, + inst_trafo.get_scaling_factor_matrix(), inst_trafo.get_mirror_matrix())); + } + else { + if (transformation_type.absolute()) { + const Geometry::Transformation& volume_trafo = volume_data.get_volume_transform(); + v.set_volume_transformation(Geometry::assemble_transform(volume_trafo.get_offset_matrix(), Geometry::rotation_transform(rotation), + volume_trafo.get_scaling_factor_matrix(), volume_trafo.get_mirror_matrix())); + } + else + transform_volume_relative(v, volume_data, transformation_type, Geometry::rotation_transform(rotation)); + } + } + +#if !DISABLE_INSTANCES_SYNCH + if (m_mode == Instance) { + int rot_axis_max = 0; + rotation.cwiseAbs().maxCoeff(&rot_axis_max); + SyncRotationType synch; + if (transformation_type.world() && rot_axis_max == 2) + synch = SyncRotationType::NONE; + else if (transformation_type.local()) + synch = SyncRotationType::FULL; + else + synch = SyncRotationType::GENERAL; + synchronize_unselected_instances(synch); + } + else if (m_mode == Volume) + synchronize_unselected_volumes(); +#endif // !DISABLE_INSTANCES_SYNCH + + set_bounding_boxes_dirty(); + wxGetApp().plater()->canvas3D()->requires_check_outside_state(); +} +#else +void Selection::rotate(const Vec3d& rotation, TransformationType transformation_type) +{ + if (!m_valid) + return; + + // Only relative rotation values are allowed in the world coordinate system. + assert(!transformation_type.world() || transformation_type.relative()); + + if (!is_wipe_tower()) { + int rot_axis_max = 0; + if (rotation.isApprox(Vec3d::Zero())) { + for (unsigned int i : m_list) { + GLVolume &v = *(*m_volumes)[i]; + if (m_mode == Instance) { + v.set_instance_rotation(m_cache.volumes_data[i].get_instance_rotation()); + v.set_instance_offset(m_cache.volumes_data[i].get_instance_position()); + } + else if (m_mode == Volume) { + v.set_volume_rotation(m_cache.volumes_data[i].get_volume_rotation()); + v.set_volume_offset(m_cache.volumes_data[i].get_volume_position()); + } + } + } + else { // this is not the wipe tower + //FIXME this does not work for absolute rotations (transformation_type.absolute() is true) + rotation.cwiseAbs().maxCoeff(&rot_axis_max); + +// if ( single instance or single volume ) + // Rotate around center , if only a single object or volume +// transformation_type.set_independent(); + + // For generic rotation, we want to rotate the first volume in selection, and then to synchronize the other volumes with it. + std::vector object_instance_first(m_model->objects.size(), -1); + auto rotate_instance = [this, &rotation, &object_instance_first, rot_axis_max, transformation_type](GLVolume &volume, int i) { + const int first_volume_idx = object_instance_first[volume.object_idx()]; + if (rot_axis_max != 2 && first_volume_idx != -1) { + // Generic rotation, but no rotation around the Z axis. + // Always do a local rotation (do not consider the selection to be a rigid body). + assert(is_approx(rotation.z(), 0.0)); + const GLVolume &first_volume = *(*m_volumes)[first_volume_idx]; + const Vec3d &rotation = first_volume.get_instance_rotation(); + const double z_diff = Geometry::rotation_diff_z(m_cache.volumes_data[first_volume_idx].get_instance_rotation(), m_cache.volumes_data[i].get_instance_rotation()); + volume.set_instance_rotation(Vec3d(rotation.x(), rotation.y(), rotation.z() + z_diff)); + } + else { + // extracts rotations from the composed transformation + const Vec3d new_rotation = transformation_type.world() ? + Geometry::extract_euler_angles(Geometry::assemble_transform(Vec3d::Zero(), rotation) * m_cache.volumes_data[i].get_instance_rotation_matrix()) : + transformation_type.absolute() ? rotation : rotation + m_cache.volumes_data[i].get_instance_rotation(); + if (rot_axis_max == 2 && transformation_type.joint()) { + // Only allow rotation of multiple instances as a single rigid body when rotating around the Z axis. + const double z_diff = Geometry::rotation_diff_z(m_cache.volumes_data[i].get_instance_rotation(), new_rotation); + volume.set_instance_offset(m_cache.dragging_center + Eigen::AngleAxisd(z_diff, Vec3d::UnitZ()) * (m_cache.volumes_data[i].get_instance_position() - m_cache.dragging_center)); + } + volume.set_instance_rotation(new_rotation); + object_instance_first[volume.object_idx()] = i; + } + }; + + for (unsigned int i : m_list) { + GLVolume &v = *(*m_volumes)[i]; + if (is_single_full_instance()) + rotate_instance(v, i); + else if (is_single_volume() || is_single_modifier()) { + if (transformation_type.independent()) + v.set_volume_rotation(m_cache.volumes_data[i].get_volume_rotation() + rotation); + else { + const Transform3d m = Geometry::assemble_transform(Vec3d::Zero(), rotation); + const Vec3d new_rotation = Geometry::extract_euler_angles(m * m_cache.volumes_data[i].get_volume_rotation_matrix()); + v.set_volume_rotation(new_rotation); + } + } + else { + if (m_mode == Instance) + rotate_instance(v, i); + else if (m_mode == Volume) { + // extracts rotations from the composed transformation + const Transform3d m = Geometry::assemble_transform(Vec3d::Zero(), rotation); + const Vec3d new_rotation = Geometry::extract_euler_angles(m * m_cache.volumes_data[i].get_volume_rotation_matrix()); + if (transformation_type.joint()) { + const Vec3d local_pivot = m_cache.volumes_data[i].get_instance_full_matrix().inverse() * m_cache.dragging_center; + const Vec3d offset = m * (m_cache.volumes_data[i].get_volume_position() - local_pivot); + v.set_volume_offset(local_pivot + offset); + } + v.set_volume_rotation(new_rotation); + } + } + } + } + +#if !DISABLE_INSTANCES_SYNCH + if (m_mode == Instance) + synchronize_unselected_instances((rot_axis_max == 2) ? SyncRotationType::NONE : SyncRotationType::GENERAL); + else if (m_mode == Volume) + synchronize_unselected_volumes(); +#endif // !DISABLE_INSTANCES_SYNCH + } + else { // it's the wipe tower that's selected and being rotated + GLVolume& volume = *((*m_volumes)[*m_list.begin()]); // the wipe tower is always alone in the selection + + // make sure the wipe tower rotates around its center, not origin + // we can assume that only Z rotation changes + const Vec3d center_local = volume.transformed_bounding_box().center() - volume.get_volume_offset(); + const Vec3d center_local_new = Eigen::AngleAxisd(rotation.z()-volume.get_volume_rotation().z(), Vec3d::UnitZ()) * center_local; + volume.set_volume_rotation(rotation); + volume.set_volume_offset(volume.get_volume_offset() + center_local - center_local_new); + } + + set_bounding_boxes_dirty(); + wxGetApp().plater()->canvas3D()->requires_check_outside_state(); +} +#endif // ENABLE_WORLD_COORDINATE + +void Selection::flattening_rotate(const Vec3d& normal) +{ + // We get the normal in untransformed coordinates. We must transform it using the instance matrix, find out + // how to rotate the instance so it faces downwards and do the rotation. All that for all selected instances. + // The function assumes that is_from_single_object() holds. + assert(Slic3r::is_approx(normal.norm(), 1.)); + + if (!m_valid) + return; + + for (unsigned int i : m_list) { + GLVolume& v = *(*m_volumes)[i]; + // Normal transformed from the object coordinate space to the world coordinate space. +#if ENABLE_WORLD_COORDINATE + const Geometry::Transformation& old_inst_trafo = v.get_instance_transformation(); + const Vec3d tnormal = old_inst_trafo.get_matrix().matrix().block(0, 0, 3, 3).inverse().transpose() * normal; + // Additional rotation to align tnormal with the down vector in the world coordinate space. + const Transform3d rotation_matrix = Transform3d(Eigen::Quaterniond().setFromTwoVectors(tnormal, -Vec3d::UnitZ())); + v.set_instance_transformation(old_inst_trafo.get_offset_matrix() * rotation_matrix * old_inst_trafo.get_matrix_no_offset()); +#else + const auto& voldata = m_cache.volumes_data[i]; + Vec3d tnormal = (Geometry::assemble_transform( + Vec3d::Zero(), voldata.get_instance_rotation(), + voldata.get_instance_scaling_factor().cwiseInverse(), voldata.get_instance_mirror()) * normal).normalized(); + // Additional rotation to align tnormal with the down vector in the world coordinate space. + auto extra_rotation = Eigen::Quaterniond().setFromTwoVectors(tnormal, -Vec3d::UnitZ()); + v.set_instance_rotation(Geometry::extract_euler_angles(extra_rotation.toRotationMatrix() * m_cache.volumes_data[i].get_instance_rotation_matrix())); +#endif // ENABLE_WORLD_COORDINATE + } + +#if !DISABLE_INSTANCES_SYNCH + // Apply the same transformation also to other instances, + // but respect their possibly diffrent z-rotation. + if (m_mode == Instance) + synchronize_unselected_instances(SyncRotationType::GENERAL); +#endif // !DISABLE_INSTANCES_SYNCH + + this->set_bounding_boxes_dirty(); +} + +#if ENABLE_WORLD_COORDINATE +void Selection::scale(const Vec3d& scale, TransformationType transformation_type) +{ + scale_and_translate(scale, Vec3d::Zero(), transformation_type); +} +#else +void Selection::scale(const Vec3d& scale, TransformationType transformation_type) +{ + if (!m_valid) + return; + + for (unsigned int i : m_list) { + GLVolume &v = *(*m_volumes)[i]; + if (is_single_full_instance()) { + if (transformation_type.relative()) { + const Transform3d m = Geometry::assemble_transform(Vec3d::Zero(), Vec3d::Zero(), scale); + const Eigen::Matrix new_matrix = (m * m_cache.volumes_data[i].get_instance_scale_matrix()).matrix().block(0, 0, 3, 3); + // extracts scaling factors from the composed transformation + const Vec3d new_scale(new_matrix.col(0).norm(), new_matrix.col(1).norm(), new_matrix.col(2).norm()); + if (transformation_type.joint()) + v.set_instance_offset(m_cache.dragging_center + m * (m_cache.volumes_data[i].get_instance_position() - m_cache.dragging_center)); + + v.set_instance_scaling_factor(new_scale); + } + else { + if (transformation_type.world() && (std::abs(scale.x() - scale.y()) > EPSILON || std::abs(scale.x() - scale.z()) > EPSILON)) { + // Non-uniform scaling. Transform the scaling factors into the local coordinate system. + // This is only possible, if the instance rotation is mulitples of ninety degrees. + assert(Geometry::is_rotation_ninety_degrees(v.get_instance_rotation())); + v.set_instance_scaling_factor((v.get_instance_transformation().get_matrix(true, false, true, true).matrix().block<3, 3>(0, 0).transpose() * scale).cwiseAbs()); + } + else + v.set_instance_scaling_factor(scale); + } + } + else if (is_single_volume() || is_single_modifier()) + v.set_volume_scaling_factor(scale); + else { + const Transform3d m = Geometry::assemble_transform(Vec3d::Zero(), Vec3d::Zero(), scale); + if (m_mode == Instance) { + const Eigen::Matrix new_matrix = (m * m_cache.volumes_data[i].get_instance_scale_matrix()).matrix().block(0, 0, 3, 3); + // extracts scaling factors from the composed transformation + const Vec3d new_scale(new_matrix.col(0).norm(), new_matrix.col(1).norm(), new_matrix.col(2).norm()); + if (transformation_type.joint()) + v.set_instance_offset(m_cache.dragging_center + m * (m_cache.volumes_data[i].get_instance_position() - m_cache.dragging_center)); + + v.set_instance_scaling_factor(new_scale); + } + else if (m_mode == Volume) { + const Eigen::Matrix new_matrix = (m * m_cache.volumes_data[i].get_volume_scale_matrix()).matrix().block(0, 0, 3, 3); + // extracts scaling factors from the composed transformation + const Vec3d new_scale(new_matrix.col(0).norm(), new_matrix.col(1).norm(), new_matrix.col(2).norm()); + if (transformation_type.joint()) { + const Vec3d offset = m * (m_cache.volumes_data[i].get_volume_position() + m_cache.volumes_data[i].get_instance_position() - m_cache.dragging_center); + v.set_volume_offset(m_cache.dragging_center - m_cache.volumes_data[i].get_instance_position() + offset); + } + v.set_volume_scaling_factor(new_scale); + } + } + } + +#if !DISABLE_INSTANCES_SYNCH + if (m_mode == Instance) + synchronize_unselected_instances(SyncRotationType::NONE); + else if (m_mode == Volume) + synchronize_unselected_volumes(); +#endif // !DISABLE_INSTANCES_SYNCH + + ensure_on_bed(); + set_bounding_boxes_dirty(); + wxGetApp().plater()->canvas3D()->requires_check_outside_state(); +} +#endif // ENABLE_WORLD_COORDINATE + +void Selection::scale_to_fit_print_volume(const BuildVolume& volume) +{ + auto fit = [this](double s, Vec3d offset) { + if (s <= 0.0 || s == 1.0) + return; + + wxGetApp().plater()->take_snapshot(_L("Scale To Fit")); + + TransformationType type; + type.set_world(); + type.set_relative(); + type.set_joint(); + + // apply scale + setup_cache(); + scale(s * Vec3d::Ones(), type); + wxGetApp().plater()->canvas3D()->do_scale(""); // avoid storing another snapshot + + // center selection on print bed + setup_cache(); + offset.z() = -get_bounding_box().min.z(); +#if ENABLE_WORLD_COORDINATE + TransformationType trafo_type; + trafo_type.set_relative(); + translate(offset, trafo_type); +#else + translate(offset); +#endif // ENABLE_WORLD_COORDINATE + wxGetApp().plater()->canvas3D()->do_move(""); // avoid storing another snapshot + + wxGetApp().obj_manipul()->set_dirty(); + }; + + auto fit_rectangle = [this, fit](const BuildVolume& volume) { + const BoundingBoxf3 print_volume = volume.bounding_volume(); + const Vec3d print_volume_size = print_volume.size(); + + // adds 1/100th of a mm on all sides to avoid false out of print volume detections due to floating-point roundings + const Vec3d box_size = get_bounding_box().size() + 0.02 * Vec3d::Ones(); + + const double sx = (box_size.x() != 0.0) ? print_volume_size.x() / box_size.x() : 0.0; + const double sy = (box_size.y() != 0.0) ? print_volume_size.y() / box_size.y() : 0.0; + const double sz = (box_size.z() != 0.0) ? print_volume_size.z() / box_size.z() : 0.0; + + if (sx != 0.0 && sy != 0.0 && sz != 0.0) + fit(std::min(sx, std::min(sy, sz)), print_volume.center() - get_bounding_box().center()); + }; + + auto fit_circle = [this, fit](const BuildVolume& volume) { + const Geometry::Circled& print_circle = volume.circle(); + double print_circle_radius = unscale(print_circle.radius); + + if (print_circle_radius == 0.0) + return; + + Points points; + double max_z = 0.0; + for (unsigned int i : m_list) { + const GLVolume& v = *(*m_volumes)[i]; + TriangleMesh hull_3d = *v.convex_hull(); + hull_3d.transform(v.world_matrix()); + max_z = std::max(max_z, hull_3d.bounding_box().size().z()); + const Polygon hull_2d = hull_3d.convex_hull(); + points.insert(points.end(), hull_2d.begin(), hull_2d.end()); + } + + if (points.empty()) + return; + + const Geometry::Circled circle = Geometry::smallest_enclosing_circle_welzl(points); + // adds 1/100th of a mm on all sides to avoid false out of print volume detections due to floating-point roundings + const double circle_radius = unscale(circle.radius) + 0.01; + + if (circle_radius == 0.0 || max_z == 0.0) + return; + + const double s = std::min(print_circle_radius / circle_radius, volume.max_print_height() / max_z); + const Vec3d sel_center = get_bounding_box().center(); + const Vec3d offset = s * (Vec3d(unscale(circle.center.x()), unscale(circle.center.y()), 0.5 * max_z) - sel_center); + const Vec3d print_center = { unscale(print_circle.center.x()), unscale(print_circle.center.y()), 0.5 * volume.max_print_height() }; + fit(s, print_center - (sel_center + offset)); + }; + + if (is_empty() || m_mode == Volume) + return; + + switch (volume.type()) + { + case BuildVolume::Type::Rectangle: { fit_rectangle(volume); break; } + case BuildVolume::Type::Circle: { fit_circle(volume); break; } + default: { break; } + } +} + +void Selection::mirror(Axis axis) +{ + if (!m_valid) + return; + + for (unsigned int i : m_list) { + GLVolume& v = *(*m_volumes)[i]; + if (is_single_full_instance()) + v.set_instance_mirror(axis, -v.get_instance_mirror(axis)); + else if (m_mode == Volume) + v.set_volume_mirror(axis, -v.get_volume_mirror(axis)); + } + +#if !DISABLE_INSTANCES_SYNCH + if (m_mode == Instance) + synchronize_unselected_instances(SyncRotationType::NONE); + else if (m_mode == Volume) + synchronize_unselected_volumes(); +#endif // !DISABLE_INSTANCES_SYNCH + + set_bounding_boxes_dirty(); +} + +#if ENABLE_WORLD_COORDINATE +void Selection::scale_and_translate(const Vec3d& scale, const Vec3d& translation, TransformationType transformation_type) +{ + if (!m_valid) + return; + + Vec3d relative_scale = scale; + + for (unsigned int i : m_list) { + GLVolume& v = *(*m_volumes)[i]; + const VolumeCache& volume_data = m_cache.volumes_data[i]; + const Geometry::Transformation& inst_trafo = volume_data.get_instance_transform(); + + if (transformation_type.absolute()) { + // convert from absolute scaling to relative scaling + BoundingBoxf3 original_box; + if (m_mode == Instance) { + assert(is_from_fully_selected_instance(i)); + if (transformation_type.world()) + original_box = get_full_unscaled_instance_bounding_box(); + else + original_box = get_full_unscaled_instance_local_bounding_box(); + } + else { + if (transformation_type.world()) + original_box = v.transformed_convex_hull_bounding_box((volume_data.get_instance_transform() * + volume_data.get_volume_transform()).get_matrix_no_scaling_factor()); + else if (transformation_type.instance()) + original_box = v.transformed_convex_hull_bounding_box(volume_data.get_volume_transform().get_matrix_no_scaling_factor()); + else + original_box = v.bounding_box(); + } + + relative_scale = original_box.size().cwiseProduct(scale).cwiseQuotient(m_box.get_bounding_box().size()); + } + + if (m_mode == Instance) { + assert(is_from_fully_selected_instance(i)); + if (transformation_type.world()) { + const Transform3d scale_matrix = Geometry::scale_transform(relative_scale); + const Transform3d offset_matrix = (transformation_type.joint() && translation.isApprox(Vec3d::Zero())) ? + // non-constrained scaling - add offset to scale around selection center + Geometry::translation_transform(m_cache.dragging_center + scale_matrix * (inst_trafo.get_offset() - m_cache.dragging_center)) : + // constrained scaling - add offset to keep constraint + Geometry::translation_transform(translation) * inst_trafo.get_offset_matrix(); + v.set_instance_transformation(offset_matrix * scale_matrix * inst_trafo.get_matrix_no_offset()); + } + else if (transformation_type.local()) { + const Transform3d scale_matrix = Geometry::scale_transform(relative_scale); + Vec3d offset; + if (transformation_type.joint() && translation.isApprox(Vec3d::Zero())) { + // non-constrained scaling - add offset to scale around selection center + offset = inst_trafo.get_matrix_no_offset().inverse() * (inst_trafo.get_offset() - m_cache.dragging_center); + offset = inst_trafo.get_matrix_no_offset() * (scale_matrix * offset - offset); + } + else + // constrained scaling - add offset to keep constraint + offset = translation; + + v.set_instance_transformation(Geometry::translation_transform(offset) * inst_trafo.get_matrix() * scale_matrix); + } + else + assert(false); + } + else + transform_volume_relative(v, volume_data, transformation_type, Geometry::translation_transform(translation) * Geometry::scale_transform(relative_scale)); + } + +#if !DISABLE_INSTANCES_SYNCH + if (m_mode == Instance) + synchronize_unselected_instances(SyncRotationType::NONE); + else if (m_mode == Volume) + synchronize_unselected_volumes(); +#endif // !DISABLE_INSTANCES_SYNCH + + ensure_on_bed(); + set_bounding_boxes_dirty(); + wxGetApp().plater()->canvas3D()->requires_check_outside_state(); +} + +void Selection::reset_skew() +{ + if (!m_valid) + return; + + for (unsigned int i : m_list) { + GLVolume& v = *(*m_volumes)[i]; + const VolumeCache& volume_data = m_cache.volumes_data[i]; + Geometry::Transformation inst_trafo = volume_data.get_instance_transform(); + Geometry::Transformation vol_trafo = volume_data.get_volume_transform(); + Geometry::Transformation world_trafo = inst_trafo * vol_trafo; + if (world_trafo.has_skew()) { + if (!inst_trafo.has_skew() && !vol_trafo.has_skew()) { + // = [I][V] + world_trafo.reset_offset(); + world_trafo.reset_skew(); + v.set_volume_transformation(vol_trafo.get_offset_matrix() * inst_trafo.get_matrix_no_offset().inverse() * world_trafo.get_matrix()); + } + else { + // = + // = [V] + // = [I] + if (inst_trafo.has_skew()) { + inst_trafo.reset_skew(); + v.set_instance_transformation(inst_trafo); + } + if (vol_trafo.has_skew()) { + vol_trafo.reset_skew(); + v.set_volume_transformation(vol_trafo); + } + } + } + else { + // [W] = [I][V] + // [W] = + if (inst_trafo.has_skew()) { + inst_trafo.reset_skew(); + v.set_instance_transformation(inst_trafo); + } + if (vol_trafo.has_skew()) { + vol_trafo.reset_skew(); + v.set_volume_transformation(vol_trafo); + } + } + } + + ensure_on_bed(); + set_bounding_boxes_dirty(); + wxGetApp().plater()->canvas3D()->requires_check_outside_state(); +} +#else +void Selection::translate(unsigned int object_idx, const Vec3d& displacement) +{ + if (!m_valid) + return; + + for (unsigned int i : m_list) { + GLVolume& v = *(*m_volumes)[i]; + if (v.object_idx() == (int)object_idx) + v.set_instance_offset(v.get_instance_offset() + displacement); + } + + std::set done; // prevent processing volumes twice + done.insert(m_list.begin(), m_list.end()); + + for (unsigned int i : m_list) { + if (done.size() == m_volumes->size()) + break; + +#if ENABLE_WIPETOWER_OBJECTID_1000_REMOVAL + if ((*m_volumes)[i]->is_wipe_tower) + continue; + + int object_idx = (*m_volumes)[i]->object_idx(); +#else + int object_idx = (*m_volumes)[i]->object_idx(); + if (object_idx >= 1000) + continue; +#endif // ENABLE_WIPETOWER_OBJECTID_1000_REMOVAL + + // Process unselected volumes of the object. + for (unsigned int j = 0; j < (unsigned int)m_volumes->size(); ++j) { + if (done.size() == m_volumes->size()) + break; + + if (done.find(j) != done.end()) + continue; + + GLVolume& v = *(*m_volumes)[j]; + if (v.object_idx() != object_idx) + continue; + + v.set_instance_offset(v.get_instance_offset() + displacement); + done.insert(j); + } + } + + this->set_bounding_boxes_dirty(); +} +#endif // ENABLE_WORLD_COORDINATE + +void Selection::translate(unsigned int object_idx, unsigned int instance_idx, const Vec3d& displacement) +{ + if (!m_valid) + return; + + for (unsigned int i : m_list) { + GLVolume& v = *(*m_volumes)[i]; + if (v.object_idx() == (int)object_idx && v.instance_idx() == (int)instance_idx) +#if ENABLE_WORLD_COORDINATE + v.set_instance_transformation(Geometry::translation_transform(displacement) * v.get_instance_transformation().get_matrix()); +#else + v.set_instance_offset(v.get_instance_offset() + displacement); +#endif // ENABLE_WORLD_COORDINATE + } + + std::set done; // prevent processing volumes twice + done.insert(m_list.begin(), m_list.end()); + + for (unsigned int i : m_list) { + if (done.size() == m_volumes->size()) + break; + +#if ENABLE_WIPETOWER_OBJECTID_1000_REMOVAL + if ((*m_volumes)[i]->is_wipe_tower) + continue; + + int object_idx = (*m_volumes)[i]->object_idx(); +#else + int object_idx = (*m_volumes)[i]->object_idx(); + if (object_idx >= 1000) + continue; +#endif // ENABLE_WIPETOWER_OBJECTID_1000_REMOVAL + + // Process unselected volumes of the object. + for (unsigned int j = 0; j < (unsigned int)m_volumes->size(); ++j) { + if (done.size() == m_volumes->size()) + break; + + if (done.find(j) != done.end()) + continue; + + GLVolume& v = *(*m_volumes)[j]; + if (v.object_idx() != object_idx || v.instance_idx() != (int)instance_idx) + continue; + +#if ENABLE_WORLD_COORDINATE + v.set_instance_transformation(Geometry::translation_transform(displacement) * v.get_instance_transformation().get_matrix()); +#else + v.set_instance_offset(v.get_instance_offset() + displacement); +#endif // ENABLE_WORLD_COORDINATE + done.insert(j); + } + } + + this->set_bounding_boxes_dirty(); +} + +#if ENABLE_WORLD_COORDINATE +int Selection::bake_transform_if_needed() const +{ + if ((is_single_full_instance() && wxGetApp().obj_manipul()->is_world_coordinates()) || + (is_single_volume_or_modifier() && !wxGetApp().obj_manipul()->is_local_coordinates())) { + // Verify whether the instance rotation is multiples of 90 degrees, so that the scaling in world coordinates is possible. + // all volumes in the selection belongs to the same instance, any of them contains the needed instance data, so we take the first one + const GLVolume& volume = *get_first_volume(); + bool needs_baking = false; + if (is_single_full_instance()) { + // Is the instance angle close to a multiple of 90 degrees? + needs_baking |= !Geometry::is_rotation_ninety_degrees(volume.get_instance_rotation()); + // Are all volumes angles close to a multiple of 90 degrees? + for (unsigned int id : get_volume_idxs()) { + if (needs_baking) + break; + needs_baking |= !Geometry::is_rotation_ninety_degrees(get_volume(id)->get_volume_rotation()); + } + } + else if (is_single_volume_or_modifier()) { + // is the volume angle close to a multiple of 90 degrees? + needs_baking |= !Geometry::is_rotation_ninety_degrees(volume.get_volume_rotation()); + if (wxGetApp().obj_manipul()->is_world_coordinates()) + // Is the instance angle close to a multiple of 90 degrees? + needs_baking |= !Geometry::is_rotation_ninety_degrees(volume.get_instance_rotation()); + } + + if (needs_baking) { + MessageDialog dlg((wxWindow*)wxGetApp().mainframe, + _L("The currently manipulated object is tilted or contains tilted parts (rotation angles are not multiples of 90°). " + "Non-uniform scaling of tilted objects is only possible in non-local coordinate systems, " + "once the rotation is embedded into the object coordinates.") + "\n" + + _L("This operation is irreversible.") + "\n" + + _L("Do you want to proceed?"), + SLIC3R_APP_NAME, + wxYES_NO | wxNO_DEFAULT | wxICON_QUESTION); + if (dlg.ShowModal() != wxID_YES) + return -1; + + wxGetApp().plater()->take_snapshot(_("Bake transform")); + + // Bake the rotation into the meshes of the object. + wxGetApp().model().objects[volume.composite_id.object_id]->bake_xy_rotation_into_meshes(volume.composite_id.instance_id); + // Update the 3D scene, selections etc. + wxGetApp().plater()->update(); + return 0; + } + } + + return 1; +} +#endif // ENABLE_WORLD_COORDINATE + +void Selection::erase() +{ + if (!m_valid) + return; + + if (is_single_full_object()) + wxGetApp().obj_list()->delete_from_model_and_list(ItemType::itObject, get_object_idx(), 0); + else if (is_multiple_full_object()) { + std::vector items; + items.reserve(m_cache.content.size()); + for (ObjectIdxsToInstanceIdxsMap::iterator it = m_cache.content.begin(); it != m_cache.content.end(); ++it) { + items.emplace_back(ItemType::itObject, it->first, 0); + } + wxGetApp().obj_list()->delete_from_model_and_list(items); + } + else if (is_multiple_full_instance()) { + std::set> instances_idxs; + for (ObjectIdxsToInstanceIdxsMap::iterator obj_it = m_cache.content.begin(); obj_it != m_cache.content.end(); ++obj_it) { + for (InstanceIdxsList::reverse_iterator inst_it = obj_it->second.rbegin(); inst_it != obj_it->second.rend(); ++inst_it) { + instances_idxs.insert(std::make_pair(obj_it->first, *inst_it)); + } + } + + std::vector items; + items.reserve(instances_idxs.size()); + for (const std::pair& i : instances_idxs) { + items.emplace_back(ItemType::itInstance, i.first, i.second); + } + wxGetApp().obj_list()->delete_from_model_and_list(items); + } + else if (is_single_full_instance()) + wxGetApp().obj_list()->delete_from_model_and_list(ItemType::itInstance, get_object_idx(), get_instance_idx()); + else if (is_mixed()) { + std::set items_set; + std::map volumes_in_obj; + + for (auto i : m_list) { + const auto gl_vol = (*m_volumes)[i]; + const auto glv_obj_idx = gl_vol->object_idx(); + const auto model_object = m_model->objects[glv_obj_idx]; + + if (model_object->instances.size() == 1) { + if (model_object->volumes.size() == 1) + items_set.insert(ItemForDelete(ItemType::itObject, glv_obj_idx, -1)); + else { + items_set.insert(ItemForDelete(ItemType::itVolume, glv_obj_idx, gl_vol->volume_idx())); + int idx = (volumes_in_obj.find(glv_obj_idx) == volumes_in_obj.end()) ? 0 : volumes_in_obj.at(glv_obj_idx); + volumes_in_obj[glv_obj_idx] = ++idx; + } + continue; + } + + const auto glv_ins_idx = gl_vol->instance_idx(); + + for (auto obj_ins : m_cache.content) { + if (obj_ins.first == glv_obj_idx) { + if (obj_ins.second.find(glv_ins_idx) != obj_ins.second.end()) { + if (obj_ins.second.size() == model_object->instances.size()) + items_set.insert(ItemForDelete(ItemType::itObject, glv_obj_idx, -1)); + else + items_set.insert(ItemForDelete(ItemType::itInstance, glv_obj_idx, glv_ins_idx)); + + break; + } + } + } + } + + std::vector items; + items.reserve(items_set.size()); + for (const ItemForDelete& i : items_set) { + if (i.type == ItemType::itVolume) { + const int vol_in_obj_cnt = volumes_in_obj.find(i.obj_idx) == volumes_in_obj.end() ? 0 : volumes_in_obj.at(i.obj_idx); + if (vol_in_obj_cnt == (int)m_model->objects[i.obj_idx]->volumes.size()) { + if (i.sub_obj_idx == vol_in_obj_cnt - 1) + items.emplace_back(ItemType::itObject, i.obj_idx, 0); + continue; + } + } + items.emplace_back(i.type, i.obj_idx, i.sub_obj_idx); + } + + wxGetApp().obj_list()->delete_from_model_and_list(items); + } + else { + std::set> volumes_idxs; + for (unsigned int i : m_list) { + const GLVolume* v = (*m_volumes)[i]; + // Only remove volumes associated with ModelVolumes from the object list. + // Temporary meshes (SLA supports or pads) are not managed by the object list. + if (v->volume_idx() >= 0) + volumes_idxs.insert(std::make_pair(v->object_idx(), v->volume_idx())); + } + + std::vector items; + items.reserve(volumes_idxs.size()); + for (const std::pair& v : volumes_idxs) { + items.emplace_back(ItemType::itVolume, v.first, v.second); + } + + wxGetApp().obj_list()->delete_from_model_and_list(items); + ensure_not_below_bed(); + } +} + +void Selection::render(float scale_factor) +{ + if (!m_valid || is_empty()) + return; + + m_scale_factor = scale_factor; + // render cumulative bounding box of selected volumes +#if ENABLE_LEGACY_OPENGL_REMOVAL +#if ENABLE_WORLD_COORDINATE + BoundingBoxf3 box; + Transform3d trafo; + const ECoordinatesType coordinates_type = wxGetApp().obj_manipul()->get_coordinates_type(); + if (coordinates_type == ECoordinatesType::World) { + box = get_bounding_box(); + trafo = Transform3d::Identity(); + } + else if (coordinates_type == ECoordinatesType::Local && is_single_volume_or_modifier()) { + const GLVolume& v = *get_first_volume(); + box = v.transformed_convex_hull_bounding_box(v.get_volume_transformation().get_scaling_factor_matrix()); + trafo = v.get_instance_transformation().get_matrix() * v.get_volume_transformation().get_matrix_no_scaling_factor(); + } + else { + const Selection::IndicesList& ids = get_volume_idxs(); + for (unsigned int id : ids) { + const GLVolume& v = *get_volume(id); + box.merge(v.transformed_convex_hull_bounding_box(v.get_volume_transformation().get_matrix())); + } + const Geometry::Transformation inst_trafo = get_first_volume()->get_instance_transformation(); + box = box.transformed(inst_trafo.get_scaling_factor_matrix()); + trafo = inst_trafo.get_matrix_no_scaling_factor(); + } + + render_bounding_box(box, trafo, ColorRGB::WHITE()); +#else + render_bounding_box(get_bounding_box(), ColorRGB::WHITE()); +#endif // ENABLE_WORLD_COORDINATE +#else + render_selected_volumes(); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + render_synchronized_volumes(); +} + +#if ENABLE_RENDER_SELECTION_CENTER +void Selection::render_center(bool gizmo_is_dragging) +{ + if (!m_valid || is_empty()) + return; + +#if ENABLE_LEGACY_OPENGL_REMOVAL + GLShaderProgram* shader = wxGetApp().get_shader("flat"); + if (shader == nullptr) + return; + + shader->start_using(); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + + const Vec3d center = gizmo_is_dragging ? m_cache.dragging_center : get_bounding_box().center(); + + glsafe(::glDisable(GL_DEPTH_TEST)); + +#if ENABLE_GL_SHADERS_ATTRIBUTES + const Camera& camera = wxGetApp().plater()->get_camera(); + Transform3d view_model_matrix = camera.get_view_matrix() * Geometry::assemble_transform(center); + + shader->set_uniform("view_model_matrix", view_model_matrix); + shader->set_uniform("projection_matrix", camera.get_projection_matrix()); +#else + glsafe(::glPushMatrix()); + glsafe(::glTranslated(center.x(), center.y(), center.z())); +#endif // ENABLE_GL_SHADERS_ATTRIBUTES + +#if ENABLE_LEGACY_OPENGL_REMOVAL + m_vbo_sphere.set_color(ColorRGBA::WHITE()); +#else + m_vbo_sphere.set_color(-1, ColorRGBA::WHITE()); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + m_vbo_sphere.render(); + +#if !ENABLE_GL_SHADERS_ATTRIBUTES + glsafe(::glPopMatrix()); +#endif // !ENABLE_GL_SHADERS_ATTRIBUTES + +#if ENABLE_LEGACY_OPENGL_REMOVAL + shader->stop_using(); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL +} +#endif // ENABLE_RENDER_SELECTION_CENTER + +void Selection::render_sidebar_hints(const std::string& sidebar_field) +{ + if (sidebar_field.empty()) + return; + +#if ENABLE_LEGACY_OPENGL_REMOVAL + GLShaderProgram* shader = wxGetApp().get_shader(boost::starts_with(sidebar_field, "layer") ? "flat" : "gouraud_light"); + if (shader == nullptr) + return; + + shader->start_using(); +#else + GLShaderProgram* shader = nullptr; + + if (!boost::starts_with(sidebar_field, "layer")) { + shader = wxGetApp().get_shader("gouraud_light"); + if (shader == nullptr) + return; + + shader->start_using(); + glsafe(::glClear(GL_DEPTH_BUFFER_BIT)); + } +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + + glsafe(::glEnable(GL_DEPTH_TEST)); + +#if ENABLE_GL_SHADERS_ATTRIBUTES + const Transform3d base_matrix = Geometry::assemble_transform(get_bounding_box().center()); + Transform3d orient_matrix = Transform3d::Identity(); +#else + glsafe(::glPushMatrix()); +#endif // ENABLE_GL_SHADERS_ATTRIBUTES + +#if ENABLE_WORLD_COORDINATE + const Vec3d center = get_bounding_box().center(); + Vec3d axes_center = center; +#endif // ENABLE_WORLD_COORDINATE + + if (!boost::starts_with(sidebar_field, "layer")) { +#if ENABLE_GL_SHADERS_ATTRIBUTES + shader->set_uniform("emission_factor", 0.05f); +#endif // ENABLE_GL_SHADERS_ATTRIBUTES +#if !ENABLE_GL_SHADERS_ATTRIBUTES && !ENABLE_WORLD_COORDINATE + const Vec3d& center = get_bounding_box().center(); +#endif // !ENABLE_GL_SHADERS_ATTRIBUTES && !ENABLE_WORLD_COORDINATE +#if ENABLE_WORLD_COORDINATE + if (is_single_full_instance() && !wxGetApp().obj_manipul()->is_world_coordinates()) { +#else + if (is_single_full_instance() && !wxGetApp().obj_manipul()->get_world_coordinates()) { +#endif // ENABLE_WORLD_COORDINATE +#if !ENABLE_GL_SHADERS_ATTRIBUTES && !ENABLE_WORLD_COORDINATE + glsafe(::glTranslated(center.x(), center.y(), center.z())); +#endif // !ENABLE_GL_SHADERS_ATTRIBUTES && !ENABLE_WORLD_COORDINATE +#if ENABLE_WORLD_COORDINATE + orient_matrix = (*m_volumes)[*m_list.begin()]->get_instance_transformation().get_rotation_matrix(); + axes_center = (*m_volumes)[*m_list.begin()]->get_instance_offset(); +#else + if (!boost::starts_with(sidebar_field, "position")) { +#if !ENABLE_GL_SHADERS_ATTRIBUTES + Transform3d orient_matrix = Transform3d::Identity(); +#endif // !ENABLE_GL_SHADERS_ATTRIBUTES + if (boost::starts_with(sidebar_field, "scale")) + orient_matrix = (*m_volumes)[*m_list.begin()]->get_instance_transformation().get_matrix(true, false, true, true); + else if (boost::starts_with(sidebar_field, "rotation")) { + if (boost::ends_with(sidebar_field, "x")) + orient_matrix = (*m_volumes)[*m_list.begin()]->get_instance_transformation().get_matrix(true, false, true, true); + else if (boost::ends_with(sidebar_field, "y")) { + const Vec3d& rotation = (*m_volumes)[*m_list.begin()]->get_instance_transformation().get_rotation(); + if (rotation.x() == 0.0) + orient_matrix = (*m_volumes)[*m_list.begin()]->get_instance_transformation().get_matrix(true, false, true, true); + else + orient_matrix.rotate(Eigen::AngleAxisd(rotation.z(), Vec3d::UnitZ())); + } + } +#if !ENABLE_GL_SHADERS_ATTRIBUTES + glsafe(::glMultMatrixd(orient_matrix.data())); +#endif // !ENABLE_GL_SHADERS_ATTRIBUTES + } +#endif // ENABLE_WORLD_COORDINATE + } +#if ENABLE_WORLD_COORDINATE + else if (is_single_volume_or_modifier()) { +#else + else if (is_single_volume() || is_single_modifier()) { +#endif // ENABLE_WORLD_COORDINATE +#if !ENABLE_GL_SHADERS_ATTRIBUTES && !ENABLE_WORLD_COORDINATE + glsafe(::glTranslated(center.x(), center.y(), center.z())); +#endif // !ENABLE_GL_SHADERS_ATTRIBUTES && !ENABLE_WORLD_COORDINATE +#if ENABLE_WORLD_COORDINATE + if (!wxGetApp().obj_manipul()->is_world_coordinates()) { + if (wxGetApp().obj_manipul()->is_local_coordinates()) { + const GLVolume* v = (*m_volumes)[*m_list.begin()]; + orient_matrix = v->get_instance_transformation().get_rotation_matrix() * v->get_volume_transformation().get_rotation_matrix(); + axes_center = (*m_volumes)[*m_list.begin()]->world_matrix().translation(); + } + else { + orient_matrix = (*m_volumes)[*m_list.begin()]->get_instance_transformation().get_rotation_matrix(); + axes_center = (*m_volumes)[*m_list.begin()]->get_instance_offset(); + } + } +#else +#if ENABLE_GL_SHADERS_ATTRIBUTES + orient_matrix = (*m_volumes)[*m_list.begin()]->get_instance_transformation().get_matrix(true, false, true, true); +#else + Transform3d orient_matrix = (*m_volumes)[*m_list.begin()]->get_instance_transformation().get_matrix(true, false, true, true); +#endif // ENABLE_GL_SHADERS_ATTRIBUTES + if (!boost::starts_with(sidebar_field, "position")) + orient_matrix = orient_matrix * (*m_volumes)[*m_list.begin()]->get_volume_transformation().get_matrix(true, false, true, true); +#if !ENABLE_GL_SHADERS_ATTRIBUTES + glsafe(::glMultMatrixd(orient_matrix.data())); +#endif // !ENABLE_GL_SHADERS_ATTRIBUTES +#endif // ENABLE_WORLD_COORDINATE + } + else { +#if ENABLE_GL_SHADERS_ATTRIBUTES || ENABLE_WORLD_COORDINATE + if (requires_local_axes()) +#if ENABLE_WORLD_COORDINATE + orient_matrix = (*m_volumes)[*m_list.begin()]->get_instance_transformation().get_rotation_matrix(); +#else + orient_matrix = (*m_volumes)[*m_list.begin()]->get_instance_transformation().get_matrix(true, false, true, true); +#endif // ENABLE_WORLD_COORDINATE +#else + glsafe(::glTranslated(center.x(), center.y(), center.z())); + if (requires_local_axes()) { + const Transform3d orient_matrix = (*m_volumes)[*m_list.begin()]->get_instance_transformation().get_matrix(true, false, true, true); + glsafe(::glMultMatrixd(orient_matrix.data())); + } +#endif // ENABLE_GL_SHADERS_ATTRIBUTES || ENABLE_WORLD_COORDINATE + } + } + +#if ENABLE_LEGACY_OPENGL_REMOVAL + if (!boost::starts_with(sidebar_field, "layer")) + glsafe(::glClear(GL_DEPTH_BUFFER_BIT)); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + +#if ENABLE_WORLD_COORDINATE + if (!boost::starts_with(sidebar_field, "layer")) { + shader->set_uniform("emission_factor", 0.1f); +#if !ENABLE_GL_SHADERS_ATTRIBUTES + glsafe(::glPushMatrix()); + glsafe(::glTranslated(center.x(), center.y(), center.z())); + glsafe(::glMultMatrixd(orient_matrix.data())); +#endif // !ENABLE_GL_SHADERS_ATTRIBUTES + } +#endif // ENABLE_WORLD_COORDINATE + +#if ENABLE_GL_SHADERS_ATTRIBUTES + if (boost::starts_with(sidebar_field, "position")) + render_sidebar_position_hints(sidebar_field, *shader, base_matrix * orient_matrix); + else if (boost::starts_with(sidebar_field, "rotation")) + render_sidebar_rotation_hints(sidebar_field, *shader, base_matrix * orient_matrix); + else if (boost::starts_with(sidebar_field, "scale") || boost::starts_with(sidebar_field, "size")) + render_sidebar_scale_hints(sidebar_field, *shader, base_matrix * orient_matrix); + else if (boost::starts_with(sidebar_field, "layer")) + render_sidebar_layers_hints(sidebar_field, *shader); + +#if ENABLE_WORLD_COORDINATE + if (!boost::starts_with(sidebar_field, "layer")) { + if (!wxGetApp().obj_manipul()->is_world_coordinates()) + m_axes.render(Geometry::assemble_transform(axes_center) * orient_matrix, 0.25f); + } +#endif // ENABLE_WORLD_COORDINATE +#else + if (boost::starts_with(sidebar_field, "position")) + render_sidebar_position_hints(sidebar_field); + else if (boost::starts_with(sidebar_field, "rotation")) + render_sidebar_rotation_hints(sidebar_field); + else if (boost::starts_with(sidebar_field, "scale") || boost::starts_with(sidebar_field, "size")) + render_sidebar_scale_hints(sidebar_field); + else if (boost::starts_with(sidebar_field, "layer")) + render_sidebar_layers_hints(sidebar_field); + +#if ENABLE_WORLD_COORDINATE + if (!boost::starts_with(sidebar_field, "layer")) { + glsafe(::glPopMatrix()); + glsafe(::glPushMatrix()); + glsafe(::glTranslated(axes_center.x(), axes_center.y(), axes_center.z())); + glsafe(::glMultMatrixd(orient_matrix.data())); + if (!wxGetApp().obj_manipul()->is_world_coordinates()) + m_axes.render(0.25f); + glsafe(::glPopMatrix()); + } +#endif // ENABLE_WORLD_COORDINATE +#endif // ENABLE_GL_SHADERS_ATTRIBUTES + +#if ENABLE_WORLD_COORDINATE +#if !ENABLE_GL_SHADERS_ATTRIBUTES + glsafe(::glPopMatrix()); +#endif // !ENABLE_GL_SHADERS_ATTRIBUTES +#endif // ENABLE_WORLD_COORDINATE + +#if !ENABLE_LEGACY_OPENGL_REMOVAL + if (!boost::starts_with(sidebar_field, "layer")) +#endif // !ENABLE_LEGACY_OPENGL_REMOVAL + shader->stop_using(); +} + +bool Selection::requires_local_axes() const +{ + return m_mode == Volume && is_from_single_instance(); +} + +void Selection::copy_to_clipboard() +{ + if (!m_valid) + return; + + m_clipboard.reset(); + + for (const ObjectIdxsToInstanceIdxsMap::value_type& object : m_cache.content) { + ModelObject* src_object = m_model->objects[object.first]; + ModelObject* dst_object = m_clipboard.add_object(); + dst_object->name = src_object->name; + dst_object->input_file = src_object->input_file; + dst_object->config.assign_config(src_object->config); + dst_object->sla_support_points = src_object->sla_support_points; + dst_object->sla_points_status = src_object->sla_points_status; + dst_object->sla_drain_holes = src_object->sla_drain_holes; + dst_object->layer_config_ranges = src_object->layer_config_ranges; // #ys_FIXME_experiment + dst_object->layer_height_profile.assign(src_object->layer_height_profile); + dst_object->origin_translation = src_object->origin_translation; + + for (int i : object.second) { + dst_object->add_instance(*src_object->instances[i]); + } + + for (unsigned int i : m_list) { + // Copy the ModelVolumes only for the selected GLVolumes of the 1st selected instance. + const GLVolume* volume = (*m_volumes)[i]; + if (volume->object_idx() == object.first && volume->instance_idx() == *object.second.begin()) { + int volume_idx = volume->volume_idx(); + if (0 <= volume_idx && volume_idx < (int)src_object->volumes.size()) { + ModelVolume* src_volume = src_object->volumes[volume_idx]; + ModelVolume* dst_volume = dst_object->add_volume(*src_volume); + dst_volume->set_new_unique_id(); + } + else + assert(false); + } + } + } + + m_clipboard.set_mode(m_mode); +} + +void Selection::paste_from_clipboard() +{ + if (!m_valid || m_clipboard.is_empty()) + return; + + switch (m_clipboard.get_mode()) + { + case Volume: + { + if (is_from_single_instance()) + paste_volumes_from_clipboard(); + + break; + } + case Instance: + { + if (m_mode == Instance) + paste_objects_from_clipboard(); + + break; + } + } +} + +std::vector Selection::get_volume_idxs_from_object(unsigned int object_idx) const +{ + std::vector idxs; + + for (unsigned int i = 0; i < (unsigned int)m_volumes->size(); ++i) { + if ((*m_volumes)[i]->object_idx() == (int)object_idx) + idxs.push_back(i); + } + + return idxs; +} + +std::vector Selection::get_volume_idxs_from_instance(unsigned int object_idx, unsigned int instance_idx) const +{ + std::vector idxs; + + for (unsigned int i = 0; i < (unsigned int)m_volumes->size(); ++i) { + const GLVolume* v = (*m_volumes)[i]; + if (v->object_idx() == (int)object_idx && v->instance_idx() == (int)instance_idx) + idxs.push_back(i); + } + + return idxs; +} + +std::vector Selection::get_volume_idxs_from_volume(unsigned int object_idx, unsigned int instance_idx, unsigned int volume_idx) const +{ + std::vector idxs; + + for (unsigned int i = 0; i < (unsigned int)m_volumes->size(); ++i) + { + const GLVolume* v = (*m_volumes)[i]; + if (v->object_idx() == (int)object_idx && v->volume_idx() == (int)volume_idx) { + if ((int)instance_idx != -1 && v->instance_idx() == (int)instance_idx) + idxs.push_back(i); + } + } + + return idxs; +} + +std::vector Selection::get_missing_volume_idxs_from(const std::vector& volume_idxs) const +{ + std::vector idxs; + + for (unsigned int i : m_list) { + std::vector::const_iterator it = std::find(volume_idxs.begin(), volume_idxs.end(), i); + if (it == volume_idxs.end()) + idxs.push_back(i); + } + + return idxs; +} + +std::vector Selection::get_unselected_volume_idxs_from(const std::vector& volume_idxs) const +{ + std::vector idxs; + + for (unsigned int i : volume_idxs) { + if (m_list.find(i) == m_list.end()) + idxs.push_back(i); + } + + return idxs; +} + +void Selection::update_valid() +{ + m_valid = (m_volumes != nullptr) && (m_model != nullptr); +} + +void Selection::update_type() +{ + m_cache.content.clear(); + m_type = Mixed; + + for (unsigned int i : m_list) { + const GLVolume* volume = (*m_volumes)[i]; + int obj_idx = volume->object_idx(); + int inst_idx = volume->instance_idx(); + ObjectIdxsToInstanceIdxsMap::iterator obj_it = m_cache.content.find(obj_idx); + if (obj_it == m_cache.content.end()) + obj_it = m_cache.content.insert(ObjectIdxsToInstanceIdxsMap::value_type(obj_idx, InstanceIdxsList())).first; + + obj_it->second.insert(inst_idx); + } + + bool requires_disable = false; + + if (!m_valid) + m_type = Invalid; + else + { + if (m_list.empty()) + m_type = Empty; + else if (m_list.size() == 1) { + const GLVolume* first = (*m_volumes)[*m_list.begin()]; + if (first->is_wipe_tower) + m_type = WipeTower; + else if (first->is_modifier) { + m_type = SingleModifier; + requires_disable = true; + } + else { + const ModelObject* model_object = m_model->objects[first->object_idx()]; + unsigned int volumes_count = (unsigned int)model_object->volumes.size(); + unsigned int instances_count = (unsigned int)model_object->instances.size(); + if (volumes_count * instances_count == 1) { + m_type = SingleFullObject; + // ensures the correct mode is selected + m_mode = Instance; + } + else if (volumes_count == 1) // instances_count > 1 + { + m_type = SingleFullInstance; + // ensures the correct mode is selected + m_mode = Instance; + } + else { + m_type = SingleVolume; + requires_disable = true; + } + } + } + else { + unsigned int sla_volumes_count = 0; + // Note: sla_volumes_count is a count of the selected sla_volumes per object instead of per instance, like a model_volumes_count is + for (unsigned int i : m_list) { + if ((*m_volumes)[i]->volume_idx() < 0) + ++sla_volumes_count; + } + + if (m_cache.content.size() == 1) // single object + { + const ModelObject* model_object = m_model->objects[m_cache.content.begin()->first]; + unsigned int model_volumes_count = (unsigned int)model_object->volumes.size(); + + unsigned int instances_count = (unsigned int)model_object->instances.size(); + unsigned int selected_instances_count = (unsigned int)m_cache.content.begin()->second.size(); + if (model_volumes_count * instances_count + sla_volumes_count == (unsigned int)m_list.size()) { + m_type = SingleFullObject; + // ensures the correct mode is selected + m_mode = Instance; + } + else if (selected_instances_count == 1) { + if (model_volumes_count + sla_volumes_count == (unsigned int)m_list.size()) { + m_type = SingleFullInstance; + // ensures the correct mode is selected + m_mode = Instance; + } + else { + unsigned int modifiers_count = 0; + for (unsigned int i : m_list) { + if ((*m_volumes)[i]->is_modifier) + ++modifiers_count; + } + + if (modifiers_count == 0) + m_type = MultipleVolume; + else if (modifiers_count == (unsigned int)m_list.size()) + m_type = MultipleModifier; + + requires_disable = true; + } + } + else if (selected_instances_count > 1 && selected_instances_count * model_volumes_count + sla_volumes_count == (unsigned int)m_list.size()) { + m_type = MultipleFullInstance; + // ensures the correct mode is selected + m_mode = Instance; + } + } + else { + unsigned int sels_cntr = 0; + for (ObjectIdxsToInstanceIdxsMap::iterator it = m_cache.content.begin(); it != m_cache.content.end(); ++it) { + const ModelObject* model_object = m_model->objects[it->first]; + unsigned int volumes_count = (unsigned int)model_object->volumes.size(); + unsigned int instances_count = (unsigned int)model_object->instances.size(); + sels_cntr += volumes_count * instances_count; + } + if (sels_cntr + sla_volumes_count == (unsigned int)m_list.size()) { + m_type = MultipleFullObject; + // ensures the correct mode is selected + m_mode = Instance; + } + } + } + } + + int object_idx = get_object_idx(); + int instance_idx = get_instance_idx(); + for (GLVolume* v : *m_volumes) { + v->disabled = requires_disable ? (v->object_idx() != object_idx) || (v->instance_idx() != instance_idx) : false; + } + +#if ENABLE_SELECTION_DEBUG_OUTPUT + std::cout << "Selection: "; + std::cout << "mode: "; + switch (m_mode) + { + case Volume: + { + std::cout << "Volume"; + break; + } + case Instance: + { + std::cout << "Instance"; + break; + } + } + + std::cout << " - type: "; + + switch (m_type) + { + case Invalid: + { + std::cout << "Invalid" << std::endl; + break; + } + case Empty: + { + std::cout << "Empty" << std::endl; + break; + } + case WipeTower: + { + std::cout << "WipeTower" << std::endl; + break; + } + case SingleModifier: + { + std::cout << "SingleModifier" << std::endl; + break; + } + case MultipleModifier: + { + std::cout << "MultipleModifier" << std::endl; + break; + } + case SingleVolume: + { + std::cout << "SingleVolume" << std::endl; + break; + } + case MultipleVolume: + { + std::cout << "MultipleVolume" << std::endl; + break; + } + case SingleFullObject: + { + std::cout << "SingleFullObject" << std::endl; + break; + } + case MultipleFullObject: + { + std::cout << "MultipleFullObject" << std::endl; + break; + } + case SingleFullInstance: + { + std::cout << "SingleFullInstance" << std::endl; + break; + } + case MultipleFullInstance: + { + std::cout << "MultipleFullInstance" << std::endl; + break; + } + case Mixed: + { + std::cout << "Mixed" << std::endl; + break; + } + } +#endif // ENABLE_SELECTION_DEBUG_OUTPUT +} + +void Selection::set_caches() +{ + m_cache.volumes_data.clear(); + m_cache.sinking_volumes.clear(); + for (unsigned int i = 0; i < (unsigned int)m_volumes->size(); ++i) { + const GLVolume& v = *(*m_volumes)[i]; + m_cache.volumes_data.emplace(i, VolumeCache(v.get_volume_transformation(), v.get_instance_transformation())); + if (v.is_sinking()) + m_cache.sinking_volumes.push_back(i); + } + m_cache.dragging_center = get_bounding_box().center(); +} + +void Selection::do_add_volume(unsigned int volume_idx) +{ + m_list.insert(volume_idx); + GLVolume* v = (*m_volumes)[volume_idx]; + v->selected = true; + if (v->hover == GLVolume::HS_Select || v->hover == GLVolume::HS_Deselect) + v->hover = GLVolume::HS_Hover; +} + +void Selection::do_add_volumes(const std::vector& volume_idxs) +{ + for (unsigned int i : volume_idxs) + { + if (i < (unsigned int)m_volumes->size()) + do_add_volume(i); + } +} + +void Selection::do_remove_volume(unsigned int volume_idx) +{ + IndicesList::iterator v_it = m_list.find(volume_idx); + if (v_it == m_list.end()) + return; + + m_list.erase(v_it); + + (*m_volumes)[volume_idx]->selected = false; +} + +void Selection::do_remove_instance(unsigned int object_idx, unsigned int instance_idx) +{ + for (unsigned int i = 0; i < (unsigned int)m_volumes->size(); ++i) { + GLVolume* v = (*m_volumes)[i]; + if (v->object_idx() == (int)object_idx && v->instance_idx() == (int)instance_idx) + do_remove_volume(i); + } +} + +void Selection::do_remove_object(unsigned int object_idx) +{ + for (unsigned int i = 0; i < (unsigned int)m_volumes->size(); ++i) { + GLVolume* v = (*m_volumes)[i]; + if (v->object_idx() == (int)object_idx) + do_remove_volume(i); + } +} + +#if !ENABLE_LEGACY_OPENGL_REMOVAL +void Selection::render_selected_volumes() const +{ + float color[3] = { 1.0f, 1.0f, 1.0f }; + render_bounding_box(get_bounding_box(), color); +} +#endif // !ENABLE_LEGACY_OPENGL_REMOVAL + +void Selection::render_synchronized_volumes() +{ + if (m_mode == Instance) + return; + +#if !ENABLE_LEGACY_OPENGL_REMOVAL + float color[3] = { 1.0f, 1.0f, 0.0f }; +#endif // !ENABLE_LEGACY_OPENGL_REMOVAL + +#if ENABLE_WORLD_COORDINATE + const ECoordinatesType coordinates_type = wxGetApp().obj_manipul()->get_coordinates_type(); + BoundingBoxf3 box; + Transform3d trafo; +#endif // ENABLE_WORLD_COORDINATE + + for (unsigned int i : m_list) { + const GLVolume& volume = *(*m_volumes)[i]; + int object_idx = volume.object_idx(); + int volume_idx = volume.volume_idx(); + for (unsigned int j = 0; j < (unsigned int)m_volumes->size(); ++j) { + if (i == j) + continue; + + const GLVolume& v = *(*m_volumes)[j]; + if (v.object_idx() != object_idx || v.volume_idx() != volume_idx) + continue; + +#if ENABLE_LEGACY_OPENGL_REMOVAL +#if ENABLE_WORLD_COORDINATE + if (coordinates_type == ECoordinatesType::World) { + box = v.transformed_convex_hull_bounding_box(); + trafo = Transform3d::Identity(); + } + else if (coordinates_type == ECoordinatesType::Local) { + box = v.bounding_box(); + trafo = v.world_matrix(); + } + else { + box = v.transformed_convex_hull_bounding_box(v.get_volume_transformation().get_matrix()); + trafo = v.get_instance_transformation().get_matrix(); + } + render_bounding_box(box, trafo, ColorRGB::YELLOW()); +#else + render_bounding_box(v.transformed_convex_hull_bounding_box(), ColorRGB::YELLOW()); +#endif // ENABLE_WORLD_COORDINATE +#else + render_bounding_box(v.transformed_convex_hull_bounding_box(), color); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + } + } +} + +#if ENABLE_LEGACY_OPENGL_REMOVAL +#if ENABLE_WORLD_COORDINATE +void Selection::render_bounding_box(const BoundingBoxf3& box, const Transform3d& trafo, const ColorRGB& color) +#else +void Selection::render_bounding_box(const BoundingBoxf3& box, const ColorRGB& color) +#endif // ENABLE_WORLD_COORDINATE +{ +#else +void Selection::render_bounding_box(const BoundingBoxf3 & box, float* color) const +{ + if (color == nullptr) + return; + + const Vec3f b_min = box.min.cast(); + const Vec3f b_max = box.max.cast(); + const Vec3f size = 0.2f * box.size().cast(); + + glsafe(::glEnable(GL_DEPTH_TEST)); + glsafe(::glColor3fv(color)); + glsafe(::glLineWidth(2.0f * m_scale_factor)); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + +#if ENABLE_LEGACY_OPENGL_REMOVAL + const BoundingBoxf3& curr_box = m_box.get_bounding_box(); + + if (!m_box.is_initialized() || !is_approx(box.min, curr_box.min) || !is_approx(box.max, curr_box.max)) { + m_box.reset(); + + const Vec3f b_min = box.min.cast(); + const Vec3f b_max = box.max.cast(); + const Vec3f size = 0.2f * box.size().cast(); + + GLModel::Geometry init_data; + init_data.format = { GLModel::Geometry::EPrimitiveType::Lines, GLModel::Geometry::EVertexLayout::P3 }; + init_data.reserve_vertices(48); + init_data.reserve_indices(48); + + // vertices + init_data.add_vertex(Vec3f(b_min.x(), b_min.y(), b_min.z())); + init_data.add_vertex(Vec3f(b_min.x() + size.x(), b_min.y(), b_min.z())); + init_data.add_vertex(Vec3f(b_min.x(), b_min.y(), b_min.z())); + init_data.add_vertex(Vec3f(b_min.x(), b_min.y() + size.y(), b_min.z())); + init_data.add_vertex(Vec3f(b_min.x(), b_min.y(), b_min.z())); + init_data.add_vertex(Vec3f(b_min.x(), b_min.y(), b_min.z() + size.z())); + + init_data.add_vertex(Vec3f(b_max.x(), b_min.y(), b_min.z())); + init_data.add_vertex(Vec3f(b_max.x() - size.x(), b_min.y(), b_min.z())); + init_data.add_vertex(Vec3f(b_max.x(), b_min.y(), b_min.z())); + init_data.add_vertex(Vec3f(b_max.x(), b_min.y() + size.y(), b_min.z())); + init_data.add_vertex(Vec3f(b_max.x(), b_min.y(), b_min.z())); + init_data.add_vertex(Vec3f(b_max.x(), b_min.y(), b_min.z() + size.z())); + + init_data.add_vertex(Vec3f(b_max.x(), b_max.y(), b_min.z())); + init_data.add_vertex(Vec3f(b_max.x() - size.x(), b_max.y(), b_min.z())); + init_data.add_vertex(Vec3f(b_max.x(), b_max.y(), b_min.z())); + init_data.add_vertex(Vec3f(b_max.x(), b_max.y() - size.y(), b_min.z())); + init_data.add_vertex(Vec3f(b_max.x(), b_max.y(), b_min.z())); + init_data.add_vertex(Vec3f(b_max.x(), b_max.y(), b_min.z() + size.z())); + + init_data.add_vertex(Vec3f(b_min.x(), b_max.y(), b_min.z())); + init_data.add_vertex(Vec3f(b_min.x() + size.x(), b_max.y(), b_min.z())); + init_data.add_vertex(Vec3f(b_min.x(), b_max.y(), b_min.z())); + init_data.add_vertex(Vec3f(b_min.x(), b_max.y() - size.y(), b_min.z())); + init_data.add_vertex(Vec3f(b_min.x(), b_max.y(), b_min.z())); + init_data.add_vertex(Vec3f(b_min.x(), b_max.y(), b_min.z() + size.z())); + + init_data.add_vertex(Vec3f(b_min.x(), b_min.y(), b_max.z())); + init_data.add_vertex(Vec3f(b_min.x() + size.x(), b_min.y(), b_max.z())); + init_data.add_vertex(Vec3f(b_min.x(), b_min.y(), b_max.z())); + init_data.add_vertex(Vec3f(b_min.x(), b_min.y() + size.y(), b_max.z())); + init_data.add_vertex(Vec3f(b_min.x(), b_min.y(), b_max.z())); + init_data.add_vertex(Vec3f(b_min.x(), b_min.y(), b_max.z() - size.z())); + + init_data.add_vertex(Vec3f(b_max.x(), b_min.y(), b_max.z())); + init_data.add_vertex(Vec3f(b_max.x() - size.x(), b_min.y(), b_max.z())); + init_data.add_vertex(Vec3f(b_max.x(), b_min.y(), b_max.z())); + init_data.add_vertex(Vec3f(b_max.x(), b_min.y() + size.y(), b_max.z())); + init_data.add_vertex(Vec3f(b_max.x(), b_min.y(), b_max.z())); + init_data.add_vertex(Vec3f(b_max.x(), b_min.y(), b_max.z() - size.z())); + + init_data.add_vertex(Vec3f(b_max.x(), b_max.y(), b_max.z())); + init_data.add_vertex(Vec3f(b_max.x() - size.x(), b_max.y(), b_max.z())); + init_data.add_vertex(Vec3f(b_max.x(), b_max.y(), b_max.z())); + init_data.add_vertex(Vec3f(b_max.x(), b_max.y() - size.y(), b_max.z())); + init_data.add_vertex(Vec3f(b_max.x(), b_max.y(), b_max.z())); + init_data.add_vertex(Vec3f(b_max.x(), b_max.y(), b_max.z() - size.z())); + + init_data.add_vertex(Vec3f(b_min.x(), b_max.y(), b_max.z())); + init_data.add_vertex(Vec3f(b_min.x() + size.x(), b_max.y(), b_max.z())); + init_data.add_vertex(Vec3f(b_min.x(), b_max.y(), b_max.z())); + init_data.add_vertex(Vec3f(b_min.x(), b_max.y() - size.y(), b_max.z())); + init_data.add_vertex(Vec3f(b_min.x(), b_max.y(), b_max.z())); + init_data.add_vertex(Vec3f(b_min.x(), b_max.y(), b_max.z() - size.z())); + + // indices + for (unsigned int i = 0; i < 48; ++i) { + init_data.add_index(i); + } + + m_box.init_from(std::move(init_data)); + } + + glsafe(::glEnable(GL_DEPTH_TEST)); + + glsafe(::glLineWidth(2.0f * m_scale_factor)); + + GLShaderProgram* shader = wxGetApp().get_shader("flat"); + if (shader == nullptr) + return; + +#if ENABLE_WORLD_COORDINATE +#if !ENABLE_GL_SHADERS_ATTRIBUTES + glsafe(::glPushMatrix()); + glsafe(::glMultMatrixd(trafo.data())); +#endif // !ENABLE_GL_SHADERS_ATTRIBUTES +#endif // ENABLE_WORLD_COORDINATE + + shader->start_using(); +#if ENABLE_GL_SHADERS_ATTRIBUTES + const Camera& camera = wxGetApp().plater()->get_camera(); +#if ENABLE_WORLD_COORDINATE + shader->set_uniform("view_model_matrix", camera.get_view_matrix() * trafo); +#else + shader->set_uniform("view_model_matrix", camera.get_view_matrix()); +#endif // ENABLE_WORLD_COORDINATE + shader->set_uniform("projection_matrix", camera.get_projection_matrix()); +#endif // ENABLE_GL_SHADERS_ATTRIBUTES + m_box.set_color(to_rgba(color)); + m_box.render(); + shader->stop_using(); + +#if ENABLE_WORLD_COORDINATE +#if !ENABLE_GL_SHADERS_ATTRIBUTES + glsafe(::glPopMatrix()); +#endif // !ENABLE_GL_SHADERS_ATTRIBUTES +#endif // ENABLE_WORLD_COORDINATE +#else + ::glBegin(GL_LINES); + + ::glVertex3f(b_min(0), b_min(1), b_min(2)); ::glVertex3f(b_min(0) + size(0), b_min(1), b_min(2)); + ::glVertex3f(b_min(0), b_min(1), b_min(2)); ::glVertex3f(b_min(0), b_min(1) + size(1), b_min(2)); + ::glVertex3f(b_min(0), b_min(1), b_min(2)); ::glVertex3f(b_min(0), b_min(1), b_min(2) + size(2)); + + ::glVertex3f(b_max(0), b_min(1), b_min(2)); ::glVertex3f(b_max(0) - size(0), b_min(1), b_min(2)); + ::glVertex3f(b_max(0), b_min(1), b_min(2)); ::glVertex3f(b_max(0), b_min(1) + size(1), b_min(2)); + ::glVertex3f(b_max(0), b_min(1), b_min(2)); ::glVertex3f(b_max(0), b_min(1), b_min(2) + size(2)); + + ::glVertex3f(b_max(0), b_max(1), b_min(2)); ::glVertex3f(b_max(0) - size(0), b_max(1), b_min(2)); + ::glVertex3f(b_max(0), b_max(1), b_min(2)); ::glVertex3f(b_max(0), b_max(1) - size(1), b_min(2)); + ::glVertex3f(b_max(0), b_max(1), b_min(2)); ::glVertex3f(b_max(0), b_max(1), b_min(2) + size(2)); + + ::glVertex3f(b_min(0), b_max(1), b_min(2)); ::glVertex3f(b_min(0) + size(0), b_max(1), b_min(2)); + ::glVertex3f(b_min(0), b_max(1), b_min(2)); ::glVertex3f(b_min(0), b_max(1) - size(1), b_min(2)); + ::glVertex3f(b_min(0), b_max(1), b_min(2)); ::glVertex3f(b_min(0), b_max(1), b_min(2) + size(2)); + + ::glVertex3f(b_min(0), b_min(1), b_max(2)); ::glVertex3f(b_min(0) + size(0), b_min(1), b_max(2)); + ::glVertex3f(b_min(0), b_min(1), b_max(2)); ::glVertex3f(b_min(0), b_min(1) + size(1), b_max(2)); + ::glVertex3f(b_min(0), b_min(1), b_max(2)); ::glVertex3f(b_min(0), b_min(1), b_max(2) - size(2)); + + ::glVertex3f(b_max(0), b_min(1), b_max(2)); ::glVertex3f(b_max(0) - size(0), b_min(1), b_max(2)); + ::glVertex3f(b_max(0), b_min(1), b_max(2)); ::glVertex3f(b_max(0), b_min(1) + size(1), b_max(2)); + ::glVertex3f(b_max(0), b_min(1), b_max(2)); ::glVertex3f(b_max(0), b_min(1), b_max(2) - size(2)); + + ::glVertex3f(b_max(0), b_max(1), b_max(2)); ::glVertex3f(b_max(0) - size(0), b_max(1), b_max(2)); + ::glVertex3f(b_max(0), b_max(1), b_max(2)); ::glVertex3f(b_max(0), b_max(1) - size(1), b_max(2)); + ::glVertex3f(b_max(0), b_max(1), b_max(2)); ::glVertex3f(b_max(0), b_max(1), b_max(2) - size(2)); + + ::glVertex3f(b_min(0), b_max(1), b_max(2)); ::glVertex3f(b_min(0) + size(0), b_max(1), b_max(2)); + ::glVertex3f(b_min(0), b_max(1), b_max(2)); ::glVertex3f(b_min(0), b_max(1) - size(1), b_max(2)); + ::glVertex3f(b_min(0), b_max(1), b_max(2)); ::glVertex3f(b_min(0), b_max(1), b_max(2) - size(2)); + + glsafe(::glEnd()); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL +} + +static ColorRGBA get_color(Axis axis) +{ + return AXES_COLOR[axis]; +} + +#if ENABLE_GL_SHADERS_ATTRIBUTES +void Selection::render_sidebar_position_hints(const std::string& sidebar_field, GLShaderProgram& shader, const Transform3d& matrix) +#else +void Selection::render_sidebar_position_hints(const std::string& sidebar_field) +#endif // ENABLE_GL_SHADERS_ATTRIBUTES +{ +#if ENABLE_LEGACY_OPENGL_REMOVAL +#if ENABLE_GL_SHADERS_ATTRIBUTES + const Camera& camera = wxGetApp().plater()->get_camera(); + const Transform3d view_matrix = camera.get_view_matrix() * matrix; + shader.set_uniform("projection_matrix", camera.get_projection_matrix()); +#endif // ENABLE_GL_SHADERS_ATTRIBUTES + + if (boost::ends_with(sidebar_field, "x")) { +#if ENABLE_GL_SHADERS_ATTRIBUTES + const Transform3d view_model_matrix = view_matrix * Geometry::assemble_transform(Vec3d::Zero(), -0.5 * PI * Vec3d::UnitZ()); + shader.set_uniform("view_model_matrix", view_model_matrix); + shader.set_uniform("normal_matrix", (Matrix3d)view_model_matrix.matrix().block(0, 0, 3, 3).inverse().transpose()); +#else + glsafe(::glRotated(-90.0, 0.0, 0.0, 1.0)); +#endif // ENABLE_GL_SHADERS_ATTRIBUTES + m_arrow.set_color(get_color(X)); + m_arrow.render(); + } + else if (boost::ends_with(sidebar_field, "y")) { +#if ENABLE_GL_SHADERS_ATTRIBUTES + shader.set_uniform("view_model_matrix", view_matrix); + shader.set_uniform("normal_matrix", (Matrix3d)view_matrix.matrix().block(0, 0, 3, 3).inverse().transpose()); +#endif // ENABLE_GL_SHADERS_ATTRIBUTES + m_arrow.set_color(get_color(Y)); + m_arrow.render(); + } + else if (boost::ends_with(sidebar_field, "z")) { +#if ENABLE_GL_SHADERS_ATTRIBUTES + const Transform3d view_model_matrix = view_matrix * Geometry::assemble_transform(Vec3d::Zero(), 0.5 * PI * Vec3d::UnitX()); + shader.set_uniform("view_model_matrix", view_model_matrix); + shader.set_uniform("normal_matrix", (Matrix3d)view_model_matrix.matrix().block(0, 0, 3, 3).inverse().transpose()); +#else + glsafe(::glRotated(90.0, 1.0, 0.0, 0.0)); +#endif // ENABLE_GL_SHADERS_ATTRIBUTES + m_arrow.set_color(get_color(Z)); + m_arrow.render(); + } +#else + if (boost::ends_with(sidebar_field, "x")) { + glsafe(::glRotated(-90.0, 0.0, 0.0, 1.0)); + m_arrow.set_color(-1, get_color(X)); + m_arrow.render(); + } + else if (boost::ends_with(sidebar_field, "y")) { + m_arrow.set_color(-1, get_color(Y)); + m_arrow.render(); + } + else if (boost::ends_with(sidebar_field, "z")) { + glsafe(::glRotated(90.0, 1.0, 0.0, 0.0)); + m_arrow.set_color(-1, get_color(Z)); + m_arrow.render(); + } +#endif // ENABLE_LEGACY_OPENGL_REMOVAL +} + +#if ENABLE_GL_SHADERS_ATTRIBUTES +void Selection::render_sidebar_rotation_hints(const std::string& sidebar_field, GLShaderProgram& shader, const Transform3d& matrix) +#else +void Selection::render_sidebar_rotation_hints(const std::string& sidebar_field) +#endif // ENABLE_GL_SHADERS_ATTRIBUTES +{ +#if ENABLE_LEGACY_OPENGL_REMOVAL +#if ENABLE_GL_SHADERS_ATTRIBUTES + auto render_sidebar_rotation_hint = [this](GLShaderProgram& shader, const Transform3d& matrix) { + Transform3d view_model_matrix = matrix; + shader.set_uniform("view_model_matrix", view_model_matrix); + shader.set_uniform("normal_matrix", (Matrix3d)view_model_matrix.matrix().block(0, 0, 3, 3).inverse().transpose()); + m_curved_arrow.render(); + view_model_matrix = matrix * Geometry::assemble_transform(Vec3d::Zero(), PI * Vec3d::UnitZ()); + shader.set_uniform("view_model_matrix", view_model_matrix); + shader.set_uniform("normal_matrix", (Matrix3d)view_model_matrix.matrix().block(0, 0, 3, 3).inverse().transpose()); + m_curved_arrow.render(); + }; + + const Camera& camera = wxGetApp().plater()->get_camera(); + const Transform3d view_matrix = camera.get_view_matrix() * matrix; + shader.set_uniform("projection_matrix", camera.get_projection_matrix()); +#else + auto render_sidebar_rotation_hint = [this]() { + m_curved_arrow.render(); + glsafe(::glRotated(180.0, 0.0, 0.0, 1.0)); + m_curved_arrow.render(); + }; +#endif // ENABLE_GL_SHADERS_ATTRIBUTES + + if (boost::ends_with(sidebar_field, "x")) { +#if !ENABLE_GL_SHADERS_ATTRIBUTES + glsafe(::glRotated(90.0, 0.0, 1.0, 0.0)); +#endif // !ENABLE_GL_SHADERS_ATTRIBUTES + m_curved_arrow.set_color(get_color(X)); +#if ENABLE_GL_SHADERS_ATTRIBUTES + render_sidebar_rotation_hint(shader, view_matrix * Geometry::assemble_transform(Vec3d::Zero(), 0.5 * PI * Vec3d::UnitY())); +#else + render_sidebar_rotation_hint(); +#endif // ENABLE_GL_SHADERS_ATTRIBUTES + } + else if (boost::ends_with(sidebar_field, "y")) { +#if !ENABLE_GL_SHADERS_ATTRIBUTES + glsafe(::glRotated(-90.0, 1.0, 0.0, 0.0)); +#endif // !ENABLE_GL_SHADERS_ATTRIBUTES + m_curved_arrow.set_color(get_color(Y)); +#if ENABLE_GL_SHADERS_ATTRIBUTES + render_sidebar_rotation_hint(shader, view_matrix * Geometry::assemble_transform(Vec3d::Zero(), -0.5 * PI * Vec3d::UnitX())); +#else + render_sidebar_rotation_hint(); +#endif // ENABLE_GL_SHADERS_ATTRIBUTES + } + else if (boost::ends_with(sidebar_field, "z")) { + m_curved_arrow.set_color(get_color(Z)); +#if ENABLE_GL_SHADERS_ATTRIBUTES + render_sidebar_rotation_hint(shader, view_matrix); +#else + render_sidebar_rotation_hint(); +#endif // ENABLE_GL_SHADERS_ATTRIBUTES + } +#else + auto render_sidebar_rotation_hint = [this]() { + m_curved_arrow.render(); + glsafe(::glRotated(180.0, 0.0, 0.0, 1.0)); + m_curved_arrow.render(); + }; + + if (boost::ends_with(sidebar_field, "x")) { + glsafe(::glRotated(90.0, 0.0, 1.0, 0.0)); + m_curved_arrow.set_color(-1, get_color(X)); + render_sidebar_rotation_hint(); + } + else if (boost::ends_with(sidebar_field, "y")) { + glsafe(::glRotated(-90.0, 1.0, 0.0, 0.0)); + m_curved_arrow.set_color(-1, get_color(Y)); + render_sidebar_rotation_hint(); + } + else if (boost::ends_with(sidebar_field, "z")) { + m_curved_arrow.set_color(-1, get_color(Z)); + render_sidebar_rotation_hint(); + } +#endif // ENABLE_LEGACY_OPENGL_REMOVAL +} + +#if ENABLE_GL_SHADERS_ATTRIBUTES +void Selection::render_sidebar_scale_hints(const std::string& sidebar_field, GLShaderProgram& shader, const Transform3d& matrix) +#else +void Selection::render_sidebar_scale_hints(const std::string& sidebar_field) +#endif // ENABLE_GL_SHADERS_ATTRIBUTES +{ +#if ENABLE_WORLD_COORDINATE + const bool uniform_scale = wxGetApp().obj_manipul()->get_uniform_scaling(); +#else + const bool uniform_scale = requires_uniform_scale() || wxGetApp().obj_manipul()->get_uniform_scaling(); +#endif // ENABLE_WORLD_COORDINATE + +#if ENABLE_GL_SHADERS_ATTRIBUTES + auto render_sidebar_scale_hint = [this, uniform_scale](Axis axis, GLShaderProgram& shader, const Transform3d& matrix) { +#else + auto render_sidebar_scale_hint = [this, uniform_scale](Axis axis) { +#endif // ENABLE_GL_SHADERS_ATTRIBUTES +#if ENABLE_LEGACY_OPENGL_REMOVAL + m_arrow.set_color(uniform_scale ? UNIFORM_SCALE_COLOR : get_color(axis)); +#else + m_arrow.set_color(-1, uniform_scale ? UNIFORM_SCALE_COLOR : get_color(axis)); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + +#if ENABLE_GL_SHADERS_ATTRIBUTES + Transform3d view_model_matrix = matrix * Geometry::assemble_transform(5.0 * Vec3d::UnitY()); + shader.set_uniform("view_model_matrix", view_model_matrix); + shader.set_uniform("normal_matrix", (Matrix3d)view_model_matrix.matrix().block(0, 0, 3, 3).inverse().transpose()); +#else + GLShaderProgram* shader = wxGetApp().get_current_shader(); + if (shader != nullptr) + shader->set_uniform("emission_factor", 0.0f); + + glsafe(::glTranslated(0.0, 5.0, 0.0)); +#endif // ENABLE_GL_SHADERS_ATTRIBUTES + m_arrow.render(); + +#if ENABLE_GL_SHADERS_ATTRIBUTES + view_model_matrix = matrix * Geometry::assemble_transform(-5.0 * Vec3d::UnitY(), PI * Vec3d::UnitZ()); + shader.set_uniform("view_model_matrix", view_model_matrix); + shader.set_uniform("normal_matrix", (Matrix3d)view_model_matrix.matrix().block(0, 0, 3, 3).inverse().transpose()); +#else + glsafe(::glTranslated(0.0, -10.0, 0.0)); + glsafe(::glRotated(180.0, 0.0, 0.0, 1.0)); +#endif // ENABLE_GL_SHADERS_ATTRIBUTES + m_arrow.render(); + }; + +#if ENABLE_GL_SHADERS_ATTRIBUTES + const Camera& camera = wxGetApp().plater()->get_camera(); + const Transform3d view_matrix = camera.get_view_matrix() * matrix; + shader.set_uniform("projection_matrix", camera.get_projection_matrix()); +#endif // ENABLE_GL_SHADERS_ATTRIBUTES + + if (boost::ends_with(sidebar_field, "x") || uniform_scale) { +#if ENABLE_GL_SHADERS_ATTRIBUTES + render_sidebar_scale_hint(X, shader, view_matrix * Geometry::assemble_transform(Vec3d::Zero(), -0.5 * PI * Vec3d::UnitZ())); +#else + glsafe(::glPushMatrix()); + glsafe(::glRotated(-90.0, 0.0, 0.0, 1.0)); + render_sidebar_scale_hint(X); + glsafe(::glPopMatrix()); +#endif // ENABLE_GL_SHADERS_ATTRIBUTES + } + + if (boost::ends_with(sidebar_field, "y") || uniform_scale) { +#if ENABLE_GL_SHADERS_ATTRIBUTES + render_sidebar_scale_hint(Y, shader, view_matrix); +#else + glsafe(::glPushMatrix()); + render_sidebar_scale_hint(Y); + glsafe(::glPopMatrix()); +#endif // ENABLE_GL_SHADERS_ATTRIBUTES + } + + if (boost::ends_with(sidebar_field, "z") || uniform_scale) { +#if ENABLE_GL_SHADERS_ATTRIBUTES + render_sidebar_scale_hint(Z, shader, view_matrix * Geometry::assemble_transform(Vec3d::Zero(), 0.5 * PI * Vec3d::UnitX())); +#else + glsafe(::glPushMatrix()); + glsafe(::glRotated(90.0, 1.0, 0.0, 0.0)); + render_sidebar_scale_hint(Z); + glsafe(::glPopMatrix()); +#endif // ENABLE_GL_SHADERS_ATTRIBUTES + } +} + +#if ENABLE_GL_SHADERS_ATTRIBUTES +void Selection::render_sidebar_layers_hints(const std::string& sidebar_field, GLShaderProgram& shader) +#else +void Selection::render_sidebar_layers_hints(const std::string& sidebar_field) +#endif // ENABLE_GL_SHADERS_ATTRIBUTES +{ + static const float Margin = 10.0f; + + std::string field = sidebar_field; + + // extract max_z + std::string::size_type pos = field.rfind("_"); + if (pos == std::string::npos) + return; + + const float max_z = float(string_to_double_decimal_point(field.substr(pos + 1))); + + // extract min_z + field = field.substr(0, pos); + pos = field.rfind("_"); + if (pos == std::string::npos) + return; + + const float min_z = float(string_to_double_decimal_point(field.substr(pos + 1))); + + // extract type + field = field.substr(0, pos); + pos = field.rfind("_"); + if (pos == std::string::npos) + return; + + const int type = std::stoi(field.substr(pos + 1)); + + const BoundingBoxf3& box = get_bounding_box(); + +#if !ENABLE_LEGACY_OPENGL_REMOVAL + const float min_x = float(box.min.x()) - Margin; + const float max_x = float(box.max.x()) + Margin; + const float min_y = float(box.min.y()) - Margin; + const float max_y = float(box.max.y()) + Margin; +#endif // !ENABLE_LEGACY_OPENGL_REMOVAL + + // view dependend order of rendering to keep correct transparency + const bool camera_on_top = wxGetApp().plater()->get_camera().is_looking_downward(); + const float z1 = camera_on_top ? min_z : max_z; + const float z2 = camera_on_top ? max_z : min_z; + +#if ENABLE_LEGACY_OPENGL_REMOVAL + const Vec3f p1 = { float(box.min.x()) - Margin, float(box.min.y()) - Margin, z1 }; + const Vec3f p2 = { float(box.max.x()) + Margin, float(box.max.y()) + Margin, z2 }; +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + + glsafe(::glEnable(GL_DEPTH_TEST)); + glsafe(::glDisable(GL_CULL_FACE)); + glsafe(::glEnable(GL_BLEND)); + glsafe(::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)); + +#if ENABLE_LEGACY_OPENGL_REMOVAL + if (!m_planes.models[0].is_initialized() || !is_approx(m_planes.check_points[0], p1)) { + m_planes.check_points[0] = p1; + m_planes.models[0].reset(); + + GLModel::Geometry init_data; + init_data.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3 }; + init_data.reserve_vertices(4); + init_data.reserve_indices(6); + + // vertices + init_data.add_vertex(Vec3f(p1.x(), p1.y(), z1)); + init_data.add_vertex(Vec3f(p2.x(), p1.y(), z1)); + init_data.add_vertex(Vec3f(p2.x(), p2.y(), z1)); + init_data.add_vertex(Vec3f(p1.x(), p2.y(), z1)); + + // indices + init_data.add_triangle(0, 1, 2); + init_data.add_triangle(2, 3, 0); + + m_planes.models[0].init_from(std::move(init_data)); + } + + if (!m_planes.models[1].is_initialized() || !is_approx(m_planes.check_points[1], p2)) { + m_planes.check_points[1] = p2; + m_planes.models[1].reset(); + + GLModel::Geometry init_data; + init_data.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3 }; + init_data.reserve_vertices(4); + init_data.reserve_indices(6); + + // vertices + init_data.add_vertex(Vec3f(p1.x(), p1.y(), z2)); + init_data.add_vertex(Vec3f(p2.x(), p1.y(), z2)); + init_data.add_vertex(Vec3f(p2.x(), p2.y(), z2)); + init_data.add_vertex(Vec3f(p1.x(), p2.y(), z2)); + + // indices + init_data.add_triangle(0, 1, 2); + init_data.add_triangle(2, 3, 0); + + m_planes.models[1].init_from(std::move(init_data)); + } + +#if ENABLE_GL_SHADERS_ATTRIBUTES + const Camera& camera = wxGetApp().plater()->get_camera(); + shader.set_uniform("view_model_matrix", camera.get_view_matrix()); + shader.set_uniform("projection_matrix", camera.get_projection_matrix()); +#endif // ENABLE_GL_SHADERS_ATTRIBUTES + + m_planes.models[0].set_color((camera_on_top && type == 1) || (!camera_on_top && type == 2) ? SOLID_PLANE_COLOR : TRANSPARENT_PLANE_COLOR); + m_planes.models[0].render(); + m_planes.models[1].set_color((camera_on_top && type == 2) || (!camera_on_top && type == 1) ? SOLID_PLANE_COLOR : TRANSPARENT_PLANE_COLOR); + m_planes.models[1].render(); +#else + ::glBegin(GL_QUADS); + ::glColor4fv((camera_on_top && type == 1) || (!camera_on_top && type == 2) ? SOLID_PLANE_COLOR.data() : TRANSPARENT_PLANE_COLOR.data()); + ::glVertex3f(min_x, min_y, z1); + ::glVertex3f(max_x, min_y, z1); + ::glVertex3f(max_x, max_y, z1); + ::glVertex3f(min_x, max_y, z1); + glsafe(::glEnd()); + + ::glBegin(GL_QUADS); + ::glColor4fv((camera_on_top && type == 2) || (!camera_on_top && type == 1) ? SOLID_PLANE_COLOR.data() : TRANSPARENT_PLANE_COLOR.data()); + ::glVertex3f(min_x, min_y, z2); + ::glVertex3f(max_x, min_y, z2); + ::glVertex3f(max_x, max_y, z2); + ::glVertex3f(min_x, max_y, z2); + glsafe(::glEnd()); +#endif // ENABLE_LEGACY_OPENGL_REMOVAL + + glsafe(::glEnable(GL_CULL_FACE)); + glsafe(::glDisable(GL_BLEND)); +} + +#ifndef NDEBUG +static bool is_rotation_xy_synchronized(const Vec3d &rot_xyz_from, const Vec3d &rot_xyz_to) +{ + const Eigen::AngleAxisd angle_axis(Geometry::rotation_xyz_diff(rot_xyz_from, rot_xyz_to)); + const Vec3d axis = angle_axis.axis(); + const double angle = angle_axis.angle(); + if (std::abs(angle) < 1e-8) + return true; + assert(std::abs(axis.x()) < 1e-8); + assert(std::abs(axis.y()) < 1e-8); + assert(std::abs(std::abs(axis.z()) - 1.) < 1e-8); + return std::abs(axis.x()) < 1e-8 && std::abs(axis.y()) < 1e-8 && std::abs(std::abs(axis.z()) - 1.) < 1e-8; +} + +static void verify_instances_rotation_synchronized(const Model &model, const GLVolumePtrs &volumes) +{ + for (int idx_object = 0; idx_object < int(model.objects.size()); ++idx_object) { + int idx_volume_first = -1; + for (int i = 0; i < (int)volumes.size(); ++i) { + if (volumes[i]->object_idx() == idx_object) { + idx_volume_first = i; + break; + } + } + assert(idx_volume_first != -1); // object without instances? + if (idx_volume_first == -1) + continue; + const Vec3d &rotation0 = volumes[idx_volume_first]->get_instance_rotation(); + for (int i = idx_volume_first + 1; i < (int)volumes.size(); ++i) + if (volumes[i]->object_idx() == idx_object) { + const Vec3d &rotation = volumes[i]->get_instance_rotation(); + assert(is_rotation_xy_synchronized(rotation, rotation0)); + } + } +} +#endif /* NDEBUG */ + +void Selection::synchronize_unselected_instances(SyncRotationType sync_rotation_type) +{ + std::set done; // prevent processing volumes twice + done.insert(m_list.begin(), m_list.end()); + + for (unsigned int i : m_list) { + if (done.size() == m_volumes->size()) + break; + + const GLVolume* volume_i = (*m_volumes)[i]; +#if ENABLE_WIPETOWER_OBJECTID_1000_REMOVAL + if (volume_i->is_wipe_tower) + continue; + + const int object_idx = volume_i->object_idx(); +#else + const int object_idx = volume_i->object_idx(); + if (object_idx >= 1000) + continue; +#endif // ENABLE_WIPETOWER_OBJECTID_1000_REMOVAL + + const int instance_idx = volume_i->instance_idx(); +#if ENABLE_WORLD_COORDINATE + const Geometry::Transformation& curr_inst_trafo_i = volume_i->get_instance_transformation(); + const Vec3d curr_inst_rotation_i = curr_inst_trafo_i.get_rotation(); + const Vec3d& curr_inst_scaling_factor_i = curr_inst_trafo_i.get_scaling_factor(); + const Vec3d& curr_inst_mirror_i = curr_inst_trafo_i.get_mirror(); + const Vec3d old_inst_rotation_i = m_cache.volumes_data[i].get_instance_transform().get_rotation(); +#else + const Vec3d& rotation = volume_i->get_instance_rotation(); + const Vec3d& scaling_factor = volume_i->get_instance_scaling_factor(); + const Vec3d& mirror = volume_i->get_instance_mirror(); +#endif // ENABLE_WORLD_COORDINATE + + // Process unselected instances. + for (unsigned int j = 0; j < (unsigned int)m_volumes->size(); ++j) { + if (done.size() == m_volumes->size()) + break; + + if (done.find(j) != done.end()) + continue; + + GLVolume* volume_j = (*m_volumes)[j]; + if (volume_j->object_idx() != object_idx || volume_j->instance_idx() == instance_idx) + continue; + +#if ENABLE_WORLD_COORDINATE + const Vec3d old_inst_rotation_j = m_cache.volumes_data[j].get_instance_transform().get_rotation(); + assert(is_rotation_xy_synchronized(old_inst_rotation_i, old_inst_rotation_j)); + const Geometry::Transformation& curr_inst_trafo_j = volume_j->get_instance_transformation(); + const Vec3d curr_inst_rotation_j = curr_inst_trafo_j.get_rotation(); + Vec3d new_inst_offset_j = curr_inst_trafo_j.get_offset(); + Vec3d new_inst_rotation_j = curr_inst_rotation_j; +#else + assert(is_rotation_xy_synchronized(m_cache.volumes_data[i].get_instance_rotation(), m_cache.volumes_data[j].get_instance_rotation())); +#endif // ENABLE_WORLD_COORDINATE + + switch (sync_rotation_type) { + case SyncRotationType::NONE: { + // z only rotation -> synch instance z + // The X,Y rotations should be synchronized from start to end of the rotation. +#if ENABLE_WORLD_COORDINATE + assert(is_rotation_xy_synchronized(curr_inst_rotation_i, curr_inst_rotation_j)); + if (wxGetApp().preset_bundle->printers.get_edited_preset().printer_technology() != ptSLA) + new_inst_offset_j.z() = curr_inst_trafo_i.get_offset().z(); +#else + assert(is_rotation_xy_synchronized(rotation, volume_j->get_instance_rotation())); + if (wxGetApp().preset_bundle->printers.get_edited_preset().printer_technology() != ptSLA) + volume_j->set_instance_offset(Z, volume_i->get_instance_offset().z()); +#endif // ENABLE_WORLD_COORDINATE + break; + } + case SyncRotationType::GENERAL: { + // generic rotation -> update instance z with the delta of the rotation. +#if ENABLE_WORLD_COORDINATE + const double z_diff = Geometry::rotation_diff_z(old_inst_rotation_i, old_inst_rotation_j); + new_inst_rotation_j = curr_inst_rotation_i + z_diff * Vec3d::UnitZ(); +#else + const double z_diff = Geometry::rotation_diff_z(m_cache.volumes_data[i].get_instance_rotation(), m_cache.volumes_data[j].get_instance_rotation()); + volume_j->set_instance_rotation({ rotation.x(), rotation.y(), rotation.z() + z_diff }); +#endif // ENABLE_WORLD_COORDINATE + break; + } +#if ENABLE_WORLD_COORDINATE + case SyncRotationType::FULL: { + // generic rotation -> update instance z with the delta of the rotation. + const Eigen::AngleAxisd angle_axis(Geometry::rotation_xyz_diff(curr_inst_rotation_i, old_inst_rotation_j)); + const Vec3d& axis = angle_axis.axis(); + const double z_diff = (std::abs(axis.x()) > EPSILON || std::abs(axis.y()) > EPSILON) ? + angle_axis.angle() * axis.z() : Geometry::rotation_diff_z(curr_inst_rotation_i, old_inst_rotation_j); + + new_inst_rotation_j = curr_inst_rotation_i + z_diff * Vec3d::UnitZ(); + break; + } +#endif // ENABLE_WORLD_COORDINATE + } + +#if ENABLE_WORLD_COORDINATE + volume_j->set_instance_transformation(Geometry::assemble_transform(new_inst_offset_j, new_inst_rotation_j, + curr_inst_scaling_factor_i, curr_inst_mirror_i)); +#else + volume_j->set_instance_scaling_factor(scaling_factor); + volume_j->set_instance_mirror(mirror); +#endif // ENABLE_WORLD_COORDINATE + + done.insert(j); + } + } + +#ifndef NDEBUG + verify_instances_rotation_synchronized(*m_model, *m_volumes); +#endif /* NDEBUG */ +} + +void Selection::synchronize_unselected_volumes() +{ + for (unsigned int i : m_list) { + const GLVolume* volume = (*m_volumes)[i]; +#if ENABLE_WIPETOWER_OBJECTID_1000_REMOVAL + if (volume->is_wipe_tower) + continue; + + const int object_idx = volume->object_idx(); +#else + const int object_idx = volume->object_idx(); + if (object_idx >= 1000) + continue; +#endif // ENABLE_WIPETOWER_OBJECTID_1000_REMOVAL + + const int volume_idx = volume->volume_idx(); +#if ENABLE_WORLD_COORDINATE + const Geometry::Transformation& trafo = volume->get_volume_transformation(); +#else + const Vec3d& offset = volume->get_volume_offset(); + const Vec3d& rotation = volume->get_volume_rotation(); + const Vec3d& scaling_factor = volume->get_volume_scaling_factor(); + const Vec3d& mirror = volume->get_volume_mirror(); +#endif // ENABLE_WORLD_COORDINATE + + // Process unselected volumes. + for (unsigned int j = 0; j < (unsigned int)m_volumes->size(); ++j) { + if (j == i) + continue; + + GLVolume* v = (*m_volumes)[j]; + if (v->object_idx() != object_idx || v->volume_idx() != volume_idx) + continue; + +#if ENABLE_WORLD_COORDINATE + v->set_volume_transformation(trafo); +#else + v->set_volume_offset(offset); + v->set_volume_rotation(rotation); + v->set_volume_scaling_factor(scaling_factor); + v->set_volume_mirror(mirror); +#endif // ENABLE_WORLD_COORDINATE + } + } +} + +void Selection::ensure_on_bed() +{ + typedef std::map, double> InstancesToZMap; + InstancesToZMap instances_min_z; + + for (size_t i = 0; i < m_volumes->size(); ++i) { + GLVolume* volume = (*m_volumes)[i]; + if (!volume->is_wipe_tower && !volume->is_modifier && + std::find(m_cache.sinking_volumes.begin(), m_cache.sinking_volumes.end(), i) == m_cache.sinking_volumes.end()) { + const double min_z = volume->transformed_convex_hull_bounding_box().min.z(); + std::pair instance = std::make_pair(volume->object_idx(), volume->instance_idx()); + InstancesToZMap::iterator it = instances_min_z.find(instance); + if (it == instances_min_z.end()) + it = instances_min_z.insert(InstancesToZMap::value_type(instance, DBL_MAX)).first; + + it->second = std::min(it->second, min_z); + } + } + + for (GLVolume* volume : *m_volumes) { + std::pair instance = std::make_pair(volume->object_idx(), volume->instance_idx()); + InstancesToZMap::iterator it = instances_min_z.find(instance); + if (it != instances_min_z.end()) + volume->set_instance_offset(Z, volume->get_instance_offset(Z) - it->second); + } +} + +void Selection::ensure_not_below_bed() +{ + typedef std::map, double> InstancesToZMap; + InstancesToZMap instances_max_z; + + for (size_t i = 0; i < m_volumes->size(); ++i) { + GLVolume* volume = (*m_volumes)[i]; + if (!volume->is_wipe_tower && !volume->is_modifier) { + const double max_z = volume->transformed_convex_hull_bounding_box().max.z(); + const std::pair instance = std::make_pair(volume->object_idx(), volume->instance_idx()); + InstancesToZMap::iterator it = instances_max_z.find(instance); + if (it == instances_max_z.end()) + it = instances_max_z.insert({ instance, -DBL_MAX }).first; + + it->second = std::max(it->second, max_z); + } + } + + if (is_any_volume()) { + for (unsigned int i : m_list) { + GLVolume& volume = *(*m_volumes)[i]; + const std::pair instance = std::make_pair(volume.object_idx(), volume.instance_idx()); + InstancesToZMap::const_iterator it = instances_max_z.find(instance); + const double z_shift = SINKING_MIN_Z_THRESHOLD - it->second; + if (it != instances_max_z.end() && z_shift > 0.0) + volume.set_volume_offset(Z, volume.get_volume_offset(Z) + z_shift); + } + } + else { + for (GLVolume* volume : *m_volumes) { + const std::pair instance = std::make_pair(volume->object_idx(), volume->instance_idx()); + InstancesToZMap::const_iterator it = instances_max_z.find(instance); + if (it != instances_max_z.end() && it->second < SINKING_MIN_Z_THRESHOLD) + volume->set_instance_offset(Z, volume->get_instance_offset(Z) + SINKING_MIN_Z_THRESHOLD - it->second); + } + } +} + +bool Selection::is_from_fully_selected_instance(unsigned int volume_idx) const +{ + struct SameInstance + { + int obj_idx; + int inst_idx; + GLVolumePtrs& volumes; + + SameInstance(int obj_idx, int inst_idx, GLVolumePtrs& volumes) : obj_idx(obj_idx), inst_idx(inst_idx), volumes(volumes) {} + bool operator () (unsigned int i) { return (volumes[i]->volume_idx() >= 0) && (volumes[i]->object_idx() == obj_idx) && (volumes[i]->instance_idx() == inst_idx); } + }; + + if ((unsigned int)m_volumes->size() <= volume_idx) + return false; + + GLVolume* volume = (*m_volumes)[volume_idx]; + int object_idx = volume->object_idx(); + if ((int)m_model->objects.size() <= object_idx) + return false; + + unsigned int count = (unsigned int)std::count_if(m_list.begin(), m_list.end(), SameInstance(object_idx, volume->instance_idx(), *m_volumes)); + return count == (unsigned int)m_model->objects[object_idx]->volumes.size(); +} + +void Selection::paste_volumes_from_clipboard() +{ +#ifdef _DEBUG + check_model_ids_validity(*m_model); +#endif /* _DEBUG */ + + int dst_obj_idx = get_object_idx(); + if ((dst_obj_idx < 0) || ((int)m_model->objects.size() <= dst_obj_idx)) + return; + + ModelObject* dst_object = m_model->objects[dst_obj_idx]; + + int dst_inst_idx = get_instance_idx(); + if ((dst_inst_idx < 0) || ((int)dst_object->instances.size() <= dst_inst_idx)) + return; + + ModelObject* src_object = m_clipboard.get_object(0); + if (src_object != nullptr) + { + ModelInstance* dst_instance = dst_object->instances[dst_inst_idx]; + BoundingBoxf3 dst_instance_bb = dst_object->instance_bounding_box(dst_inst_idx); +#if ENABLE_WORLD_COORDINATE + Transform3d src_matrix = src_object->instances[0]->get_transformation().get_matrix_no_offset(); + Transform3d dst_matrix = dst_instance->get_transformation().get_matrix_no_offset(); +#else + Transform3d src_matrix = src_object->instances[0]->get_transformation().get_matrix(true); + Transform3d dst_matrix = dst_instance->get_transformation().get_matrix(true); +#endif // ENABLE_WORLD_COORDINATE + bool from_same_object = (src_object->input_file == dst_object->input_file) && src_matrix.isApprox(dst_matrix); + + // used to keep relative position of multivolume selections when pasting from another object + BoundingBoxf3 total_bb; + + ModelVolumePtrs volumes; + for (ModelVolume* src_volume : src_object->volumes) + { + ModelVolume* dst_volume = dst_object->add_volume(*src_volume); + dst_volume->set_new_unique_id(); + if (from_same_object) + { +// // if the volume comes from the same object, apply the offset in world system +// double offset = wxGetApp().plater()->canvas3D()->get_size_proportional_to_max_bed_size(0.05); +// dst_volume->translate(dst_matrix.inverse() * Vec3d(offset, offset, 0.0)); + } + else + { + // if the volume comes from another object, apply the offset as done when adding modifiers + // see ObjectList::load_generic_subobject() + total_bb.merge(dst_volume->mesh().bounding_box().transformed(src_volume->get_matrix())); + } + + volumes.push_back(dst_volume); +#ifdef _DEBUG + check_model_ids_validity(*m_model); +#endif /* _DEBUG */ + } + + // keeps relative position of multivolume selections + if (!from_same_object) + { + for (ModelVolume* v : volumes) + { + v->set_offset((v->get_offset() - total_bb.center()) + dst_matrix.inverse() * (Vec3d(dst_instance_bb.max(0), dst_instance_bb.min(1), dst_instance_bb.min(2)) + 0.5 * total_bb.size() - dst_instance->get_transformation().get_offset())); + } + } + + wxGetApp().obj_list()->paste_volumes_into_list(dst_obj_idx, volumes); + } + +#ifdef _DEBUG + check_model_ids_validity(*m_model); +#endif /* _DEBUG */ +} + +void Selection::paste_objects_from_clipboard() +{ +#ifdef _DEBUG + check_model_ids_validity(*m_model); +#endif /* _DEBUG */ + + std::vector object_idxs; + const ModelObjectPtrs& src_objects = m_clipboard.get_objects(); + for (const ModelObject* src_object : src_objects) + { + ModelObject* dst_object = m_model->add_object(*src_object); + double offset = wxGetApp().plater()->canvas3D()->get_size_proportional_to_max_bed_size(0.05); + Vec3d displacement(offset, offset, 0.0); + for (ModelInstance* inst : dst_object->instances) + { + inst->set_offset(inst->get_offset() + displacement); + } + + object_idxs.push_back(m_model->objects.size() - 1); +#ifdef _DEBUG + check_model_ids_validity(*m_model); +#endif /* _DEBUG */ + } + + wxGetApp().obj_list()->paste_objects_into_list(object_idxs); + +#ifdef _DEBUG + check_model_ids_validity(*m_model); +#endif /* _DEBUG */ +} + +#if ENABLE_WORLD_COORDINATE +void Selection::transform_volume_relative(GLVolume& volume, const VolumeCache& volume_data, TransformationType transformation_type, + const Transform3d& transform) +{ + const Geometry::Transformation& inst_trafo = volume_data.get_instance_transform(); + const Geometry::Transformation& volume_trafo = volume_data.get_volume_transform(); + if (transformation_type.world()) { + const Transform3d inst_matrix_no_offset = inst_trafo.get_matrix_no_offset(); + const Transform3d new_volume_matrix = inst_matrix_no_offset.inverse() * transform * inst_matrix_no_offset; + volume.set_volume_transformation(volume_trafo.get_offset_matrix() * new_volume_matrix * volume_trafo.get_matrix_no_offset()); + } + else if (transformation_type.instance()) + volume.set_volume_transformation(volume_trafo.get_offset_matrix() * transform * volume_trafo.get_matrix_no_offset()); + else if (transformation_type.local()) { + const Geometry::Transformation trafo(transform); + volume.set_volume_transformation(trafo.get_offset_matrix() * volume_trafo.get_matrix() * trafo.get_matrix_no_offset()); + } + else + assert(false); +} +#endif // ENABLE_WORLD_COORDINATE + +} // namespace GUI +} // namespace Slic3r diff --git a/src/slic3r/GUI/Selection.hpp b/src/slic3r/GUI/Selection.hpp index c9e55cf82..0e6922c63 100644 --- a/src/slic3r/GUI/Selection.hpp +++ b/src/slic3r/GUI/Selection.hpp @@ -2,7 +2,12 @@ #define slic3r_GUI_Selection_hpp_ #include "libslic3r/Geometry.hpp" +#if ENABLE_WORLD_COORDINATE +#include "GUI_Geometry.hpp" +#include "CoordAxes.hpp" +#else #include "GLModel.hpp" +#endif // ENABLE_WORLD_COORDINATE #include #include @@ -24,6 +29,7 @@ using ModelObjectPtrs = std::vector; namespace GUI { +#if !ENABLE_WORLD_COORDINATE class TransformationType { public: @@ -76,6 +82,7 @@ private: Enum m_value; }; +#endif // !ENABLE_WORLD_COORDINATE class Selection { @@ -110,16 +117,19 @@ private: private: struct TransformCache { - Vec3d position; - Vec3d rotation; - Vec3d scaling_factor; - Vec3d mirror; - Transform3d rotation_matrix; - Transform3d scale_matrix; - Transform3d mirror_matrix; - Transform3d full_matrix; + Vec3d position{ Vec3d::Zero() }; + Vec3d rotation{ Vec3d::Zero() }; + Vec3d scaling_factor{ Vec3d::Ones() }; + Vec3d mirror{ Vec3d::Ones() }; + Transform3d rotation_matrix{ Transform3d::Identity() }; + Transform3d scale_matrix{ Transform3d::Identity() }; + Transform3d mirror_matrix{ Transform3d::Identity() }; + Transform3d full_matrix{ Transform3d::Identity() }; +#if ENABLE_WORLD_COORDINATE + Geometry::Transformation transform; +#endif // ENABLE_WORLD_COORDINATE - TransformCache(); + TransformCache() = default; explicit TransformCache(const Geometry::Transformation& transform); }; @@ -131,13 +141,18 @@ private: VolumeCache(const Geometry::Transformation& volume_transform, const Geometry::Transformation& instance_transform); const Vec3d& get_volume_position() const { return m_volume.position; } +#if !ENABLE_WORLD_COORDINATE const Vec3d& get_volume_rotation() const { return m_volume.rotation; } const Vec3d& get_volume_scaling_factor() const { return m_volume.scaling_factor; } const Vec3d& get_volume_mirror() const { return m_volume.mirror; } +#endif // !ENABLE_WORLD_COORDINATE const Transform3d& get_volume_rotation_matrix() const { return m_volume.rotation_matrix; } const Transform3d& get_volume_scale_matrix() const { return m_volume.scale_matrix; } const Transform3d& get_volume_mirror_matrix() const { return m_volume.mirror_matrix; } const Transform3d& get_volume_full_matrix() const { return m_volume.full_matrix; } +#if ENABLE_WORLD_COORDINATE + const Geometry::Transformation& get_volume_transform() const { return m_volume.transform; } +#endif // ENABLE_WORLD_COORDINATE const Vec3d& get_instance_position() const { return m_instance.position; } const Vec3d& get_instance_rotation() const { return m_instance.rotation; } @@ -147,6 +162,9 @@ private: const Transform3d& get_instance_scale_matrix() const { return m_instance.scale_matrix; } const Transform3d& get_instance_mirror_matrix() const { return m_instance.mirror_matrix; } const Transform3d& get_instance_full_matrix() const { return m_instance.full_matrix; } +#if ENABLE_WORLD_COORDINATE + const Geometry::Transformation& get_instance_transform() const { return m_instance.transform; } +#endif // ENABLE_WORLD_COORDINATE }; public: @@ -207,15 +225,32 @@ private: Cache m_cache; Clipboard m_clipboard; std::optional m_bounding_box; - // Bounding box of a selection, with no instance scaling applied. This bounding box - // is useful for absolute scaling of tilted objects in world coordinate space. + // Bounding box of a single full instance selection, in world coordinates, with no instance scaling applied. + // This bounding box is useful for absolute scaling of tilted objects in world coordinate space. + // Modifiers are NOT taken in account std::optional m_unscaled_instance_bounding_box; + // Bounding box of a single full instance selection, in world coordinates. + // Modifiers are NOT taken in account std::optional m_scaled_instance_bounding_box; +#if ENABLE_WORLD_COORDINATE + // Bounding box of a single full instance selection, in world coordinates, with no instance scaling applied. + // Modifiers are taken in account + std::optional m_full_unscaled_instance_bounding_box; + // Bounding box of a single full instance selection, in world coordinates. + // Modifiers are taken in account + std::optional m_full_scaled_instance_bounding_box; + // Bounding box of a single full instance selection, in local coordinates, with no instance scaling applied. + // Modifiers are taken in account + std::optional m_full_unscaled_instance_local_bounding_box; +#endif // ENABLE_WORLD_COORDINATE #if ENABLE_RENDER_SELECTION_CENTER GLModel m_vbo_sphere; #endif // ENABLE_RENDER_SELECTION_CENTER +#if ENABLE_WORLD_COORDINATE + CoordAxes m_axes; +#endif // ENABLE_WORLD_COORDINATE GLModel m_arrow; GLModel m_curved_arrow; #if ENABLE_LEGACY_OPENGL_REMOVAL @@ -290,6 +325,9 @@ public: bool is_from_single_object() const; bool is_sla_compliant() const; bool is_instance_mode() const { return m_mode == Instance; } +#if ENABLE_WORLD_COORDINATE + bool is_single_volume_or_modifier() const { return is_single_volume() || is_single_modifier(); } +#endif // ENABLE_WORLD_COORDINATE bool contains_volume(unsigned int volume_idx) const { return m_list.find(volume_idx) != m_list.end(); } // returns true if the selection contains all the given indices @@ -299,7 +337,18 @@ public: // returns true if the selection contains all and only the given indices bool matches(const std::vector& volume_idxs) const; +#if ENABLE_WORLD_COORDINATE + enum class EUniformScaleRequiredReason : unsigned char + { + NotRequired, + InstanceNotAxisAligned_World, + VolumeNotAxisAligned_World, + VolumeNotAxisAligned_Instance, + MultipleSelection, + }; +#else bool requires_uniform_scale() const; +#endif // ENABLE_WORLD_COORDINATE // Returns the the object id if the selection is from a single object, otherwise is -1 int get_object_idx() const; @@ -311,28 +360,60 @@ public: const IndicesList& get_volume_idxs() const { return m_list; } const GLVolume* get_volume(unsigned int volume_idx) const; + const GLVolume* get_first_volume() const { return get_volume(*m_list.begin()); } const ObjectIdxsToInstanceIdxsMap& get_content() const { return m_cache.content; } unsigned int volumes_count() const { return (unsigned int)m_list.size(); } const BoundingBoxf3& get_bounding_box() const; - // Bounding box of a selection, with no instance scaling applied. This bounding box - // is useful for absolute scaling of tilted objects in world coordinate space. + // Bounding box of a single full instance selection, in world coordinates, with no instance scaling applied. + // This bounding box is useful for absolute scaling of tilted objects in world coordinate space. + // Modifiers are NOT taken in account const BoundingBoxf3& get_unscaled_instance_bounding_box() const; + // Bounding box of a single full instance selection, in world coordinates. + // Modifiers are NOT taken in account const BoundingBoxf3& get_scaled_instance_bounding_box() const; +#if ENABLE_WORLD_COORDINATE + // Bounding box of a single full instance selection, in world coordinates, with no instance scaling applied. + // Modifiers are taken in account + const BoundingBoxf3& get_full_unscaled_instance_bounding_box() const; + // Bounding box of a single full instance selection, in world coordinates. + // Modifiers are taken in account + const BoundingBoxf3& get_full_scaled_instance_bounding_box() const; + + // Bounding box of a single full instance selection, in local coordinates, with no instance scaling applied. + // Modifiers are taken in account + const BoundingBoxf3& get_full_unscaled_instance_local_bounding_box() const; +#endif // ENABLE_WORLD_COORDINATE void setup_cache(); +#if ENABLE_WORLD_COORDINATE + void translate(const Vec3d& displacement, TransformationType transformation_type); +#else void translate(const Vec3d& displacement, bool local = false); +#endif // ENABLE_WORLD_COORDINATE void rotate(const Vec3d& rotation, TransformationType transformation_type); void flattening_rotate(const Vec3d& normal); void scale(const Vec3d& scale, TransformationType transformation_type); void scale_to_fit_print_volume(const BuildVolume& volume); void mirror(Axis axis); - +#if ENABLE_WORLD_COORDINATE + void scale_and_translate(const Vec3d& scale, const Vec3d& translation, TransformationType transformation_type); + void reset_skew(); +#else void translate(unsigned int object_idx, const Vec3d& displacement); +#endif // ENABLE_WORLD_COORDINATE void translate(unsigned int object_idx, unsigned int instance_idx, const Vec3d& displacement); +#if ENABLE_WORLD_COORDINATE + // returns: + // -1 if the user refused to proceed with baking when asked + // 0 if the baking was performed + // 1 if no baking was needed + int bake_transform_if_needed() const; +#endif // ENABLE_WORLD_COORDINATE + void erase(); void render(float scale_factor = 1.0); @@ -368,10 +449,23 @@ private: void do_remove_volume(unsigned int volume_idx); void do_remove_instance(unsigned int object_idx, unsigned int instance_idx); void do_remove_object(unsigned int object_idx); +#if ENABLE_WORLD_COORDINATE + void set_bounding_boxes_dirty() { + m_bounding_box.reset(); + m_unscaled_instance_bounding_box.reset(); m_scaled_instance_bounding_box.reset(); + m_full_unscaled_instance_bounding_box.reset(); m_full_scaled_instance_bounding_box.reset(); + m_full_unscaled_instance_local_bounding_box.reset();; + } +#else void set_bounding_boxes_dirty() { m_bounding_box.reset(); m_unscaled_instance_bounding_box.reset(); m_scaled_instance_bounding_box.reset(); } +#endif // ENABLE_WORLD_COORDINATE void render_synchronized_volumes(); #if ENABLE_LEGACY_OPENGL_REMOVAL +#if ENABLE_WORLD_COORDINATE + void render_bounding_box(const BoundingBoxf3& box, const Transform3d& trafo, const ColorRGB& color); +#else void render_bounding_box(const BoundingBoxf3& box, const ColorRGB& color); +#endif // ENABLE_WORLD_COORDINATE #else void render_selected_volumes() const; void render_bounding_box(const BoundingBoxf3& box, float* color) const; @@ -389,11 +483,15 @@ private: #endif // ENABLE_GL_SHADERS_ATTRIBUTES public: - enum SyncRotationType { + enum class SyncRotationType { // Do not synchronize rotation. Either not rotating at all, or rotating by world Z axis. - SYNC_ROTATION_NONE = 0, + NONE = 0, // Synchronize after rotation by an axis not parallel with Z. - SYNC_ROTATION_GENERAL = 1, + GENERAL = 1, +#if ENABLE_WORLD_COORDINATE + // Fully synchronize rotation. + FULL = 2, +#endif // ENABLE_WORLD_COORDINATE }; void synchronize_unselected_instances(SyncRotationType sync_rotation_type); void synchronize_unselected_volumes(); @@ -405,6 +503,11 @@ private: void paste_volumes_from_clipboard(); void paste_objects_from_clipboard(); + +#if ENABLE_WORLD_COORDINATE + void transform_volume_relative(GLVolume& volume, const VolumeCache& volume_data, TransformationType transformation_type, + const Transform3d& transform); +#endif // ENABLE_WORLD_COORDINATE }; } // namespace GUI diff --git a/src/slic3r/GUI/SendSystemInfoDialog.cpp b/src/slic3r/GUI/SendSystemInfoDialog.cpp index 9a97532a7..03a38acbf 100644 --- a/src/slic3r/GUI/SendSystemInfoDialog.cpp +++ b/src/slic3r/GUI/SendSystemInfoDialog.cpp @@ -141,7 +141,7 @@ static bool check_internet_connection_win() { bool internet = true; // return true if COM object creation fails. - if (CoInitializeEx(NULL, COINIT_APARTMENTTHREADED) == S_OK) { + if (SUCCEEDED(CoInitializeEx(NULL, COINIT_APARTMENTTHREADED))) { { CComPtr pNLM; if (pNLM.CoCreateInstance(CLSID_NetworkListManager) == S_OK) { diff --git a/src/slic3r/GUI/SysInfoDialog.cpp b/src/slic3r/GUI/SysInfoDialog.cpp index 06cf2b73d..d4d83dfda 100644 --- a/src/slic3r/GUI/SysInfoDialog.cpp +++ b/src/slic3r/GUI/SysInfoDialog.cpp @@ -7,6 +7,8 @@ #include +#include + #include #include @@ -40,7 +42,12 @@ std::string get_main_info(bool format_as_html) if (!format_as_html) out << b_start << (wxGetApp().is_editor() ? SLIC3R_APP_NAME : GCODEVIEWER_APP_NAME) << b_end << line_end; out << b_start << "Version: " << b_end << SLIC3R_VERSION << line_end; - out << b_start << "Build: " << b_end << (wxGetApp().is_editor() ? SLIC3R_BUILD_ID : GCODEVIEWER_BUILD_ID) << line_end; + + std::string build_id = SLIC3R_BUILD_ID; + if (! wxGetApp().is_editor()) + boost::replace_first(build_id, SLIC3R_APP_NAME, GCODEVIEWER_APP_NAME); + out << b_start << "Build: " << b_end << build_id << line_end; + out << line_end; out << b_start << "Operating System: " << b_end << wxPlatformInfo::Get().GetOperatingSystemFamilyName() << line_end; out << b_start << "System Architecture: " << b_end << wxPlatformInfo::Get().GetArchName() << line_end; @@ -95,8 +102,10 @@ SysInfoDialog::SysInfoDialog() main_sizer->Add(hsizer, 1, wxEXPAND | wxALL, 10); // logo - m_logo_bmp = ScalableBitmap(this, wxGetApp().logo_name(), 192); - m_logo = new wxStaticBitmap(this, wxID_ANY, m_logo_bmp.bmp()); + //m_logo_bmp = ScalableBitmap(this, wxGetApp().logo_name(), 192); + //m_logo = new wxStaticBitmap(this, wxID_ANY, m_logo_bmp.bmp()); + m_logo = new wxStaticBitmap(this, wxID_ANY, *get_bmp_bundle(wxGetApp().logo_name(), 192)); + hsizer->Add(m_logo, 0, wxALIGN_CENTER_VERTICAL); wxBoxSizer* vsizer = new wxBoxSizer(wxVERTICAL); @@ -187,8 +196,8 @@ SysInfoDialog::SysInfoDialog() void SysInfoDialog::on_dpi_changed(const wxRect &suggested_rect) { - m_logo_bmp.msw_rescale(); - m_logo->SetBitmap(m_logo_bmp.bmp()); + //m_logo_bmp.msw_rescale(); + //m_logo->SetBitmap(m_logo_bmp.bmp()); wxFont font = get_default_font(this); const int fs = font.GetPointSize() - 1; diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 96151d8c7..d8d6dbf19 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -155,10 +155,8 @@ void Tab::create_preset_tab() add_scaled_button(panel, &m_btn_edit_ph_printer, "cog"); m_show_incompatible_presets = false; - add_scaled_bitmap(this, m_bmp_show_incompatible_presets, "flag_red"); - add_scaled_bitmap(this, m_bmp_hide_incompatible_presets, "flag_green"); - add_scaled_button(panel, &m_btn_hide_incompatible_presets, m_bmp_hide_incompatible_presets.name()); + add_scaled_button(panel, &m_btn_hide_incompatible_presets, "flag_green"); m_btn_compare_preset->SetToolTip(_L("Compare this preset with some another")); // TRN "Save current Settings" @@ -207,7 +205,7 @@ void Tab::create_preset_tab() #endif m_mode_sizer = new ModeSizer(panel, int (0.5*em_unit(this))); - const float scale_factor = /*wxGetApp().*/em_unit(this)*0.1;// GetContentScaleFactor(); + const float scale_factor = em_unit(this)*0.1;// GetContentScaleFactor(); m_hsizer = new wxBoxSizer(wxHORIZONTAL); sizer->Add(m_hsizer, 0, wxEXPAND | wxBOTTOM, 3); m_hsizer->Add(m_presets_choice, 0, wxLEFT | wxRIGHT | wxTOP | wxALIGN_CENTER_VERTICAL, 3); @@ -254,11 +252,8 @@ void Tab::create_preset_tab() m_treectrl = new wxTreeCtrl(panel, wxID_ANY, wxDefaultPosition, wxSize(20 * m_em_unit, -1), wxTR_NO_BUTTONS | wxTR_HIDE_ROOT | wxTR_SINGLE | wxTR_NO_LINES | wxBORDER_SUNKEN | wxWANTS_CHARS); m_left_sizer->Add(m_treectrl, 1, wxEXPAND); - const int img_sz = int(16 * scale_factor + 0.5f); - m_icons = new wxImageList(img_sz, img_sz, true, 1); - // Index of the last icon inserted into $self->{icons}. + // Index of the last icon inserted into m_treectrl m_icon_count = -1; - m_treectrl->AssignImageList(m_icons); m_treectrl->AddRoot("root"); m_treectrl->SetIndent(0); wxGetApp().UpdateDarkUI(m_treectrl); @@ -321,6 +316,14 @@ void Tab::create_preset_tab() // Initialize the DynamicPrintConfig by default keys/values. build(); + if (!m_scaled_icons_list.empty()) { + // update icons for tree_ctrl + wxVector img_bundles; + for (const ScalableBitmap& bmp : m_scaled_icons_list) + img_bundles.push_back(bmp.bmp()); + m_treectrl->SetImages(img_bundles); + } + // ys_FIXME: Following should not be needed, the function will be called later // (update_mode->update_visibility->rebuild_page_tree). This does not work, during the // second call of rebuild_page_tree m_treectrl->GetFirstVisibleItem(); returns zero @@ -336,7 +339,7 @@ void Tab::add_scaled_button(wxWindow* parent, const wxString& label/* = wxEmptyString*/, long style /*= wxBU_EXACTFIT | wxNO_BORDER*/) { - *btn = new ScalableButton(parent, wxID_ANY, icon_name, label, wxDefaultSize, wxDefaultPosition, style, true); + *btn = new ScalableButton(parent, wxID_ANY, icon_name, label, wxDefaultSize, wxDefaultPosition, style); m_scaled_buttons.push_back(*btn); } @@ -366,7 +369,6 @@ Slic3r::GUI::PageShp Tab::add_options_page(const wxString& title, const std::str if (icon_idx == -1) { // Add a new icon to the icon list. m_scaled_icons_list.push_back(ScalableBitmap(this, icon)); - m_icons->Add(m_scaled_icons_list.back().bmp()); icon_idx = ++m_icon_count; m_icon_index[icon] = icon_idx; } @@ -656,16 +658,6 @@ void TabPrinter::init_options_list() m_options_list.emplace("extruders_count", m_opt_status_value); } -void TabPrinter::msw_rescale() -{ - Tab::msw_rescale(); - - if (m_reset_to_filament_color) - m_reset_to_filament_color->msw_rescale(); - - Layout(); -} - void TabSLAMaterial::init_options_list() { if (!m_options_list.empty()) @@ -772,8 +764,11 @@ void Tab::update_changed_tree_ui() void Tab::update_undo_buttons() { - m_undo_btn-> SetBitmap_(m_is_modified_values ? m_bmp_value_revert: m_bmp_white_bullet); - m_undo_to_sys_btn-> SetBitmap_(m_is_nonsys_values ? *m_bmp_non_system : m_bmp_value_lock); + m_undo_btn-> SetBitmap_(m_is_modified_values ? m_bmp_value_revert.name(): m_bmp_white_bullet.name()); + m_undo_to_sys_btn-> SetBitmap_(m_is_nonsys_values ? m_bmp_non_system->name() : m_bmp_value_lock.name()); + + //m_undo_btn-> SetBitmap_(m_is_modified_values ? m_bmp_value_revert: m_bmp_white_bullet); + //m_undo_to_sys_btn-> SetBitmap_(m_is_nonsys_values ? *m_bmp_non_system : m_bmp_value_lock); m_undo_btn->SetToolTip(m_is_modified_values ? m_ttg_value_revert : m_ttg_white_bullet); m_undo_to_sys_btn->SetToolTip(m_is_nonsys_values ? *m_ttg_non_system : m_ttg_value_lock); @@ -921,31 +916,9 @@ void Tab::msw_rescale() { m_em_unit = em_unit(m_parent); - if (m_mode_sizer) - m_mode_sizer->msw_rescale(); m_presets_choice->msw_rescale(); - m_treectrl->SetMinSize(wxSize(20 * m_em_unit, -1)); - // rescale buttons and cached bitmaps - for (const auto btn : m_scaled_buttons) - btn->msw_rescale(); - for (const auto bmp : m_scaled_bitmaps) - bmp->msw_rescale(); - - if (m_detach_preset_btn) - m_detach_preset_btn->msw_rescale(); - - // rescale icons for tree_ctrl - for (ScalableBitmap& bmp : m_scaled_icons_list) - bmp.msw_rescale(); - // recreate and set new ImageList for tree_ctrl - m_icons->RemoveAll(); - m_icons = new wxImageList(m_scaled_icons_list.front().bmp().GetWidth(), m_scaled_icons_list.front().bmp().GetHeight()); - for (ScalableBitmap& bmp : m_scaled_icons_list) - m_icons->Add(bmp.bmp()); - m_treectrl->AssignImageList(m_icons); - // rescale options_groups if (m_active_page) m_active_page->msw_rescale(); @@ -959,28 +932,28 @@ void Tab::sys_color_changed() // update buttons and cached bitmaps for (const auto btn : m_scaled_buttons) - btn->msw_rescale(); + btn->sys_color_changed(); for (const auto bmp : m_scaled_bitmaps) - bmp->msw_rescale(); + bmp->sys_color_changed(); if (m_detach_preset_btn) - m_detach_preset_btn->msw_rescale(); + m_detach_preset_btn->sys_color_changed(); + + update_show_hide_incompatible_button(); // update icons for tree_ctrl - for (ScalableBitmap& bmp : m_scaled_icons_list) - bmp.msw_rescale(); - // recreate and set new ImageList for tree_ctrl - m_icons->RemoveAll(); - m_icons = new wxImageList(m_scaled_icons_list.front().bmp().GetWidth(), m_scaled_icons_list.front().bmp().GetHeight()); - for (ScalableBitmap& bmp : m_scaled_icons_list) - m_icons->Add(bmp.bmp()); - m_treectrl->AssignImageList(m_icons); + wxVector img_bundles; + for (ScalableBitmap& bmp : m_scaled_icons_list) { + bmp.sys_color_changed(); + img_bundles.push_back(bmp.bmp()); + } + m_treectrl->SetImages(img_bundles); // Colors for ui "decoration" update_label_colours(); #ifdef _WIN32 wxWindowUpdateLocker noUpdates(this); if (m_mode_sizer) - m_mode_sizer->msw_rescale(); + m_mode_sizer->sys_color_changed(); wxGetApp().UpdateDarkUI(this); wxGetApp().UpdateDarkUI(m_treectrl); #endif @@ -991,6 +964,7 @@ void Tab::sys_color_changed() m_active_page->sys_color_changed(); Layout(); + Refresh(); } Field* Tab::get_field(const t_config_option_key& opt_key, int opt_index/* = -1*/) const @@ -1254,7 +1228,7 @@ void Tab::build_preset_description_line(ConfigOptionsGroup* optgroup) auto detach_preset_btn = [this](wxWindow* parent) { m_detach_preset_btn = new ScalableButton(parent, wxID_ANY, "lock_open_sys", _L("Detach from system preset"), - wxDefaultSize, wxDefaultPosition, wxBU_LEFT | wxBU_EXACTFIT, true); + wxDefaultSize, wxDefaultPosition, wxBU_LEFT | wxBU_EXACTFIT); ScalableButton* btn = m_detach_preset_btn; btn->SetFont(Slic3r::GUI::wxGetApp().normal_font()); @@ -1667,14 +1641,14 @@ void TabPrint::build() option.opt.height = 5;//50; optgroup->append_single_option_line(option); - page = add_options_page(L("Notes"), "note.png"); + page = add_options_page(L("Notes"), "note"); optgroup = page->new_optgroup(L("Notes"), 0); option = optgroup->get_option("notes"); option.opt.full_width = true; option.opt.height = 25;//250; optgroup->append_single_option_line(option); - page = add_options_page(L("Dependencies"), "wrench.png"); + page = add_options_page(L("Dependencies"), "wrench"); optgroup = page->new_optgroup(L("Profile dependencies")); create_line_with_widget(optgroup.get(), "compatible_printers", "", [this](wxWindow* parent) { @@ -1914,7 +1888,7 @@ void TabFilament::build() m_presets = &m_preset_bundle->filaments; load_initial_data(); - auto page = add_options_page(L("Filament"), "spool.png"); + auto page = add_options_page(L("Filament"), "spool"); auto optgroup = page->new_optgroup(L("Filament")); optgroup->append_single_option_line("filament_colour"); optgroup->append_single_option_line("filament_diameter"); @@ -2054,7 +2028,7 @@ void TabFilament::build() option.opt.height = gcode_field_height;// 150; optgroup->append_single_option_line(option); - page = add_options_page(L("Notes"), "note.png"); + page = add_options_page(L("Notes"), "note"); optgroup = page->new_optgroup(L("Notes"), 0); optgroup->label_width = 0; option = optgroup->get_option("filament_notes"); @@ -2062,7 +2036,7 @@ void TabFilament::build() option.opt.height = notes_field_height;// 250; optgroup->append_single_option_line(option); - page = add_options_page(L("Dependencies"), "wrench.png"); + page = add_options_page(L("Dependencies"), "wrench"); optgroup = page->new_optgroup(L("Profile dependencies")); create_line_with_widget(optgroup.get(), "compatible_printers", "", [this](wxWindow* parent) { return compatible_widget_create(parent, m_compatible_printers); @@ -2446,14 +2420,14 @@ void TabPrinter::build_fff() option.opt.height = gcode_field_height;//150; optgroup->append_single_option_line(option); - page = add_options_page(L("Notes"), "note.png"); + page = add_options_page(L("Notes"), "note"); optgroup = page->new_optgroup(L("Notes"), 0); option = optgroup->get_option("printer_notes"); option.opt.full_width = true; option.opt.height = notes_field_height;//250; optgroup->append_single_option_line(option); - page = add_options_page(L("Dependencies"), "wrench.png"); + page = add_options_page(L("Dependencies"), "wrench"); optgroup = page->new_optgroup(L("Profile dependencies")); build_preset_description_line(optgroup.get()); @@ -2523,14 +2497,14 @@ void TabPrinter::build_sla() const int notes_field_height = 25; // 250 - page = add_options_page(L("Notes"), "note.png"); + page = add_options_page(L("Notes"), "note"); optgroup = page->new_optgroup(L("Notes"), 0); option = optgroup->get_option("printer_notes"); option.opt.full_width = true; option.opt.height = notes_field_height;//250; optgroup->append_single_option_line(option); - page = add_options_page(L("Dependencies"), "wrench.png"); + page = add_options_page(L("Dependencies"), "wrench"); optgroup = page->new_optgroup(L("Profile dependencies")); build_preset_description_line(optgroup.get()); @@ -2788,7 +2762,7 @@ void TabPrinter::build_unregular_pages(bool from_initial_build/* = false*/) auto reset_to_filament_color = [this, extruder_idx](wxWindow* parent) { m_reset_to_filament_color = new ScalableButton(parent, wxID_ANY, "undo", _L("Reset to Filament Color"), - wxDefaultSize, wxDefaultPosition, wxBU_LEFT | wxBU_EXACTFIT, true); + wxDefaultSize, wxDefaultPosition, wxBU_LEFT | wxBU_EXACTFIT); ScalableButton* btn = m_reset_to_filament_color; btn->SetFont(Slic3r::GUI::wxGetApp().normal_font()); btn->SetSize(btn->GetBestSize()); @@ -3755,8 +3729,7 @@ void Tab::toggle_show_hide_incompatible() void Tab::update_show_hide_incompatible_button() { - m_btn_hide_incompatible_presets->SetBitmap_(m_show_incompatible_presets ? - m_bmp_show_incompatible_presets : m_bmp_hide_incompatible_presets); + m_btn_hide_incompatible_presets->SetBitmap(*get_bmp_bundle(m_show_incompatible_presets ? "flag_red" : "flag_green")); m_btn_hide_incompatible_presets->SetToolTip(m_show_incompatible_presets ? "Both compatible an incompatible presets are shown. Click to hide presets not compatible with the current printer." : "Only compatible presets are shown. Click to show both the presets compatible and not compatible with the current printer."); @@ -3805,7 +3778,7 @@ wxSizer* Tab::compatible_widget_create(wxWindow* parent, PresetDependencies &dep deps.checkbox->SetFont(Slic3r::GUI::wxGetApp().normal_font()); wxGetApp().UpdateDarkUI(deps.checkbox, false, true); deps.btn = new ScalableButton(parent, wxID_ANY, "printer", from_u8((boost::format(" %s %s") % _utf8(L("Set")) % std::string(dots.ToUTF8())).str()), - wxDefaultSize, wxDefaultPosition, wxBU_LEFT | wxBU_EXACTFIT, true); + wxDefaultSize, wxDefaultPosition, wxBU_LEFT | wxBU_EXACTFIT); deps.btn->SetFont(Slic3r::GUI::wxGetApp().normal_font()); deps.btn->SetSize(deps.btn->GetBestSize()); @@ -4114,7 +4087,7 @@ bool SubstitutionManager::is_empty_substitutions() wxSizer* TabPrint::create_manage_substitution_widget(wxWindow* parent) { auto create_btn = [parent](ScalableButton** btn, const wxString& label, const std::string& icon_name) { - *btn = new ScalableButton(parent, wxID_ANY, icon_name, " " + label + " ", wxDefaultSize, wxDefaultPosition, wxBU_LEFT | wxBU_EXACTFIT, true); + *btn = new ScalableButton(parent, wxID_ANY, icon_name, " " + label + " ", wxDefaultSize, wxDefaultPosition, wxBU_LEFT | wxBU_EXACTFIT); (*btn)->SetFont(wxGetApp().normal_font()); (*btn)->SetSize((*btn)->GetBestSize()); }; @@ -4167,7 +4140,7 @@ wxSizer* TabPrint::create_substitutions_widget(wxWindow* parent) wxSizer* TabPrinter::create_bed_shape_widget(wxWindow* parent) { ScalableButton* btn = new ScalableButton(parent, wxID_ANY, "printer", " " + _(L("Set")) + " " + dots, - wxDefaultSize, wxDefaultPosition, wxBU_LEFT | wxBU_EXACTFIT, true); + wxDefaultSize, wxDefaultPosition, wxBU_LEFT | wxBU_EXACTFIT); btn->SetFont(wxGetApp().normal_font()); btn->SetSize(btn->GetBestSize()); @@ -4561,7 +4534,7 @@ void TabSLAMaterial::build() optgroup->append_line(line); - page = add_options_page(L("Notes"), "note.png"); + page = add_options_page(L("Notes"), "note"); optgroup = page->new_optgroup(L("Notes"), 0); optgroup->label_width = 0; Option option = optgroup->get_option("material_notes"); @@ -4569,7 +4542,7 @@ void TabSLAMaterial::build() option.opt.height = 25;//250; optgroup->append_single_option_line(option); - page = add_options_page(L("Dependencies"), "wrench.png"); + page = add_options_page(L("Dependencies"), "wrench"); optgroup = page->new_optgroup(L("Profile dependencies")); create_line_with_widget(optgroup.get(), "compatible_printers", "", [this](wxWindow* parent) { @@ -4590,7 +4563,7 @@ void TabSLAMaterial::build() build_preset_description_line(optgroup.get()); - page = add_options_page(L("Material printing profile"), "note.png"); + page = add_options_page(L("Material printing profile"), "note"); optgroup = page->new_optgroup(L("Material printing profile")); option = optgroup->get_option("material_print_speed"); optgroup->append_single_option_line(option); diff --git a/src/slic3r/GUI/Tab.hpp b/src/slic3r/GUI/Tab.hpp index d6f6e0112..6625f71f6 100644 --- a/src/slic3r/GUI/Tab.hpp +++ b/src/slic3r/GUI/Tab.hpp @@ -174,7 +174,6 @@ protected: wxBoxSizer* m_hsizer; wxBoxSizer* m_left_sizer; wxTreeCtrl* m_treectrl; - wxImageList* m_icons; wxScrolledWindow* m_page_view {nullptr}; wxBoxSizer* m_page_sizer {nullptr}; @@ -203,10 +202,6 @@ protected: ScalableButton* m_undo_to_sys_btn; ScalableButton* m_question_btn; - // Cached bitmaps. - // A "flag" icon to be displayned next to the preset name in the Tab's combo box. - ScalableBitmap m_bmp_show_incompatible_presets; - ScalableBitmap m_bmp_hide_incompatible_presets; // Bitmaps to be shown on the "Revert to system" aka "Lock to system" button next to each input field. ScalableBitmap m_bmp_value_lock; ScalableBitmap m_bmp_value_unlock; @@ -505,7 +500,6 @@ public: void build_unregular_pages(bool from_initial_build = false); void on_preset_loaded() override; void init_options_list() override; - void msw_rescale() override; bool supports_printer_technology(const PrinterTechnology /* tech */) const override { return true; } wxSizer* create_bed_shape_widget(wxWindow* parent); diff --git a/src/slic3r/GUI/UnsavedChangesDialog.cpp b/src/slic3r/GUI/UnsavedChangesDialog.cpp index cab978284..ec5286abe 100644 --- a/src/slic3r/GUI/UnsavedChangesDialog.cpp +++ b/src/slic3r/GUI/UnsavedChangesDialog.cpp @@ -115,29 +115,21 @@ wxIcon ModelNode::get_bitmap(const wxString& color) wxBitmap ModelNode::get_bitmap(const wxString& color) #endif // __linux__ { - /* It's supposed that standard size of an icon is 48px*16px for 100% scaled display. - * So set sizes for solid_colored icons used for filament preset - * and scale them in respect to em_unit value - */ - const double em = em_unit(m_parent_win); - const int icon_width = lround(6.4 * em); - const int icon_height = lround(1.6 * em); - - BitmapCache bmp_cache; - ColorRGB rgb; - decode_color(into_u8(color), rgb); - // there is no need to scale created solid bitmap + wxBitmap bmp = get_solid_bmp_bundle(64, 16, into_u8(color))->GetBitmapFor(m_parent_win); + if (!m_toggle) + bmp = bmp.ConvertToDisabled(); #ifndef __linux__ - return bmp_cache.mksolid(icon_width, icon_height, rgb, true); + return bmp; #else wxIcon icon; - icon.CopyFromBitmap(bmp_cache.mksolid(icon_width, icon_height, rgb, true)); + icon.CopyFromBitmap(bmp); return icon; #endif // __linux__ } // option node ModelNode::ModelNode(ModelNode* parent, const wxString& text, const wxString& old_value, const wxString& new_value) : + m_parent_win(parent->m_parent_win), m_parent(parent), m_old_color(old_value.StartsWith("#") ? old_value : ""), m_new_color(new_value.StartsWith("#") ? new_value : ""), @@ -202,18 +194,22 @@ void ModelNode::UpdateIcons() { // update icons for the colors, if any exists if (!m_old_color.IsEmpty()) - m_old_color_bmp = get_bitmap(m_toggle ? m_old_color : wxString::FromUTF8(grey.c_str())); + m_old_color_bmp = get_bitmap(m_old_color); if (!m_new_color.IsEmpty()) - m_new_color_bmp = get_bitmap(m_toggle ? m_new_color : wxString::FromUTF8(grey.c_str())); + m_new_color_bmp = get_bitmap(m_new_color); // update main icon, if any exists if (m_icon_name.empty()) return; + wxBitmap bmp = get_bmp_bundle(m_icon_name)->GetBitmapFor(m_parent_win); + if (!m_toggle) + bmp = bmp.ConvertToDisabled(); + #ifdef __linux__ - m_icon.CopyFromBitmap(create_scaled_bitmap(m_icon_name, m_parent_win, 16, !m_toggle)); + m_icon.CopyFromBitmap(bmp); #else - m_icon = create_scaled_bitmap(m_icon_name, m_parent_win, 16, !m_toggle); + m_icon = bmp; #endif //__linux__ } @@ -838,7 +834,7 @@ void UnsavedChangesDialog::build(Preset::Type type, PresetCollection* dependent_ auto add_btn = [this, buttons, btn_font, dependent_presets](ScalableButton** btn, int& btn_id, const std::string& icon_name, Action close_act, const wxString& label, bool process_enable = true) { - *btn = new ScalableButton(this, btn_id = NewControlId(), icon_name, label, wxDefaultSize, wxDefaultPosition, wxBORDER_DEFAULT, true, 24); + *btn = new ScalableButton(this, btn_id = NewControlId(), icon_name, label, wxDefaultSize, wxDefaultPosition, wxBORDER_DEFAULT, 24); buttons->Add(*btn, 1, wxLEFT, 5); (*btn)->SetFont(btn_font); @@ -876,7 +872,7 @@ void UnsavedChangesDialog::build(Preset::Type type, PresetCollection* dependent_ if (ActionButtons::SAVE & m_buttons) add_btn(&m_save_btn, m_save_btn_id, "save", Action::Save, _L("Save")); - ScalableButton* cancel_btn = new ScalableButton(this, wxID_CANCEL, "cross", _L("Cancel"), wxDefaultSize, wxDefaultPosition, wxBORDER_DEFAULT, true, 24); + ScalableButton* cancel_btn = new ScalableButton(this, wxID_CANCEL, "cross", _L("Cancel"), wxDefaultSize, wxDefaultPosition, wxBORDER_DEFAULT, 24); buttons->Add(cancel_btn, 1, wxLEFT|wxRIGHT, 5); cancel_btn->SetFont(btn_font); cancel_btn->Bind(wxEVT_BUTTON, [this](wxEvent&) { this->EndModal(wxID_CANCEL); }); @@ -1307,8 +1303,6 @@ void UnsavedChangesDialog::on_dpi_changed(const wxRect& suggested_rect) int em = em_unit(); msw_buttons_rescale(this, em, { wxID_CANCEL, m_save_btn_id, m_move_btn_id, m_continue_btn_id }); - for (auto btn : { m_save_btn, m_transfer_btn, m_discard_btn } ) - if (btn) btn->msw_rescale(); const wxSize& size = wxSize(70 * em, 30 * em); SetMinSize(size); @@ -1322,7 +1316,7 @@ void UnsavedChangesDialog::on_dpi_changed(const wxRect& suggested_rect) void UnsavedChangesDialog::on_sys_color_changed() { for (auto btn : { m_save_btn, m_transfer_btn, m_discard_btn } ) - btn->msw_rescale(); + btn->sys_color_changed(); // msw_rescale updates just icons, so use it m_tree->Rescale(); @@ -1720,10 +1714,16 @@ void DiffPresetDialog::on_dpi_changed(const wxRect&) const wxSize& size = wxSize(80 * em, 30 * em); SetMinSize(size); + auto rescale = [em](PresetComboBox* pcb) { + pcb->msw_rescale(); + wxSize sz = wxSize(35 * em, -1); + pcb->SetMinSize(sz); + pcb->SetSize(sz); + }; + for (auto preset_combos : m_preset_combos) { - preset_combos.presets_left->msw_rescale(); - preset_combos.equal_bmp->msw_rescale(); - preset_combos.presets_right->msw_rescale(); + rescale(preset_combos.presets_left); + rescale(preset_combos.presets_right); } m_tree->Rescale(em); @@ -1741,9 +1741,9 @@ void DiffPresetDialog::on_sys_color_changed() #endif for (auto preset_combos : m_preset_combos) { - preset_combos.presets_left->msw_rescale(); - preset_combos.equal_bmp->msw_rescale(); - preset_combos.presets_right->msw_rescale(); + preset_combos.presets_left->sys_color_changed(); + preset_combos.equal_bmp->sys_color_changed(); + preset_combos.presets_right->sys_color_changed(); } // msw_rescale updates just icons, so use it m_tree->Rescale(); diff --git a/src/slic3r/GUI/wxExtensions.cpp b/src/slic3r/GUI/wxExtensions.cpp index dc177ade3..c397b4b39 100644 --- a/src/slic3r/GUI/wxExtensions.cpp +++ b/src/slic3r/GUI/wxExtensions.cpp @@ -16,6 +16,7 @@ #include "Plater.hpp" #include "../Utils/MacDarkMode.hpp" #include "BitmapComboBox.hpp" +#include "libslic3r/Utils.hpp" #include "OG_CustomCtrl.hpp" #include "libslic3r/Color.hpp" @@ -23,16 +24,15 @@ #ifndef __linux__ // msw_menuitem_bitmaps is used for MSW and OSX static std::map msw_menuitem_bitmaps; -#ifdef __WXMSW__ -void msw_rescale_menu(wxMenu* menu) +void sys_color_changed_menu(wxMenu* menu) { struct update_icons { static void run(wxMenuItem* item) { const auto it = msw_menuitem_bitmaps.find(item->GetId()); if (it != msw_menuitem_bitmaps.end()) { - const wxBitmap& item_icon = create_menu_bitmap(it->second); - if (item_icon.IsOk()) - item->SetBitmap(item_icon); + wxBitmapBundle* item_icon = get_bmp_bundle(it->second); + if (item_icon->IsOk()) + item->SetBitmap(*item_icon); } if (item->IsSubMenu()) for (wxMenuItem *sub_item : item->GetSubMenu()->GetMenuItems()) @@ -43,35 +43,24 @@ void msw_rescale_menu(wxMenu* menu) for (wxMenuItem *item : menu->GetMenuItems()) update_icons::run(item); } -#endif /* __WXMSW__ */ -#endif /* no __WXGTK__ */ +#endif /* no __linux__ */ void enable_menu_item(wxUpdateUIEvent& evt, std::function const cb_condition, wxMenuItem* item, wxWindow* win) { const bool enable = cb_condition(); evt.Enable(enable); - -#ifdef __WXOSX__ - const auto it = msw_menuitem_bitmaps.find(item->GetId()); - if (it != msw_menuitem_bitmaps.end()) - { - const wxBitmap& item_icon = create_scaled_bitmap(it->second, win, 16, !enable); - if (item_icon.IsOk()) - item->SetBitmap(item_icon); - } -#endif // __WXOSX__ } wxMenuItem* append_menu_item(wxMenu* menu, int id, const wxString& string, const wxString& description, - std::function cb, const wxBitmap& icon, wxEvtHandler* event_handler, + std::function cb, wxBitmapBundle* icon, wxEvtHandler* event_handler, std::function const cb_condition, wxWindow* parent, int insert_pos/* = wxNOT_FOUND*/) { if (id == wxID_ANY) id = wxNewId(); auto *item = new wxMenuItem(menu, id, string, description); - if (icon.IsOk()) { - item->SetBitmap(icon); + if (icon && icon->IsOk()) { + item->SetBitmap(*icon); } if (insert_pos == wxNOT_FOUND) menu->Append(item); @@ -100,12 +89,12 @@ wxMenuItem* append_menu_item(wxMenu* menu, int id, const wxString& string, const if (id == wxID_ANY) id = wxNewId(); - const wxBitmap& bmp = !icon.empty() ? create_menu_bitmap(icon) : wxNullBitmap; // FIXME: pass window ptr -//#ifdef __WXMSW__ -#ifndef __WXGTK__ - if (bmp.IsOk()) + wxBitmapBundle* bmp = icon.empty() ? nullptr : get_bmp_bundle(icon); + +#ifndef __linux__ + if (bmp && bmp->IsOk()) msw_menuitem_bitmaps[id] = icon; -#endif /* __WXMSW__ */ +#endif /* no __linux__ */ return append_menu_item(menu, id, string, description, cb, bmp, event_handler, cb_condition, parent, insert_pos); } @@ -118,11 +107,11 @@ wxMenuItem* append_submenu(wxMenu* menu, wxMenu* sub_menu, int id, const wxStrin wxMenuItem* item = new wxMenuItem(menu, id, string, description); if (!icon.empty()) { - item->SetBitmap(create_menu_bitmap(icon)); // FIXME: pass window ptr -//#ifdef __WXMSW__ -#ifndef __WXGTK__ + item->SetBitmap(*get_bmp_bundle(icon)); + +#ifndef __linux__ msw_menuitem_bitmaps[id] = icon; -#endif /* __WXMSW__ */ +#endif // no __linux__ } item->SetSubMenu(sub_menu); @@ -420,9 +409,53 @@ int mode_icon_px_size() #endif } -wxBitmap create_menu_bitmap(const std::string& bmp_name) +#ifdef __WXGTK2__ +static int scale() { - return create_scaled_bitmap(bmp_name, nullptr, 16, false, "", true); + return int(em_unit(nullptr) * 0.1f + 0.5f); +} +#endif // __WXGTK2__ + +wxBitmapBundle* get_bmp_bundle(const std::string& bmp_name_in, int px_cnt/* = 16*/) +{ +#ifdef __WXGTK2__ + px_cnt *= scale(); +#endif // __WXGTK2__ + + static Slic3r::GUI::BitmapCache cache; + + std::string bmp_name = bmp_name_in; + boost::replace_last(bmp_name, ".png", ""); + + // Try loading an SVG first, then PNG if SVG is not found: + wxBitmapBundle* bmp = cache.from_svg(bmp_name, px_cnt, px_cnt, Slic3r::GUI::wxGetApp().dark_mode()); + if (bmp == nullptr) { + bmp = cache.from_png(bmp_name, px_cnt, px_cnt); + if (!bmp) + // Neither SVG nor PNG has been found, raise error + throw Slic3r::RuntimeError("Could not load bitmap: " + bmp_name); + } + return bmp; +} + +wxBitmapBundle* get_empty_bmp_bundle(int width, int height) +{ + static Slic3r::GUI::BitmapCache cache; +#ifdef __WXGTK2__ + return cache.mkclear_bndl(width * scale(), height * scale()); +#else + return cache.mkclear_bndl(width, height); +#endif // __WXGTK2__ +} + +wxBitmapBundle* get_solid_bmp_bundle(int width, int height, const std::string& color ) +{ + static Slic3r::GUI::BitmapCache cache; +#ifdef __WXGTK2__ + return cache.mksolid_bndl(width * scale(), height * scale(), color, 1, Slic3r::GUI::wxGetApp().dark_mode()); +#else + return cache.mksolid_bndl(width, height, color, 1, Slic3r::GUI::wxGetApp().dark_mode()); +#endif // __WXGTK2__ } // win is used to get a correct em_unit value @@ -463,41 +496,17 @@ wxBitmap create_scaled_bitmap( const std::string& bmp_name_in, return *bmp; } -std::vector get_extruder_color_icons(bool thin_icon/* = false*/) +std::vector get_extruder_color_icons(bool thin_icon/* = false*/) { - static Slic3r::GUI::BitmapCache bmp_cache; - // Create the bitmap with color bars. - std::vector bmps; + std::vector bmps; std::vector colors = Slic3r::GUI::wxGetApp().plater()->get_extruder_colors_from_plater_config(); if (colors.empty()) return bmps; - /* It's supposed that standard size of an icon is 36px*16px for 100% scaled display. - * So set sizes for solid_colored icons used for filament preset - * and scale them in respect to em_unit value - */ - const double em = Slic3r::GUI::wxGetApp().em_unit(); - const int icon_width = lround((thin_icon ? 1.6 : 3.2) * em); - const int icon_height = lround(1.6 * em); - - bool dark_mode = Slic3r::GUI::wxGetApp().dark_mode(); - for (const std::string& color : colors) - { - std::string bitmap_key = color + "-h" + std::to_string(icon_height) + "-w" + std::to_string(icon_width); - - wxBitmap* bitmap = bmp_cache.find(bitmap_key); - if (bitmap == nullptr) { - // Paint the color icon. - Slic3r::ColorRGB rgb; - Slic3r::decode_color(color, rgb); - // there is no neede to scale created solid bitmap - bitmap = bmp_cache.insert(bitmap_key, bmp_cache.mksolid(icon_width, icon_height, rgb, true, 1, dark_mode)); - } - bmps.emplace_back(bitmap); - } + bmps.emplace_back(get_solid_bmp_bundle(thin_icon ? 16 : 32, 16, color)); return bmps; } @@ -510,7 +519,7 @@ void apply_extruder_selector(Slic3r::GUI::BitmapComboBox** ctrl, wxSize size/* = wxDefaultSize*/, bool use_thin_icon/* = false*/) { - std::vector icons = get_extruder_color_icons(use_thin_icon); + std::vector icons = get_extruder_color_icons(use_thin_icon); if (!*ctrl) { *ctrl = new Slic3r::GUI::BitmapComboBox(parent, wxID_ANY, wxEmptyString, pos, size, 0, nullptr, wxCB_READONLY); @@ -536,7 +545,7 @@ void apply_extruder_selector(Slic3r::GUI::BitmapComboBox** ctrl, int i = 0; wxString str = _(L("Extruder")); - for (wxBitmap* bmp : icons) { + for (wxBitmapBundle* bmp : icons) { if (i == 0) { if (!first_item.empty()) (*ctrl)->Append(_(first_item), *bmp); @@ -570,7 +579,7 @@ LockButton::LockButton( wxWindow *parent, Slic3r::GUI::wxGetApp().UpdateDarkUI(this); SetBitmap(m_bmp_lock_open.bmp()); SetBitmapDisabled(m_bmp_lock_open.bmp()); - SetBitmapHover(m_bmp_lock_closed_f.bmp()); + SetBitmapCurrent(m_bmp_lock_closed_f.bmp()); //button events Bind(wxEVT_BUTTON, &LockButton::OnButton, this); @@ -581,8 +590,12 @@ void LockButton::OnButton(wxCommandEvent& event) if (m_disabled) return; +#if ENABLE_WORLD_COORDINATE + SetLock(!m_is_pushed); +#else m_is_pushed = !m_is_pushed; update_button_bitmaps(); +#endif // ENABLE_WORLD_COORDINATE event.Skip(); } @@ -595,12 +608,14 @@ void LockButton::SetLock(bool lock) } } -void LockButton::msw_rescale() +void LockButton::sys_color_changed() { - m_bmp_lock_closed.msw_rescale(); - m_bmp_lock_closed_f.msw_rescale(); - m_bmp_lock_open.msw_rescale(); - m_bmp_lock_open_f.msw_rescale(); + Slic3r::GUI::wxGetApp().UpdateDarkUI(this); + + m_bmp_lock_closed.sys_color_changed(); + m_bmp_lock_closed_f.sys_color_changed(); + m_bmp_lock_open.sys_color_changed(); + m_bmp_lock_open_f.sys_color_changed(); update_button_bitmaps(); } @@ -608,7 +623,7 @@ void LockButton::msw_rescale() void LockButton::update_button_bitmaps() { SetBitmap(m_is_pushed ? m_bmp_lock_closed.bmp() : m_bmp_lock_open.bmp()); - SetBitmapHover(m_is_pushed ? m_bmp_lock_closed_f.bmp() : m_bmp_lock_open_f.bmp()); + SetBitmapCurrent(m_is_pushed ? m_bmp_lock_closed_f.bmp() : m_bmp_lock_open_f.bmp()); Refresh(); Update(); @@ -635,7 +650,7 @@ ModeButton::ModeButton( wxWindow* parent, const wxString& mode/* = wxEmptyString*/, const std::string& icon_name/* = ""*/, int px_cnt/* = 16*/) : - ScalableButton(parent, wxID_ANY, ScalableBitmap(parent, icon_name, px_cnt), mode, wxBU_EXACTFIT) + ScalableButton(parent, wxID_ANY, icon_name, mode, wxDefaultSize, wxDefaultPosition, wxBU_EXACTFIT, px_cnt) { Init(mode); } @@ -745,11 +760,10 @@ void ModeSizer::set_items_border(int border) item->SetBorder(border); } -void ModeSizer::msw_rescale() +void ModeSizer::sys_color_changed() { - this->SetHGap(std::lround(m_hgap_unscaled * em_unit(m_parent))); for (size_t m = 0; m < m_mode_btns.size(); m++) - m_mode_btns[m]->msw_rescale(); + m_mode_btns[m]->sys_color_changed(); } // ---------------------------------------------------------------------------- @@ -789,40 +803,13 @@ ScalableBitmap::ScalableBitmap( wxWindow *parent, m_parent(parent), m_icon_name(icon_name), m_px_cnt(px_cnt) { - m_bmp = create_scaled_bitmap(icon_name, parent, px_cnt, grayscale); + m_bmp = *get_bmp_bundle(icon_name, px_cnt); + m_bitmap = m_bmp.GetBitmapFor(m_parent); } -wxSize ScalableBitmap::GetBmpSize() const +void ScalableBitmap::sys_color_changed() { -#ifdef __APPLE__ - return m_bmp.GetScaledSize(); -#else - return m_bmp.GetSize(); -#endif -} - -int ScalableBitmap::GetBmpWidth() const -{ -#ifdef __APPLE__ - return m_bmp.GetScaledWidth(); -#else - return m_bmp.GetWidth(); -#endif -} - -int ScalableBitmap::GetBmpHeight() const -{ -#ifdef __APPLE__ - return m_bmp.GetScaledHeight(); -#else - return m_bmp.GetHeight(); -#endif -} - - -void ScalableBitmap::msw_rescale() -{ - m_bmp = create_scaled_bitmap(m_icon_name, m_parent, m_px_cnt, m_grayscale); + m_bmp = *get_bmp_bundle(m_icon_name, m_px_cnt); } // ---------------------------------------------------------------------------- @@ -836,11 +823,9 @@ ScalableButton::ScalableButton( wxWindow * parent, const wxSize& size /* = wxDefaultSize*/, const wxPoint& pos /* = wxDefaultPosition*/, long style /*= wxBU_EXACTFIT | wxNO_BORDER*/, - bool use_default_disabled_bitmap/* = false*/, int bmp_px_cnt/* = 16*/) : m_parent(parent), m_current_icon_name(icon_name), - m_use_default_disabled_bitmap (use_default_disabled_bitmap), m_px_cnt(bmp_px_cnt), m_has_border(!(style & wxNO_BORDER)) { @@ -848,9 +833,7 @@ ScalableButton::ScalableButton( wxWindow * parent, Slic3r::GUI::wxGetApp().UpdateDarkUI(this); if (!icon_name.empty()) { - SetBitmap(create_scaled_bitmap(icon_name, parent, m_px_cnt)); - if (m_use_default_disabled_bitmap) - SetBitmapDisabled(create_scaled_bitmap(m_current_icon_name, m_parent, m_px_cnt, true)); + SetBitmap(*get_bmp_bundle(icon_name, m_px_cnt)); if (!label.empty()) SetBitmapMargins(int(0.5* em_unit(parent)), 0); } @@ -892,13 +875,12 @@ bool ScalableButton::SetBitmap_(const std::string& bmp_name) if (m_current_icon_name.empty()) return false; - wxBitmap bmp = create_scaled_bitmap(m_current_icon_name, m_parent, m_px_cnt); + wxBitmapBundle bmp = *get_bmp_bundle(m_current_icon_name, m_px_cnt); SetBitmap(bmp); SetBitmapCurrent(bmp); SetBitmapPressed(bmp); SetBitmapFocus(bmp); - if (m_use_default_disabled_bitmap) - SetBitmapDisabled(create_scaled_bitmap(m_current_icon_name, m_parent, m_px_cnt, true)); + SetBitmapDisabled(bmp); return true; } @@ -917,37 +899,21 @@ int ScalableButton::GetBitmapHeight() #endif } -void ScalableButton::UseDefaultBitmapDisabled() -{ - m_use_default_disabled_bitmap = true; - SetBitmapDisabled(create_scaled_bitmap(m_current_icon_name, m_parent, m_px_cnt, true)); -} - -void ScalableButton::msw_rescale() +void ScalableButton::sys_color_changed() { Slic3r::GUI::wxGetApp().UpdateDarkUI(this, m_has_border); - if (!m_current_icon_name.empty()) { - wxBitmap bmp = create_scaled_bitmap(m_current_icon_name, m_parent, m_px_cnt); - SetBitmap(bmp); - SetBitmapCurrent(bmp); - SetBitmapPressed(bmp); - SetBitmapFocus(bmp); - if (!m_disabled_icon_name.empty()) - SetBitmapDisabled(create_scaled_bitmap(m_disabled_icon_name, m_parent, m_px_cnt)); - else if (m_use_default_disabled_bitmap) - SetBitmapDisabled(create_scaled_bitmap(m_current_icon_name, m_parent, m_px_cnt, true)); - } - - if (m_width > 0 || m_height>0) - { - const int em = em_unit(m_parent); - wxSize size(m_width * em, m_height * em); - SetMinSize(size); - } + wxBitmapBundle bmp = *get_bmp_bundle(m_current_icon_name, m_px_cnt); + SetBitmap(bmp); + SetBitmapCurrent(bmp); + SetBitmapPressed(bmp); + SetBitmapFocus(bmp); + if (!m_disabled_icon_name.empty()) + SetBitmapDisabled(*get_bmp_bundle(m_disabled_icon_name, m_px_cnt)); + if (!GetLabelText().IsEmpty()) + SetBitmapMargins(int(0.5 * em_unit(m_parent)), 0); } - // ---------------------------------------------------------------------------- // BlinkingBitmap // ---------------------------------------------------------------------------- @@ -958,13 +924,6 @@ BlinkingBitmap::BlinkingBitmap(wxWindow* parent, const std::string& icon_name) : bmp = ScalableBitmap(parent, icon_name); } -void BlinkingBitmap::msw_rescale() -{ - bmp.msw_rescale(); - this->SetSize(bmp.GetBmpSize()); - this->SetMinSize(bmp.GetBmpSize()); -} - void BlinkingBitmap::invalidate() { this->SetBitmap(wxNullBitmap); diff --git a/src/slic3r/GUI/wxExtensions.hpp b/src/slic3r/GUI/wxExtensions.hpp index f3921919e..db05af9eb 100644 --- a/src/slic3r/GUI/wxExtensions.hpp +++ b/src/slic3r/GUI/wxExtensions.hpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -15,14 +16,14 @@ #include -#ifdef __WXMSW__ -void msw_rescale_menu(wxMenu* menu); -#else /* __WXMSW__ */ -inline void msw_rescale_menu(wxMenu* /* menu */) {} -#endif /* __WXMSW__ */ +#ifndef __linux__ +void sys_color_changed_menu(wxMenu* menu); +#else +inline void sys_color_changed_menu(wxMenu* /* menu */) {} +#endif // no __linux__ wxMenuItem* append_menu_item(wxMenu* menu, int id, const wxString& string, const wxString& description, - std::function cb, const wxBitmap& icon, wxEvtHandler* event_handler = nullptr, + std::function cb, wxBitmapBundle* icon, wxEvtHandler* event_handler = nullptr, std::function const cb_condition = []() { return true;}, wxWindow* parent = nullptr, int insert_pos = wxNOT_FOUND); wxMenuItem* append_menu_item(wxMenu* menu, int id, const wxString& string, const wxString& description, std::function cb, const std::string& icon = "", wxEvtHandler* event_handler = nullptr, @@ -49,14 +50,16 @@ void msw_buttons_rescale(wxDialog* dlg, const int em_unit, const std::vector< int em_unit(wxWindow* win); int mode_icon_px_size(); -wxBitmap create_menu_bitmap(const std::string& bmp_name); +wxBitmapBundle* get_bmp_bundle(const std::string& bmp_name, int px_cnt = 16); +wxBitmapBundle* get_empty_bmp_bundle(int width, int height); +wxBitmapBundle* get_solid_bmp_bundle(int width, int height, const std::string& color); wxBitmap create_scaled_bitmap(const std::string& bmp_name, wxWindow *win = nullptr, const int px_cnt = 16, const bool grayscale = false, const std::string& new_color = std::string(), // color witch will used instead of orange const bool menu_bitmap = false); -std::vector get_extruder_color_icons(bool thin_icon = false); +std::vector get_extruder_color_icons(bool thin_icon = false); namespace Slic3r { namespace GUI { @@ -145,21 +148,28 @@ public: ~ScalableBitmap() {} - wxSize GetBmpSize() const; - int GetBmpWidth() const; - int GetBmpHeight() const; + void sys_color_changed(); - void msw_rescale(); + const wxBitmapBundle& bmp() const { return m_bmp; } + wxBitmap get_bitmap() { return m_bmp.GetBitmapFor(m_parent); } + wxWindow* parent() const { return m_parent;} + const std::string& name() const { return m_icon_name; } + int px_cnt() const { return m_px_cnt;} - const wxBitmap& bmp() const { return m_bmp; } - wxBitmap& bmp() { return m_bmp; } - const std::string& name() const{ return m_icon_name; } - - int px_cnt()const {return m_px_cnt;} + wxSize GetSize() const { +#ifdef __WIN32__ + return m_bmp.GetPreferredBitmapSizeFor(m_parent); +#else + return m_bmp.GetDefaultSize(); +#endif + } + int GetWidth() const { return GetSize().GetWidth(); } + int GetHeight() const { return GetSize().GetHeight(); } private: wxWindow* m_parent{ nullptr }; - wxBitmap m_bmp = wxBitmap(); + wxBitmapBundle m_bmp = wxBitmapBundle(); + wxBitmap m_bitmap = wxBitmap(); std::string m_icon_name = ""; int m_px_cnt {16}; bool m_grayscale {false}; @@ -189,7 +199,7 @@ public: void enable() { m_disabled = false; } void disable() { m_disabled = true; } - void msw_rescale(); + void sys_color_changed(); protected: void update_button_bitmaps(); @@ -221,7 +231,6 @@ public: const wxSize& size = wxDefaultSize, const wxPoint& pos = wxDefaultPosition, long style = wxBU_EXACTFIT | wxNO_BORDER, - bool use_default_disabled_bitmap = false, int bmp_px_cnt = 16); ScalableButton( @@ -237,9 +246,8 @@ public: bool SetBitmap_(const std::string& bmp_name); void SetBitmapDisabled_(const ScalableBitmap &bmp); int GetBitmapHeight(); - void UseDefaultBitmapDisabled(); - void msw_rescale(); + void sys_color_changed(); private: wxWindow* m_parent { nullptr }; @@ -248,8 +256,6 @@ private: int m_width {-1}; // should be multiplied to em_unit int m_height{-1}; // should be multiplied to em_unit - bool m_use_default_disabled_bitmap {false}; - // bitmap dimensions int m_px_cnt{ 16 }; bool m_has_border {false}; @@ -315,7 +321,7 @@ public: void set_items_flag(int flag); void set_items_border(int border); - void msw_rescale(); + void sys_color_changed(); const std::vector& get_btns() { return m_mode_btns; } private: @@ -363,12 +369,11 @@ public: ~BlinkingBitmap() {} - void msw_rescale(); void invalidate(); void activate(); void blink(); - const wxBitmap& get_bmp() const { return bmp.bmp(); } + const wxBitmapBundle& get_bmp() const { return bmp.bmp(); } private: ScalableBitmap bmp; diff --git a/src/slic3r/Utils/FixModelByWin10.cpp b/src/slic3r/Utils/FixModelByWin10.cpp index 296c58622..6291d8734 100644 --- a/src/slic3r/Utils/FixModelByWin10.cpp +++ b/src/slic3r/Utils/FixModelByWin10.cpp @@ -1,449 +1,449 @@ -#ifdef HAS_WIN10SDK - -#ifndef NOMINMAX -# define NOMINMAX -#endif - -// Windows Runtime -#include -// for ComPtr -#include - -// from C:/Program Files (x86)/Windows Kits/10/Include/10.0.17134.0/ -#include -#include -#include - -#include "FixModelByWin10.hpp" - -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include - -#include "libslic3r/Model.hpp" -#include "libslic3r/Print.hpp" -#include "libslic3r/PresetBundle.hpp" -#include "libslic3r/Format/3mf.hpp" -#include "../GUI/GUI.hpp" -#include "../GUI/I18N.hpp" -#include "../GUI/MsgDialog.hpp" - -#include -#include - -extern "C"{ - // from rapi.h - typedef HRESULT (__stdcall* FunctionRoInitialize)(int); - typedef HRESULT (__stdcall* FunctionRoUninitialize)(); - typedef HRESULT (__stdcall* FunctionRoActivateInstance)(HSTRING activatableClassId, IInspectable **instance); - typedef HRESULT (__stdcall* FunctionRoGetActivationFactory)(HSTRING activatableClassId, REFIID iid, void **factory); - // from winstring.h - typedef HRESULT (__stdcall* FunctionWindowsCreateString)(LPCWSTR sourceString, UINT32 length, HSTRING *string); - typedef HRESULT (__stdcall* FunctionWindowsDelteString)(HSTRING string); -} - -namespace Slic3r { - -HMODULE s_hRuntimeObjectLibrary = nullptr; -FunctionRoInitialize s_RoInitialize = nullptr; -FunctionRoUninitialize s_RoUninitialize = nullptr; -FunctionRoActivateInstance s_RoActivateInstance = nullptr; -FunctionRoGetActivationFactory s_RoGetActivationFactory = nullptr; -FunctionWindowsCreateString s_WindowsCreateString = nullptr; -FunctionWindowsDelteString s_WindowsDeleteString = nullptr; - -bool winrt_load_runtime_object_library() -{ - if (s_hRuntimeObjectLibrary == nullptr) - s_hRuntimeObjectLibrary = LoadLibrary(L"ComBase.dll"); - if (s_hRuntimeObjectLibrary != nullptr) { - s_RoInitialize = (FunctionRoInitialize) GetProcAddress(s_hRuntimeObjectLibrary, "RoInitialize"); - s_RoUninitialize = (FunctionRoUninitialize) GetProcAddress(s_hRuntimeObjectLibrary, "RoUninitialize"); - s_RoActivateInstance = (FunctionRoActivateInstance) GetProcAddress(s_hRuntimeObjectLibrary, "RoActivateInstance"); - s_RoGetActivationFactory = (FunctionRoGetActivationFactory) GetProcAddress(s_hRuntimeObjectLibrary, "RoGetActivationFactory"); - s_WindowsCreateString = (FunctionWindowsCreateString) GetProcAddress(s_hRuntimeObjectLibrary, "WindowsCreateString"); - s_WindowsDeleteString = (FunctionWindowsDelteString) GetProcAddress(s_hRuntimeObjectLibrary, "WindowsDeleteString"); - } - return s_RoInitialize && s_RoUninitialize && s_RoActivateInstance && s_WindowsCreateString && s_WindowsDeleteString; -} - -static HRESULT winrt_activate_instance(const std::wstring &class_name, IInspectable **pinst) -{ - HSTRING hClassName; - HRESULT hr = (*s_WindowsCreateString)(class_name.c_str(), class_name.size(), &hClassName); - if (S_OK != hr) - return hr; - hr = (*s_RoActivateInstance)(hClassName, pinst); - (*s_WindowsDeleteString)(hClassName); - return hr; -} - -template -static HRESULT winrt_activate_instance(const std::wstring &class_name, TYPE **pinst) -{ - IInspectable *pinspectable = nullptr; - HRESULT hr = winrt_activate_instance(class_name, &pinspectable); - if (S_OK != hr) - return hr; - hr = pinspectable->QueryInterface(__uuidof(TYPE), (void**)pinst); - pinspectable->Release(); - return hr; -} - -static HRESULT winrt_get_activation_factory(const std::wstring &class_name, REFIID iid, void **pinst) -{ - HSTRING hClassName; - HRESULT hr = (*s_WindowsCreateString)(class_name.c_str(), class_name.size(), &hClassName); - if (S_OK != hr) - return hr; - hr = (*s_RoGetActivationFactory)(hClassName, iid, pinst); - (*s_WindowsDeleteString)(hClassName); - return hr; -} - -template -static HRESULT winrt_get_activation_factory(const std::wstring &class_name, TYPE **pinst) -{ - return winrt_get_activation_factory(class_name, __uuidof(TYPE), reinterpret_cast(pinst)); -} - -// To be called often to test whether to cancel the operation. -typedef std::function ThrowOnCancelFn; - -template -static AsyncStatus winrt_async_await(const Microsoft::WRL::ComPtr &asyncAction, ThrowOnCancelFn throw_on_cancel, int blocking_tick_ms = 100) -{ - Microsoft::WRL::ComPtr asyncInfo; - asyncAction.As(&asyncInfo); - AsyncStatus status; - // Ugly blocking loop until the RepairAsync call finishes. -//FIXME replace with a callback. -// https://social.msdn.microsoft.com/Forums/en-US/a5038fb4-b7b7-4504-969d-c102faa389fb/trying-to-block-an-async-operation-and-wait-for-a-particular-time?forum=vclanguage - for (;;) { - asyncInfo->get_Status(&status); - if (status != AsyncStatus::Started) - return status; - throw_on_cancel(); - ::Sleep(blocking_tick_ms); - } -} - -static HRESULT winrt_open_file_stream( - const std::wstring &path, - ABI::Windows::Storage::FileAccessMode mode, - ABI::Windows::Storage::Streams::IRandomAccessStream **fileStream, - ThrowOnCancelFn throw_on_cancel) -{ - // Get the file factory. - Microsoft::WRL::ComPtr fileFactory; - HRESULT hr = winrt_get_activation_factory(L"Windows.Storage.StorageFile", fileFactory.GetAddressOf()); - if (FAILED(hr)) return hr; - - // Open the file asynchronously. - HSTRING hstr_path; - hr = (*s_WindowsCreateString)(path.c_str(), path.size(), &hstr_path); - if (FAILED(hr)) return hr; - Microsoft::WRL::ComPtr> fileOpenAsync; - hr = fileFactory->GetFileFromPathAsync(hstr_path, fileOpenAsync.GetAddressOf()); - if (FAILED(hr)) return hr; - (*s_WindowsDeleteString)(hstr_path); - - // Wait until the file gets open, get the actual file. - AsyncStatus status = winrt_async_await(fileOpenAsync, throw_on_cancel); - Microsoft::WRL::ComPtr storageFile; - if (status == AsyncStatus::Completed) { - hr = fileOpenAsync->GetResults(storageFile.GetAddressOf()); - } else { - Microsoft::WRL::ComPtr asyncInfo; - hr = fileOpenAsync.As(&asyncInfo); - if (FAILED(hr)) return hr; - HRESULT err; - hr = asyncInfo->get_ErrorCode(&err); - return FAILED(hr) ? hr : err; - } - - Microsoft::WRL::ComPtr> fileStreamAsync; - hr = storageFile->OpenAsync(mode, fileStreamAsync.GetAddressOf()); - if (FAILED(hr)) return hr; - - status = winrt_async_await(fileStreamAsync, throw_on_cancel); - if (status == AsyncStatus::Completed) { - hr = fileStreamAsync->GetResults(fileStream); - } else { - Microsoft::WRL::ComPtr asyncInfo; - hr = fileStreamAsync.As(&asyncInfo); - if (FAILED(hr)) return hr; - HRESULT err; - hr = asyncInfo->get_ErrorCode(&err); - if (!FAILED(hr)) - hr = err; - } - return hr; -} - -bool is_windows10() -{ - HKEY hKey; - LONG lRes = RegOpenKeyExW(HKEY_LOCAL_MACHINE, L"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion", 0, KEY_READ, &hKey); - if (lRes == ERROR_SUCCESS) { - WCHAR szBuffer[512]; - DWORD dwBufferSize = sizeof(szBuffer); - lRes = RegQueryValueExW(hKey, L"ProductName", 0, nullptr, (LPBYTE)szBuffer, &dwBufferSize); - if (lRes == ERROR_SUCCESS) - return wcsncmp(szBuffer, L"Windows 10", 10) == 0; - RegCloseKey(hKey); - } - return false; -} - -// Progress function, to be called regularly to update the progress. -typedef std::function ProgressFn; - -void fix_model_by_win10_sdk(const std::string &path_src, const std::string &path_dst, ProgressFn on_progress, ThrowOnCancelFn throw_on_cancel) -{ - if (! is_windows10()) - throw Slic3r::RuntimeError("fix_model_by_win10_sdk called on non Windows 10 system"); - - if (! winrt_load_runtime_object_library()) - throw Slic3r::RuntimeError("Failed to initialize the WinRT library."); - - HRESULT hr = (*s_RoInitialize)(RO_INIT_MULTITHREADED); - { - on_progress(L("Exporting source model"), 20); - - Microsoft::WRL::ComPtr fileStream; - hr = winrt_open_file_stream(boost::nowide::widen(path_src), ABI::Windows::Storage::FileAccessMode::FileAccessMode_Read, fileStream.GetAddressOf(), throw_on_cancel); - - Microsoft::WRL::ComPtr printing3d3mfpackage; - hr = winrt_activate_instance(L"Windows.Graphics.Printing3D.Printing3D3MFPackage", printing3d3mfpackage.GetAddressOf()); - - Microsoft::WRL::ComPtr> modelAsync; - hr = printing3d3mfpackage->LoadModelFromPackageAsync(fileStream.Get(), modelAsync.GetAddressOf()); - - AsyncStatus status = winrt_async_await(modelAsync, throw_on_cancel); - Microsoft::WRL::ComPtr model; - if (status == AsyncStatus::Completed) - hr = modelAsync->GetResults(model.GetAddressOf()); - else - throw Slic3r::RuntimeError(L("Failed loading the input model.")); - - Microsoft::WRL::ComPtr> meshes; - hr = model->get_Meshes(meshes.GetAddressOf()); - unsigned num_meshes = 0; - hr = meshes->get_Size(&num_meshes); - - on_progress(L("Repairing model by the Netfabb service"), 40); - - Microsoft::WRL::ComPtr repairAsync; - hr = model->RepairAsync(repairAsync.GetAddressOf()); - status = winrt_async_await(repairAsync, throw_on_cancel); - if (status != AsyncStatus::Completed) - throw Slic3r::RuntimeError(L("Mesh repair failed.")); - repairAsync->GetResults(); - - on_progress(L("Loading repaired model"), 60); - - // Verify the number of meshes returned after the repair action. - meshes.Reset(); - hr = model->get_Meshes(meshes.GetAddressOf()); - hr = meshes->get_Size(&num_meshes); - - // Save model to this class' Printing3D3MFPackage. - Microsoft::WRL::ComPtr saveToPackageAsync; - hr = printing3d3mfpackage->SaveModelToPackageAsync(model.Get(), saveToPackageAsync.GetAddressOf()); - status = winrt_async_await(saveToPackageAsync, throw_on_cancel); - if (status != AsyncStatus::Completed) - throw Slic3r::RuntimeError(L("Saving mesh into the 3MF container failed.")); - hr = saveToPackageAsync->GetResults(); - - Microsoft::WRL::ComPtr> generatorStreamAsync; - hr = printing3d3mfpackage->SaveAsync(generatorStreamAsync.GetAddressOf()); - status = winrt_async_await(generatorStreamAsync, throw_on_cancel); - if (status != AsyncStatus::Completed) - throw Slic3r::RuntimeError(L("Saving mesh into the 3MF container failed.")); - Microsoft::WRL::ComPtr generatorStream; - hr = generatorStreamAsync->GetResults(generatorStream.GetAddressOf()); - - // Go to the beginning of the stream. - generatorStream->Seek(0); - Microsoft::WRL::ComPtr inputStream; - hr = generatorStream.As(&inputStream); - - // Get the buffer factory. - Microsoft::WRL::ComPtr bufferFactory; - hr = winrt_get_activation_factory(L"Windows.Storage.Streams.Buffer", bufferFactory.GetAddressOf()); - - // Open the destination file. - FILE *fout = boost::nowide::fopen(path_dst.c_str(), "wb"); - try { - Microsoft::WRL::ComPtr buffer; - byte *buffer_ptr; - bufferFactory->Create(65536 * 2048, buffer.GetAddressOf()); - { - Microsoft::WRL::ComPtr bufferByteAccess; - buffer.As(&bufferByteAccess); - hr = bufferByteAccess->Buffer(&buffer_ptr); - } - uint32_t length; - hr = buffer->get_Length(&length); - Microsoft::WRL::ComPtr> asyncRead; - for (;;) { - hr = inputStream->ReadAsync(buffer.Get(), 65536 * 2048, ABI::Windows::Storage::Streams::InputStreamOptions_ReadAhead, asyncRead.GetAddressOf()); - status = winrt_async_await(asyncRead, throw_on_cancel); - if (status != AsyncStatus::Completed) - throw Slic3r::RuntimeError(L("Saving mesh into the 3MF container failed.")); - hr = buffer->get_Length(&length); - if (length == 0) - break; - fwrite(buffer_ptr, length, 1, fout); - } - } catch (...) { - fclose(fout); - throw; - } - fclose(fout); - // Here all the COM objects will be released through the ComPtr destructors. - } - (*s_RoUninitialize)(); -} - -class RepairCanceledException : public std::exception { -public: - const char* what() const throw() { return "Model repair has been canceled"; } -}; - -// returt FALSE, if fixing was canceled -// fix_result is empty, if fixing finished successfully -// fix_result containes a message if fixing failed -bool fix_model_by_win10_sdk_gui(ModelObject &model_object, int volume_idx, wxProgressDialog& progress_dialog, const wxString& msg_header, std::string& fix_result) -{ - std::mutex mutex; - std::condition_variable condition; - std::unique_lock lock(mutex); - struct Progress { - std::string message; - int percent = 0; - bool updated = false; - } progress; - std::atomic canceled = false; - std::atomic finished = false; - - std::vector volumes; - if (volume_idx == -1) - volumes = model_object.volumes; - else - volumes.emplace_back(model_object.volumes[volume_idx]); - - // Executing the calculation in a background thread, so that the COM context could be created with its own threading model. - // (It seems like wxWidgets initialize the COM contex as single threaded and we need a multi-threaded context). - bool success = false; - size_t ivolume = 0; - auto on_progress = [&mutex, &condition, &ivolume, &volumes, &progress](const char *msg, unsigned prcnt) { - std::lock_guard lk(mutex); - progress.message = msg; - progress.percent = (int)floor((float(prcnt) + float(ivolume) * 100.f) / float(volumes.size())); - progress.updated = true; - condition.notify_all(); - }; - auto worker_thread = boost::thread([&model_object, &volumes, &ivolume, on_progress, &success, &canceled, &finished]() { - try { - std::vector meshes_repaired; - meshes_repaired.reserve(volumes.size()); - for (; ivolume < volumes.size(); ++ ivolume) { - on_progress(L("Exporting source model"), 0); - boost::filesystem::path path_src = boost::filesystem::temp_directory_path() / boost::filesystem::unique_path(); - path_src += ".3mf"; - Model model; - ModelObject *mo = model.add_object(); - mo->add_volume(*volumes[ivolume]); - - // We are about to save a 3mf, fix it by netfabb and load the fixed 3mf back. - // store_3mf currently bakes the volume transformation into the mesh itself. - // If we then loaded the repaired 3mf and pushed the mesh into the original ModelVolume - // (which remembers the matrix the whole time), the transformation would be used twice. - // We will therefore set the volume transform on the dummy ModelVolume to identity. - mo->volumes.back()->set_transformation(Geometry::Transformation()); - - mo->add_instance(); - if (!Slic3r::store_3mf(path_src.string().c_str(), &model, nullptr, false, nullptr, false)) { - boost::filesystem::remove(path_src); - throw Slic3r::RuntimeError(L("Export of a temporary 3mf file failed")); - } - model.clear_objects(); - model.clear_materials(); - boost::filesystem::path path_dst = boost::filesystem::temp_directory_path() / boost::filesystem::unique_path(); - path_dst += ".3mf"; - fix_model_by_win10_sdk(path_src.string().c_str(), path_dst.string(), on_progress, - [&canceled]() { if (canceled) throw RepairCanceledException(); }); - boost::filesystem::remove(path_src); - // PresetBundle bundle; - on_progress(L("Loading repaired model"), 80); - DynamicPrintConfig config; - ConfigSubstitutionContext config_substitutions{ ForwardCompatibilitySubstitutionRule::EnableSilent }; - bool loaded = Slic3r::load_3mf(path_dst.string().c_str(), config, config_substitutions, &model, false); - boost::filesystem::remove(path_dst); - if (! loaded) - throw Slic3r::RuntimeError(L("Import of the repaired 3mf file failed")); - if (model.objects.size() == 0) - throw Slic3r::RuntimeError(L("Repaired 3MF file does not contain any object")); - if (model.objects.size() > 1) - throw Slic3r::RuntimeError(L("Repaired 3MF file contains more than one object")); - if (model.objects.front()->volumes.size() == 0) - throw Slic3r::RuntimeError(L("Repaired 3MF file does not contain any volume")); - if (model.objects.front()->volumes.size() > 1) - throw Slic3r::RuntimeError(L("Repaired 3MF file contains more than one volume")); - meshes_repaired.emplace_back(std::move(model.objects.front()->volumes.front()->mesh())); - } - for (size_t i = 0; i < volumes.size(); ++ i) { - volumes[i]->set_mesh(std::move(meshes_repaired[i])); - volumes[i]->calculate_convex_hull(); - volumes[i]->set_new_unique_id(); - } - model_object.invalidate_bounding_box(); - -- ivolume; - on_progress(L("Model repair finished"), 100); - success = true; - finished = true; - } catch (RepairCanceledException & /* ex */) { - canceled = true; - finished = true; - on_progress(L("Model repair canceled"), 100); - } catch (std::exception &ex) { - success = false; - finished = true; - on_progress(ex.what(), 100); - } - }); - while (! finished) { - condition.wait_for(lock, std::chrono::milliseconds(250), [&progress]{ return progress.updated; }); - // decrease progress.percent value to avoid closing of the progress dialog - if (!progress_dialog.Update(progress.percent-1, msg_header + _(progress.message))) - canceled = true; - else - progress_dialog.Fit(); - progress.updated = false; - } - - if (canceled) { - // Nothing to show. - } else if (success) { - fix_result = ""; - } else { - fix_result = progress.message; - } - worker_thread.join(); - return !canceled; -} - -} // namespace Slic3r - -#endif /* HAS_WIN10SDK */ +#ifdef HAS_WIN10SDK + +#ifndef NOMINMAX +# define NOMINMAX +#endif + +// Windows Runtime +#include +// for ComPtr +#include + +// from C:/Program Files (x86)/Windows Kits/10/Include/10.0.17134.0/ +#include +#include +#include + +#include "FixModelByWin10.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "libslic3r/Model.hpp" +#include "libslic3r/Print.hpp" +#include "libslic3r/PresetBundle.hpp" +#include "libslic3r/Format/3mf.hpp" +#include "../GUI/GUI.hpp" +#include "../GUI/I18N.hpp" +#include "../GUI/MsgDialog.hpp" + +#include +#include + +extern "C"{ + // from rapi.h + typedef HRESULT (__stdcall* FunctionRoInitialize)(int); + typedef HRESULT (__stdcall* FunctionRoUninitialize)(); + typedef HRESULT (__stdcall* FunctionRoActivateInstance)(HSTRING activatableClassId, IInspectable **instance); + typedef HRESULT (__stdcall* FunctionRoGetActivationFactory)(HSTRING activatableClassId, REFIID iid, void **factory); + // from winstring.h + typedef HRESULT (__stdcall* FunctionWindowsCreateString)(LPCWSTR sourceString, UINT32 length, HSTRING *string); + typedef HRESULT (__stdcall* FunctionWindowsDelteString)(HSTRING string); +} + +namespace Slic3r { + +HMODULE s_hRuntimeObjectLibrary = nullptr; +FunctionRoInitialize s_RoInitialize = nullptr; +FunctionRoUninitialize s_RoUninitialize = nullptr; +FunctionRoActivateInstance s_RoActivateInstance = nullptr; +FunctionRoGetActivationFactory s_RoGetActivationFactory = nullptr; +FunctionWindowsCreateString s_WindowsCreateString = nullptr; +FunctionWindowsDelteString s_WindowsDeleteString = nullptr; + +bool winrt_load_runtime_object_library() +{ + if (s_hRuntimeObjectLibrary == nullptr) + s_hRuntimeObjectLibrary = LoadLibrary(L"ComBase.dll"); + if (s_hRuntimeObjectLibrary != nullptr) { + s_RoInitialize = (FunctionRoInitialize) GetProcAddress(s_hRuntimeObjectLibrary, "RoInitialize"); + s_RoUninitialize = (FunctionRoUninitialize) GetProcAddress(s_hRuntimeObjectLibrary, "RoUninitialize"); + s_RoActivateInstance = (FunctionRoActivateInstance) GetProcAddress(s_hRuntimeObjectLibrary, "RoActivateInstance"); + s_RoGetActivationFactory = (FunctionRoGetActivationFactory) GetProcAddress(s_hRuntimeObjectLibrary, "RoGetActivationFactory"); + s_WindowsCreateString = (FunctionWindowsCreateString) GetProcAddress(s_hRuntimeObjectLibrary, "WindowsCreateString"); + s_WindowsDeleteString = (FunctionWindowsDelteString) GetProcAddress(s_hRuntimeObjectLibrary, "WindowsDeleteString"); + } + return s_RoInitialize && s_RoUninitialize && s_RoActivateInstance && s_WindowsCreateString && s_WindowsDeleteString; +} + +static HRESULT winrt_activate_instance(const std::wstring &class_name, IInspectable **pinst) +{ + HSTRING hClassName; + HRESULT hr = (*s_WindowsCreateString)(class_name.c_str(), class_name.size(), &hClassName); + if (S_OK != hr) + return hr; + hr = (*s_RoActivateInstance)(hClassName, pinst); + (*s_WindowsDeleteString)(hClassName); + return hr; +} + +template +static HRESULT winrt_activate_instance(const std::wstring &class_name, TYPE **pinst) +{ + IInspectable *pinspectable = nullptr; + HRESULT hr = winrt_activate_instance(class_name, &pinspectable); + if (S_OK != hr) + return hr; + hr = pinspectable->QueryInterface(__uuidof(TYPE), (void**)pinst); + pinspectable->Release(); + return hr; +} + +static HRESULT winrt_get_activation_factory(const std::wstring &class_name, REFIID iid, void **pinst) +{ + HSTRING hClassName; + HRESULT hr = (*s_WindowsCreateString)(class_name.c_str(), class_name.size(), &hClassName); + if (S_OK != hr) + return hr; + hr = (*s_RoGetActivationFactory)(hClassName, iid, pinst); + (*s_WindowsDeleteString)(hClassName); + return hr; +} + +template +static HRESULT winrt_get_activation_factory(const std::wstring &class_name, TYPE **pinst) +{ + return winrt_get_activation_factory(class_name, __uuidof(TYPE), reinterpret_cast(pinst)); +} + +// To be called often to test whether to cancel the operation. +typedef std::function ThrowOnCancelFn; + +template +static AsyncStatus winrt_async_await(const Microsoft::WRL::ComPtr &asyncAction, ThrowOnCancelFn throw_on_cancel, int blocking_tick_ms = 100) +{ + Microsoft::WRL::ComPtr asyncInfo; + asyncAction.As(&asyncInfo); + AsyncStatus status; + // Ugly blocking loop until the RepairAsync call finishes. +//FIXME replace with a callback. +// https://social.msdn.microsoft.com/Forums/en-US/a5038fb4-b7b7-4504-969d-c102faa389fb/trying-to-block-an-async-operation-and-wait-for-a-particular-time?forum=vclanguage + for (;;) { + asyncInfo->get_Status(&status); + if (status != AsyncStatus::Started) + return status; + throw_on_cancel(); + ::Sleep(blocking_tick_ms); + } +} + +static HRESULT winrt_open_file_stream( + const std::wstring &path, + ABI::Windows::Storage::FileAccessMode mode, + ABI::Windows::Storage::Streams::IRandomAccessStream **fileStream, + ThrowOnCancelFn throw_on_cancel) +{ + // Get the file factory. + Microsoft::WRL::ComPtr fileFactory; + HRESULT hr = winrt_get_activation_factory(L"Windows.Storage.StorageFile", fileFactory.GetAddressOf()); + if (FAILED(hr)) return hr; + + // Open the file asynchronously. + HSTRING hstr_path; + hr = (*s_WindowsCreateString)(path.c_str(), path.size(), &hstr_path); + if (FAILED(hr)) return hr; + Microsoft::WRL::ComPtr> fileOpenAsync; + hr = fileFactory->GetFileFromPathAsync(hstr_path, fileOpenAsync.GetAddressOf()); + if (FAILED(hr)) return hr; + (*s_WindowsDeleteString)(hstr_path); + + // Wait until the file gets open, get the actual file. + AsyncStatus status = winrt_async_await(fileOpenAsync, throw_on_cancel); + Microsoft::WRL::ComPtr storageFile; + if (status == AsyncStatus::Completed) { + hr = fileOpenAsync->GetResults(storageFile.GetAddressOf()); + } else { + Microsoft::WRL::ComPtr asyncInfo; + hr = fileOpenAsync.As(&asyncInfo); + if (FAILED(hr)) return hr; + HRESULT err; + hr = asyncInfo->get_ErrorCode(&err); + return FAILED(hr) ? hr : err; + } + + Microsoft::WRL::ComPtr> fileStreamAsync; + hr = storageFile->OpenAsync(mode, fileStreamAsync.GetAddressOf()); + if (FAILED(hr)) return hr; + + status = winrt_async_await(fileStreamAsync, throw_on_cancel); + if (status == AsyncStatus::Completed) { + hr = fileStreamAsync->GetResults(fileStream); + } else { + Microsoft::WRL::ComPtr asyncInfo; + hr = fileStreamAsync.As(&asyncInfo); + if (FAILED(hr)) return hr; + HRESULT err; + hr = asyncInfo->get_ErrorCode(&err); + if (!FAILED(hr)) + hr = err; + } + return hr; +} + +bool is_windows10() +{ + HKEY hKey; + LONG lRes = RegOpenKeyExW(HKEY_LOCAL_MACHINE, L"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion", 0, KEY_READ, &hKey); + if (lRes == ERROR_SUCCESS) { + WCHAR szBuffer[512]; + DWORD dwBufferSize = sizeof(szBuffer); + lRes = RegQueryValueExW(hKey, L"ProductName", 0, nullptr, (LPBYTE)szBuffer, &dwBufferSize); + if (lRes == ERROR_SUCCESS) + return wcsncmp(szBuffer, L"Windows 10", 10) == 0; + RegCloseKey(hKey); + } + return false; +} + +// Progress function, to be called regularly to update the progress. +typedef std::function ProgressFn; + +void fix_model_by_win10_sdk(const std::string &path_src, const std::string &path_dst, ProgressFn on_progress, ThrowOnCancelFn throw_on_cancel) +{ + if (! is_windows10()) + throw Slic3r::RuntimeError("fix_model_by_win10_sdk called on non Windows 10 system"); + + if (! winrt_load_runtime_object_library()) + throw Slic3r::RuntimeError("Failed to initialize the WinRT library."); + + HRESULT hr = (*s_RoInitialize)(RO_INIT_MULTITHREADED); + { + on_progress(L("Exporting source model"), 20); + + Microsoft::WRL::ComPtr fileStream; + hr = winrt_open_file_stream(boost::nowide::widen(path_src), ABI::Windows::Storage::FileAccessMode::FileAccessMode_Read, fileStream.GetAddressOf(), throw_on_cancel); + + Microsoft::WRL::ComPtr printing3d3mfpackage; + hr = winrt_activate_instance(L"Windows.Graphics.Printing3D.Printing3D3MFPackage", printing3d3mfpackage.GetAddressOf()); + + Microsoft::WRL::ComPtr> modelAsync; + hr = printing3d3mfpackage->LoadModelFromPackageAsync(fileStream.Get(), modelAsync.GetAddressOf()); + + AsyncStatus status = winrt_async_await(modelAsync, throw_on_cancel); + Microsoft::WRL::ComPtr model; + if (status == AsyncStatus::Completed) + hr = modelAsync->GetResults(model.GetAddressOf()); + else + throw Slic3r::RuntimeError(L("Failed loading the input model.")); + + Microsoft::WRL::ComPtr> meshes; + hr = model->get_Meshes(meshes.GetAddressOf()); + unsigned num_meshes = 0; + hr = meshes->get_Size(&num_meshes); + + on_progress(L("Repairing model by the Netfabb service"), 40); + + Microsoft::WRL::ComPtr repairAsync; + hr = model->RepairAsync(repairAsync.GetAddressOf()); + status = winrt_async_await(repairAsync, throw_on_cancel); + if (status != AsyncStatus::Completed) + throw Slic3r::RuntimeError(L("Mesh repair failed.")); + repairAsync->GetResults(); + + on_progress(L("Loading repaired model"), 60); + + // Verify the number of meshes returned after the repair action. + meshes.Reset(); + hr = model->get_Meshes(meshes.GetAddressOf()); + hr = meshes->get_Size(&num_meshes); + + // Save model to this class' Printing3D3MFPackage. + Microsoft::WRL::ComPtr saveToPackageAsync; + hr = printing3d3mfpackage->SaveModelToPackageAsync(model.Get(), saveToPackageAsync.GetAddressOf()); + status = winrt_async_await(saveToPackageAsync, throw_on_cancel); + if (status != AsyncStatus::Completed) + throw Slic3r::RuntimeError(L("Saving mesh into the 3MF container failed.")); + hr = saveToPackageAsync->GetResults(); + + Microsoft::WRL::ComPtr> generatorStreamAsync; + hr = printing3d3mfpackage->SaveAsync(generatorStreamAsync.GetAddressOf()); + status = winrt_async_await(generatorStreamAsync, throw_on_cancel); + if (status != AsyncStatus::Completed) + throw Slic3r::RuntimeError(L("Saving mesh into the 3MF container failed.")); + Microsoft::WRL::ComPtr generatorStream; + hr = generatorStreamAsync->GetResults(generatorStream.GetAddressOf()); + + // Go to the beginning of the stream. + generatorStream->Seek(0); + Microsoft::WRL::ComPtr inputStream; + hr = generatorStream.As(&inputStream); + + // Get the buffer factory. + Microsoft::WRL::ComPtr bufferFactory; + hr = winrt_get_activation_factory(L"Windows.Storage.Streams.Buffer", bufferFactory.GetAddressOf()); + + // Open the destination file. + FILE *fout = boost::nowide::fopen(path_dst.c_str(), "wb"); + try { + Microsoft::WRL::ComPtr buffer; + byte *buffer_ptr; + bufferFactory->Create(65536 * 2048, buffer.GetAddressOf()); + { + Microsoft::WRL::ComPtr bufferByteAccess; + buffer.As(&bufferByteAccess); + hr = bufferByteAccess->Buffer(&buffer_ptr); + } + uint32_t length; + hr = buffer->get_Length(&length); + Microsoft::WRL::ComPtr> asyncRead; + for (;;) { + hr = inputStream->ReadAsync(buffer.Get(), 65536 * 2048, ABI::Windows::Storage::Streams::InputStreamOptions_ReadAhead, asyncRead.GetAddressOf()); + status = winrt_async_await(asyncRead, throw_on_cancel); + if (status != AsyncStatus::Completed) + throw Slic3r::RuntimeError(L("Saving mesh into the 3MF container failed.")); + hr = buffer->get_Length(&length); + if (length == 0) + break; + fwrite(buffer_ptr, length, 1, fout); + } + } catch (...) { + fclose(fout); + throw; + } + fclose(fout); + // Here all the COM objects will be released through the ComPtr destructors. + } + (*s_RoUninitialize)(); +} + +class RepairCanceledException : public std::exception { +public: + const char* what() const throw() { return "Model repair has been canceled"; } +}; + +// returt FALSE, if fixing was canceled +// fix_result is empty, if fixing finished successfully +// fix_result containes a message if fixing failed +bool fix_model_by_win10_sdk_gui(ModelObject &model_object, int volume_idx, wxProgressDialog& progress_dialog, const wxString& msg_header, std::string& fix_result) +{ + std::mutex mutex; + std::condition_variable condition; + std::unique_lock lock(mutex); + struct Progress { + std::string message; + int percent = 0; + bool updated = false; + } progress; + std::atomic canceled = false; + std::atomic finished = false; + + std::vector volumes; + if (volume_idx == -1) + volumes = model_object.volumes; + else + volumes.emplace_back(model_object.volumes[volume_idx]); + + // Executing the calculation in a background thread, so that the COM context could be created with its own threading model. + // (It seems like wxWidgets initialize the COM contex as single threaded and we need a multi-threaded context). + bool success = false; + size_t ivolume = 0; + auto on_progress = [&mutex, &condition, &ivolume, &volumes, &progress](const char *msg, unsigned prcnt) { + std::lock_guard lk(mutex); + progress.message = msg; + progress.percent = (int)floor((float(prcnt) + float(ivolume) * 100.f) / float(volumes.size())); + progress.updated = true; + condition.notify_all(); + }; + auto worker_thread = boost::thread([&model_object, &volumes, &ivolume, on_progress, &success, &canceled, &finished]() { + try { + std::vector meshes_repaired; + meshes_repaired.reserve(volumes.size()); + for (; ivolume < volumes.size(); ++ ivolume) { + on_progress(L("Exporting source model"), 0); + boost::filesystem::path path_src = boost::filesystem::temp_directory_path() / boost::filesystem::unique_path(); + path_src += ".3mf"; + Model model; + ModelObject *mo = model.add_object(); + mo->add_volume(*volumes[ivolume]); + + // We are about to save a 3mf, fix it by netfabb and load the fixed 3mf back. + // store_3mf currently bakes the volume transformation into the mesh itself. + // If we then loaded the repaired 3mf and pushed the mesh into the original ModelVolume + // (which remembers the matrix the whole time), the transformation would be used twice. + // We will therefore set the volume transform on the dummy ModelVolume to identity. + mo->volumes.back()->set_transformation(Geometry::Transformation()); + + mo->add_instance(); + if (!Slic3r::store_3mf(path_src.string().c_str(), &model, nullptr, false, nullptr, false)) { + boost::filesystem::remove(path_src); + throw Slic3r::RuntimeError(L("Export of a temporary 3mf file failed")); + } + model.clear_objects(); + model.clear_materials(); + boost::filesystem::path path_dst = boost::filesystem::temp_directory_path() / boost::filesystem::unique_path(); + path_dst += ".3mf"; + fix_model_by_win10_sdk(path_src.string().c_str(), path_dst.string(), on_progress, + [&canceled]() { if (canceled) throw RepairCanceledException(); }); + boost::filesystem::remove(path_src); + // PresetBundle bundle; + on_progress(L("Loading repaired model"), 80); + DynamicPrintConfig config; + ConfigSubstitutionContext config_substitutions{ ForwardCompatibilitySubstitutionRule::EnableSilent }; + bool loaded = Slic3r::load_3mf(path_dst.string().c_str(), config, config_substitutions, &model, false); + boost::filesystem::remove(path_dst); + if (! loaded) + throw Slic3r::RuntimeError(L("Import of the repaired 3mf file failed")); + if (model.objects.size() == 0) + throw Slic3r::RuntimeError(L("Repaired 3MF file does not contain any object")); + if (model.objects.size() > 1) + throw Slic3r::RuntimeError(L("Repaired 3MF file contains more than one object")); + if (model.objects.front()->volumes.size() == 0) + throw Slic3r::RuntimeError(L("Repaired 3MF file does not contain any volume")); + if (model.objects.front()->volumes.size() > 1) + throw Slic3r::RuntimeError(L("Repaired 3MF file contains more than one volume")); + meshes_repaired.emplace_back(std::move(model.objects.front()->volumes.front()->mesh())); + } + for (size_t i = 0; i < volumes.size(); ++ i) { + volumes[i]->set_mesh(std::move(meshes_repaired[i])); + volumes[i]->calculate_convex_hull(); + volumes[i]->set_new_unique_id(); + } + model_object.invalidate_bounding_box(); + -- ivolume; + on_progress(L("Model repair finished"), 100); + success = true; + finished = true; + } catch (RepairCanceledException & /* ex */) { + canceled = true; + finished = true; + on_progress(L("Model repair canceled"), 100); + } catch (std::exception &ex) { + success = false; + finished = true; + on_progress(ex.what(), 100); + } + }); + while (! finished) { + condition.wait_for(lock, std::chrono::milliseconds(250), [&progress]{ return progress.updated; }); + // decrease progress.percent value to avoid closing of the progress dialog + if (!progress_dialog.Update(progress.percent-1, msg_header + _(progress.message))) + canceled = true; + else + progress_dialog.Fit(); + progress.updated = false; + } + + if (canceled) { + // Nothing to show. + } else if (success) { + fix_result = ""; + } else { + fix_result = progress.message; + } + worker_thread.join(); + return !canceled; +} + +} // namespace Slic3r + +#endif /* HAS_WIN10SDK */ diff --git a/src/slic3r/Utils/Http.cpp b/src/slic3r/Utils/Http.cpp index 68ddda041..a6ad54b32 100644 --- a/src/slic3r/Utils/Http.cpp +++ b/src/slic3r/Utils/Http.cpp @@ -181,7 +181,7 @@ Http::priv::~priv() bool Http::priv::ca_file_supported(::CURL *curl) { -#ifdef _WIN32 +#if defined(_WIN32) || defined(__APPLE__) bool res = false; #else bool res = true; @@ -194,6 +194,7 @@ bool Http::priv::ca_file_supported(::CURL *curl) if (::curl_easy_getinfo(curl, CURLINFO_TLS_SSL_PTR, &tls) == CURLE_OK) { if (tls->backend == CURLSSLBACKEND_SCHANNEL || tls->backend == CURLSSLBACKEND_DARWINSSL) { // With Windows and OS X native SSL support, cert files cannot be set + // DK: OSX is now not building CURL and links system one, thus we do not know which backend is installed. Still, false will be returned since the ifdef at the begining if this function. res = false; } } diff --git a/src/slic3r/Utils/WinRegistry.cpp b/src/slic3r/Utils/WinRegistry.cpp new file mode 100644 index 000000000..068a7fff1 --- /dev/null +++ b/src/slic3r/Utils/WinRegistry.cpp @@ -0,0 +1,490 @@ +#include "libslic3r/Technologies.hpp" +#include "WinRegistry.hpp" + +#ifdef _WIN32 +#include +#include +#include +#include + +namespace Slic3r { + +// Helper class which automatically closes the handle when +// going out of scope +class AutoHandle +{ + HANDLE m_handle{ nullptr }; + +public: + explicit AutoHandle(HANDLE handle) : m_handle(handle) {} + ~AutoHandle() { if (m_handle != nullptr) ::CloseHandle(m_handle); } + HANDLE get() { return m_handle; } +}; + +// Helper class which automatically closes the key when +// going out of scope +class AutoRegKey +{ + HKEY m_key{ nullptr }; + +public: + explicit AutoRegKey(HKEY key) : m_key(key) {} + ~AutoRegKey() { if (m_key != nullptr) ::RegCloseKey(m_key); } + HKEY get() { return m_key; } +}; + +// returns true if the given value is set/modified into Windows registry +static bool set_into_win_registry(HKEY hkeyHive, const wchar_t* pszVar, const wchar_t* pszValue) +{ + // see as reference: https://stackoverflow.com/questions/20245262/c-program-needs-an-file-association + wchar_t szValueCurrent[1000]; + DWORD dwType; + DWORD dwSize = sizeof(szValueCurrent); + + LSTATUS iRC = ::RegGetValueW(hkeyHive, pszVar, nullptr, RRF_RT_ANY, &dwType, szValueCurrent, &dwSize); + + const bool bDidntExist = iRC == ERROR_FILE_NOT_FOUND; + + if (iRC != ERROR_SUCCESS && !bDidntExist) + // an error occurred + return false; + + if (!bDidntExist) { + if (dwType != REG_SZ) + // invalid type + return false; + + if (::wcscmp(szValueCurrent, pszValue) == 0) + // value already set + return false; + } + + DWORD dwDisposition; + HKEY hkey; + iRC = ::RegCreateKeyExW(hkeyHive, pszVar, 0, 0, 0, KEY_ALL_ACCESS, nullptr, &hkey, &dwDisposition); + bool ret = false; + if (iRC == ERROR_SUCCESS) { + iRC = ::RegSetValueExW(hkey, L"", 0, REG_SZ, (BYTE*)pszValue, (::wcslen(pszValue) + 1) * sizeof(wchar_t)); + if (iRC == ERROR_SUCCESS) + ret = true; + } + + RegCloseKey(hkey); + return ret; +} + +static std::wstring get_current_user_string_sid() +{ + HANDLE rawProcessToken; + if (!::OpenProcessToken(::GetCurrentProcess(), TOKEN_QUERY, + &rawProcessToken)) + return L""; + + AutoHandle processToken(rawProcessToken); + + DWORD userSize = 0; + if (!(!::GetTokenInformation(processToken.get(), TokenUser, nullptr, 0, &userSize) && + GetLastError() == ERROR_INSUFFICIENT_BUFFER)) + return L""; + + std::vector userBytes(userSize); + if (!::GetTokenInformation(processToken.get(), TokenUser, userBytes.data(), userSize, &userSize)) + return L""; + + wchar_t* rawSid = nullptr; + if (!::ConvertSidToStringSidW(reinterpret_cast(userBytes.data())->User.Sid, &rawSid)) + return L""; + + return rawSid; +} + +/* + * Create the string which becomes the input to the UserChoice hash. + * + * @see generate_user_choice_hash() for parameters. + * + * @return The formatted string, empty string on failure. + * + * NOTE: This uses the format as of Windows 10 20H2 (latest as of this writing), + * used at least since 1803. + * There was at least one older version, not currently supported: On Win10 RTM + * (build 10240, aka 1507) the hash function is the same, but the timestamp and + * User Experience string aren't included; instead (for protocols) the string + * ends with the exe path. The changelog of SetUserFTA suggests the algorithm + * changed in 1703, so there may be two versions: before 1703, and 1703 to now. + */ +static std::wstring format_user_choice_string(const wchar_t* aExt, const wchar_t* aUserSid, const wchar_t* aProgId, SYSTEMTIME aTimestamp) +{ + aTimestamp.wSecond = 0; + aTimestamp.wMilliseconds = 0; + + FILETIME fileTime = { 0 }; + if (!::SystemTimeToFileTime(&aTimestamp, &fileTime)) + return L""; + + // This string is built into Windows as part of the UserChoice hash algorithm. + // It might vary across Windows SKUs (e.g. Windows 10 vs. Windows Server), or + // across builds of the same SKU, but this is the only currently known + // version. There isn't any known way of deriving it, so we assume this + // constant value. If we are wrong, we will not be able to generate correct + // UserChoice hashes. + const wchar_t* userExperience = + L"User Choice set via Windows User Experience " + L"{D18B6DD5-6124-4341-9318-804003BAFA0B}"; + + const wchar_t* userChoiceFmt = + L"%s%s%s" + L"%08lx" + L"%08lx" + L"%s"; + + int userChoiceLen = _scwprintf(userChoiceFmt, aExt, aUserSid, aProgId, + fileTime.dwHighDateTime, fileTime.dwLowDateTime, userExperience); + userChoiceLen += 1; // _scwprintf does not include the terminator + + std::wstring userChoice(userChoiceLen, L'\0'); + _snwprintf_s(userChoice.data(), userChoiceLen, _TRUNCATE, userChoiceFmt, aExt, + aUserSid, aProgId, fileTime.dwHighDateTime, fileTime.dwLowDateTime, userExperience); + + ::CharLowerW(userChoice.data()); + return userChoice; +} + +// @return The MD5 hash of the input, nullptr on failure. +static std::vector cng_md5(const unsigned char* bytes, ULONG bytesLen) { + constexpr ULONG MD5_BYTES = 16; + constexpr ULONG MD5_DWORDS = MD5_BYTES / sizeof(DWORD); + std::vector hash; + + BCRYPT_ALG_HANDLE hAlg = nullptr; + if (NT_SUCCESS(::BCryptOpenAlgorithmProvider(&hAlg, BCRYPT_MD5_ALGORITHM, nullptr, 0))) { + BCRYPT_HASH_HANDLE hHash = nullptr; + // As of Windows 7 the hash handle will manage its own object buffer when + // pbHashObject is nullptr and cbHashObject is 0. + if (NT_SUCCESS(::BCryptCreateHash(hAlg, &hHash, nullptr, 0, nullptr, 0, 0))) { + // BCryptHashData promises not to modify pbInput. + if (NT_SUCCESS(::BCryptHashData(hHash, const_cast(bytes), bytesLen, 0))) { + hash.resize(MD5_DWORDS); + if (!NT_SUCCESS(::BCryptFinishHash(hHash, reinterpret_cast(hash.data()), + MD5_DWORDS * sizeof(DWORD), 0))) + hash.clear(); + } + ::BCryptDestroyHash(hHash); + } + ::BCryptCloseAlgorithmProvider(hAlg, 0); + } + + return hash; +} + +static inline DWORD word_swap(DWORD v) +{ + return (v >> 16) | (v << 16); +} + +// @return The input bytes encoded as base64, nullptr on failure. +static std::wstring crypto_api_base64_encode(const unsigned char* bytes, DWORD bytesLen) { + DWORD base64Len = 0; + if (!::CryptBinaryToStringW(bytes, bytesLen, CRYPT_STRING_BASE64 | CRYPT_STRING_NOCRLF, nullptr, &base64Len)) + return L""; + + std::wstring base64(base64Len, L'\0'); + if (!::CryptBinaryToStringW(bytes, bytesLen, CRYPT_STRING_BASE64 | CRYPT_STRING_NOCRLF, base64.data(), &base64Len)) + return L""; + + return base64; +} + +/* + * Generate the UserChoice Hash. + * + * This implementation is based on the references listed above. + * It is organized to show the logic as clearly as possible, but at some + * point the reasoning is just "this is how it works". + * + * @param inputString A null-terminated string to hash. + * + * @return The base64-encoded hash, or empty string on failure. + */ +static std::wstring hash_string(const wchar_t* inputString) +{ + auto inputBytes = reinterpret_cast(inputString); + int inputByteCount = (::lstrlenW(inputString) + 1) * sizeof(wchar_t); + + constexpr size_t DWORDS_PER_BLOCK = 2; + constexpr size_t BLOCK_SIZE = sizeof(DWORD) * DWORDS_PER_BLOCK; + // Incomplete blocks are ignored. + int blockCount = inputByteCount / BLOCK_SIZE; + + if (blockCount == 0) + return L""; + + // Compute an MD5 hash. md5[0] and md5[1] will be used as constant multipliers + // in the scramble below. + auto md5 = cng_md5(inputBytes, inputByteCount); + if (md5.empty()) + return L""; + + // The following loop effectively computes two checksums, scrambled like a + // hash after every DWORD is added. + + // Constant multipliers for the scramble, one set for each DWORD in a block. + const DWORD C0s[DWORDS_PER_BLOCK][5] = { + {md5[0] | 1, 0xCF98B111uL, 0x87085B9FuL, 0x12CEB96DuL, 0x257E1D83uL}, + {md5[1] | 1, 0xA27416F5uL, 0xD38396FFuL, 0x7C932B89uL, 0xBFA49F69uL} }; + const DWORD C1s[DWORDS_PER_BLOCK][5] = { + {md5[0] | 1, 0xEF0569FBuL, 0x689B6B9FuL, 0x79F8A395uL, 0xC3EFEA97uL}, + {md5[1] | 1, 0xC31713DBuL, 0xDDCD1F0FuL, 0x59C3AF2DuL, 0x35BD1EC9uL} }; + + // The checksums. + DWORD h0 = 0; + DWORD h1 = 0; + // Accumulated total of the checksum after each DWORD. + DWORD h0Acc = 0; + DWORD h1Acc = 0; + + for (int i = 0; i < blockCount; ++i) { + for (int j = 0; j < DWORDS_PER_BLOCK; ++j) { + const DWORD* C0 = C0s[j]; + const DWORD* C1 = C1s[j]; + + DWORD input; + memcpy(&input, &inputBytes[(i * DWORDS_PER_BLOCK + j) * sizeof(DWORD)], sizeof(DWORD)); + + h0 += input; + // Scramble 0 + h0 *= C0[0]; + h0 = word_swap(h0) * C0[1]; + h0 = word_swap(h0) * C0[2]; + h0 = word_swap(h0) * C0[3]; + h0 = word_swap(h0) * C0[4]; + h0Acc += h0; + + h1 += input; + // Scramble 1 + h1 = word_swap(h1) * C1[1] + h1 * C1[0]; + h1 = (h1 >> 16) * C1[2] + h1 * C1[3]; + h1 = word_swap(h1) * C1[4] + h1; + h1Acc += h1; + } + } + + DWORD hash[2] = { h0 ^ h1, h0Acc ^ h1Acc }; + return crypto_api_base64_encode(reinterpret_cast(hash), sizeof(hash)); +} + +static std::wstring generate_user_choice_hash(const wchar_t* aExt, const wchar_t* aUserSid, const wchar_t* aProgId, SYSTEMTIME aTimestamp) +{ + const std::wstring userChoice = format_user_choice_string(aExt, aUserSid, aProgId, aTimestamp); + if (userChoice.empty()) + return L""; + + return hash_string(userChoice.c_str()); +} + +static bool add_milliseconds_to_system_time(SYSTEMTIME& aSystemTime, ULONGLONG aIncrementMS) +{ + FILETIME fileTime; + ULARGE_INTEGER fileTimeInt; + if (!::SystemTimeToFileTime(&aSystemTime, &fileTime)) + return false; + + fileTimeInt.LowPart = fileTime.dwLowDateTime; + fileTimeInt.HighPart = fileTime.dwHighDateTime; + + // FILETIME is in units of 100ns. + fileTimeInt.QuadPart += aIncrementMS * 1000 * 10; + + fileTime.dwLowDateTime = fileTimeInt.LowPart; + fileTime.dwHighDateTime = fileTimeInt.HighPart; + SYSTEMTIME tmpSystemTime; + if (!::FileTimeToSystemTime(&fileTime, &tmpSystemTime)) + return false; + + aSystemTime = tmpSystemTime; + return true; +} + +// Compare two SYSTEMTIMEs as FILETIME after clearing everything +// below minutes. +static bool check_equal_minutes(SYSTEMTIME aSystemTime1, SYSTEMTIME aSystemTime2) +{ + aSystemTime1.wSecond = 0; + aSystemTime1.wMilliseconds = 0; + + aSystemTime2.wSecond = 0; + aSystemTime2.wMilliseconds = 0; + + FILETIME fileTime1; + FILETIME fileTime2; + if (!::SystemTimeToFileTime(&aSystemTime1, &fileTime1) || !::SystemTimeToFileTime(&aSystemTime2, &fileTime2)) + return false; + + return (fileTime1.dwLowDateTime == fileTime2.dwLowDateTime) && (fileTime1.dwHighDateTime == fileTime2.dwHighDateTime); +} + +static std::wstring get_association_key_path(const wchar_t* aExt) +{ + const wchar_t* keyPathFmt; + if (aExt[0] == L'.') + keyPathFmt = L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Explorer\\FileExts\\%s"; + else + keyPathFmt = L"SOFTWARE\\Microsoft\\Windows\\Shell\\Associations\\UrlAssociations\\%s"; + + int keyPathLen = _scwprintf(keyPathFmt, aExt); + keyPathLen += 1; // _scwprintf does not include the terminator + + std::wstring keyPath(keyPathLen, '\0'); + _snwprintf_s(keyPath.data(), keyPathLen, _TRUNCATE, keyPathFmt, aExt); + return keyPath; +} + +/* + * Set an association with a UserChoice key + * + * Removes the old key, creates a new one with ProgID and Hash set to + * enable a new asociation. + * + * @param aExt File type or protocol to associate + * @param aProgID ProgID to use for the asociation + * + * @return true if successful, false on error. + */ +static bool set_user_choice(const wchar_t* aExt, const wchar_t* aProgID) { + + const std::wstring aSid = get_current_user_string_sid(); + if (aSid.empty()) + return false; + + SYSTEMTIME hashTimestamp; + ::GetSystemTime(&hashTimestamp); + std::wstring hash = generate_user_choice_hash(aExt, aSid.c_str(), aProgID, hashTimestamp); + if (hash.empty()) + return false; + + // The hash changes at the end of each minute, so check that the hash should + // be the same by the time we're done writing. + const ULONGLONG kWriteTimingThresholdMilliseconds = 100; + // Generating the hash could have taken some time, so start from now. + SYSTEMTIME writeEndTimestamp; + ::GetSystemTime(&writeEndTimestamp); + if (!add_milliseconds_to_system_time(writeEndTimestamp, +kWriteTimingThresholdMilliseconds)) + return false; + + if (!check_equal_minutes(hashTimestamp, writeEndTimestamp)) { + ::Sleep(kWriteTimingThresholdMilliseconds * 2); + + // For consistency, use the current time. + ::GetSystemTime(&hashTimestamp); + hash = generate_user_choice_hash(aExt, aSid.c_str(), aProgID, hashTimestamp); + if (hash.empty()) + return false; + } + + const std::wstring assocKeyPath = get_association_key_path(aExt); + if (assocKeyPath.empty()) + return false; + + LSTATUS ls; + HKEY rawAssocKey; + ls = ::RegOpenKeyExW(HKEY_CURRENT_USER, assocKeyPath.data(), 0, KEY_READ | KEY_WRITE, &rawAssocKey); + if (ls != ERROR_SUCCESS) + return false; + + AutoRegKey assocKey(rawAssocKey); + + HKEY currUserChoiceKey; + ls = ::RegOpenKeyExW(assocKey.get(), L"UserChoice", 0, KEY_READ, &currUserChoiceKey); + if (ls == ERROR_SUCCESS) { + ::RegCloseKey(currUserChoiceKey); + // When Windows creates this key, it is read-only (Deny Set Value), so we need + // to delete it first. + // We don't set any similar special permissions. + ls = ::RegDeleteKeyW(assocKey.get(), L"UserChoice"); + if (ls != ERROR_SUCCESS) + return false; + } + + HKEY rawUserChoiceKey; + ls = ::RegCreateKeyExW(assocKey.get(), L"UserChoice", 0, nullptr, + 0 /* options */, KEY_READ | KEY_WRITE, + 0 /* security attributes */, &rawUserChoiceKey, + nullptr); + if (ls != ERROR_SUCCESS) + return false; + + AutoRegKey userChoiceKey(rawUserChoiceKey); + DWORD progIdByteCount = (::lstrlenW(aProgID) + 1) * sizeof(wchar_t); + ls = ::RegSetValueExW(userChoiceKey.get(), L"ProgID", 0, REG_SZ, reinterpret_cast(aProgID), progIdByteCount); + if (ls != ERROR_SUCCESS) + return false; + + DWORD hashByteCount = (::lstrlenW(hash.data()) + 1) * sizeof(wchar_t); + ls = ::RegSetValueExW(userChoiceKey.get(), L"Hash", 0, REG_SZ, reinterpret_cast(hash.data()), hashByteCount); + if (ls != ERROR_SUCCESS) + return false; + + return true; +} + +static bool set_as_default_per_file_type(const std::wstring& extension, const std::wstring& prog_id) +{ + const std::wstring reg_extension = get_association_key_path(extension.c_str()); + if (reg_extension.empty()) + return false; + + bool needs_update = true; + bool modified = false; + HKEY rawAssocKey = nullptr; + LSTATUS res = ::RegOpenKeyExW(HKEY_CURRENT_USER, reg_extension.c_str(), 0, KEY_READ, &rawAssocKey); + AutoRegKey assoc_key(rawAssocKey); + if (res == ERROR_SUCCESS) { + DWORD data_size_bytes = 0; + res = ::RegGetValueW(assoc_key.get(), L"UserChoice", L"ProgId", RRF_RT_REG_SZ, nullptr, nullptr, &data_size_bytes); + if (res == ERROR_SUCCESS) { + // +1 in case dataSizeBytes was odd, +1 to ensure termination + DWORD data_size_chars = (data_size_bytes / sizeof(wchar_t)) + 2; + std::wstring curr_prog_id(data_size_chars, L'\0'); + res = ::RegGetValueW(assoc_key.get(), L"UserChoice", L"ProgId", RRF_RT_REG_SZ, nullptr, curr_prog_id.data(), &data_size_bytes); + if (res == ERROR_SUCCESS) { + const std::wstring::size_type pos = curr_prog_id.find_first_of(L'\0'); + if (pos != std::wstring::npos) + curr_prog_id = curr_prog_id.substr(0, pos); + needs_update = !boost::algorithm::iequals(curr_prog_id, prog_id); + } + } + } + + if (needs_update) + modified = set_user_choice(extension.c_str(), prog_id.c_str()); + + return modified; +} + +void associate_file_type(const std::wstring& extension, const std::wstring& prog_id, const std::wstring& prog_desc, bool set_as_default) +{ + assert(!extension.empty() && extension.front() == L'.'); + + const std::wstring reg_extension = L"SOFTWARE\\Classes\\" + extension; + const std::wstring reg_prog_id = L"SOFTWARE\\Classes\\" + prog_id; + const std::wstring reg_prog_id_command = L"SOFTWARE\\Classes\\" + prog_id + +L"\\Shell\\Open\\Command"; + + wchar_t app_path[1040]; + ::GetModuleFileNameW(nullptr, app_path, sizeof(app_path)); + const std::wstring prog_command = L"\"" + std::wstring(app_path) + L"\"" + L" \"%1\""; + + bool modified = false; + modified |= set_into_win_registry(HKEY_CURRENT_USER, reg_extension.c_str(), prog_id.c_str()); + modified |= set_into_win_registry(HKEY_CURRENT_USER, reg_prog_id.c_str(), prog_desc.c_str()); + modified |= set_into_win_registry(HKEY_CURRENT_USER, reg_prog_id_command.c_str(), prog_command.c_str()); + if (set_as_default) + modified |= set_as_default_per_file_type(extension, prog_id); + + // notify Windows only when any of the values gets changed + if (modified) + ::SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_IDLIST, nullptr, nullptr); +} + +} // namespace Slic3r + +#endif // _WIN32 diff --git a/src/slic3r/Utils/WinRegistry.hpp b/src/slic3r/Utils/WinRegistry.hpp new file mode 100644 index 000000000..bc0875d32 --- /dev/null +++ b/src/slic3r/Utils/WinRegistry.hpp @@ -0,0 +1,23 @@ +#ifndef slic3r_Utils_WinRegistry_hpp_ +#define slic3r_Utils_WinRegistry_hpp_ + +#ifdef _WIN32 + +#include + +namespace Slic3r { + +// Creates a Windows registry key for the files with the given 'extension' and associates them to the application 'prog_id'. +// If 'set_as_default' is true, the application 'prog_id' is set ad default application for the file type 'extension'. +// The file type registration implementation is based on code taken from: +// https://stackoverflow.com/questions/20245262/c-program-needs-an-file-association +// The set as default implementation is based on code taken from: +// https://hg.mozilla.org/mozilla-central/rev/e928b3e95a6c3b7257d0ba475fc2303bfbad1874 +// https://hg.mozilla.org/releases/mozilla-release/diff/7e775ce432b599c6daf7ac379aa42f1e9b3b33ed/browser/components/shell/WindowsUserChoice.cpp +void associate_file_type(const std::wstring& extension, const std::wstring& prog_id, const std::wstring& prog_desc, bool set_as_default); + +} // namespace Slic3r + +#endif // _WIN32 + +#endif // slic3r_Utils_WinRegistry_hpp_ diff --git a/tests/fff_print/CMakeLists.txt b/tests/fff_print/CMakeLists.txt index 4e8821287..0b3b920b9 100644 --- a/tests/fff_print/CMakeLists.txt +++ b/tests/fff_print/CMakeLists.txt @@ -4,6 +4,7 @@ add_executable(${_TEST_NAME}_tests test_avoid_crossing_perimeters.cpp test_bridges.cpp test_cooling.cpp + test_clipper.cpp test_custom_gcode.cpp test_data.cpp test_data.hpp diff --git a/tests/fff_print/test_clipper.cpp b/tests/fff_print/test_clipper.cpp new file mode 100644 index 000000000..dc49ce0f1 --- /dev/null +++ b/tests/fff_print/test_clipper.cpp @@ -0,0 +1,43 @@ +#include + +#include "test_data.hpp" +#include "clipper/clipper_z.hpp" + +using namespace Slic3r; + +// Test case for an issue with duplicity vertices (same XY coordinates but differ in Z coordinates) in Clipper 6.2.9, +// (related to https://sourceforge.net/p/polyclipping/bugs/160/) that was fixed in Clipper 6.4.2. +SCENARIO("Clipper Z", "[ClipperZ]") +{ + ClipperLib_Z::Path subject; + + subject.emplace_back(-2000, -1000, 10); + subject.emplace_back(-2000, 1000, 10); + subject.emplace_back( 2000, 1000, 10); + subject.emplace_back( 2000, -1000, 10); + + ClipperLib_Z::Path clip; + clip.emplace_back(-1000, -2000, -5); + clip.emplace_back(-1000, 2000, -5); + clip.emplace_back( 1000, 2000, -5); + clip.emplace_back( 1000, -2000, -5); + + ClipperLib_Z::Clipper clipper; + clipper.ZFillFunction([](const ClipperLib_Z::IntPoint &e1bot, const ClipperLib_Z::IntPoint &e1top, const ClipperLib_Z::IntPoint &e2bot, + const ClipperLib_Z::IntPoint &e2top, ClipperLib_Z::IntPoint &pt) { + pt.z() = 1; + }); + + clipper.AddPath(subject, ClipperLib_Z::ptSubject, false); + clipper.AddPath(clip, ClipperLib_Z::ptClip, true); + + ClipperLib_Z::PolyTree polytree; + ClipperLib_Z::Paths paths; + clipper.Execute(ClipperLib_Z::ctIntersection, polytree, ClipperLib_Z::pftNonZero, ClipperLib_Z::pftNonZero); + ClipperLib_Z::PolyTreeToPaths(polytree, paths); + + REQUIRE(paths.size() == 1); + REQUIRE(paths.front().size() == 2); + for (const ClipperLib_Z::IntPoint &pt : paths.front()) + REQUIRE(pt.z() == 1); +} \ No newline at end of file diff --git a/tests/libnest2d/CMakeLists.txt b/tests/libnest2d/CMakeLists.txt index 9bafe84a0..ea4f4255e 100644 --- a/tests/libnest2d/CMakeLists.txt +++ b/tests/libnest2d/CMakeLists.txt @@ -1,6 +1,8 @@ get_filename_component(_TEST_NAME ${CMAKE_CURRENT_LIST_DIR} NAME) add_executable(${_TEST_NAME}_tests ${_TEST_NAME}_tests_main.cpp printer_parts.cpp printer_parts.hpp) -target_link_libraries(${_TEST_NAME}_tests test_common libnest2d ) + +# mold linker for successful linking needs also to link TBB library and link it before libslic3r. +target_link_libraries(${_TEST_NAME}_tests test_common TBB::tbb libnest2d ) set_property(TARGET ${_TEST_NAME}_tests PROPERTY FOLDER "tests") # catch_discover_tests(${_TEST_NAME}_tests TEST_PREFIX "${_TEST_NAME}: ") diff --git a/tests/libslic3r/test_aabbindirect.cpp b/tests/libslic3r/test_aabbindirect.cpp index 3b834c442..87cf80943 100644 --- a/tests/libslic3r/test_aabbindirect.cpp +++ b/tests/libslic3r/test_aabbindirect.cpp @@ -3,6 +3,7 @@ #include #include +#include using namespace Slic3r; @@ -58,3 +59,287 @@ TEST_CASE("Building a tree over a box, ray caster and closest query", "[AABBIndi REQUIRE(closest_point.y() == Approx(0.5)); REQUIRE(closest_point.z() == Approx(1.)); } + +TEST_CASE("Creating a several 2d lines, testing closest point query", "[AABBIndirect]") +{ + std::vector lines { }; + lines.push_back(Linef(Vec2d(0.0, 0.0), Vec2d(1.0, 0.0))); + lines.push_back(Linef(Vec2d(1.0, 0.0), Vec2d(1.0, 1.0))); + lines.push_back(Linef(Vec2d(1.0, 1.0), Vec2d(0.0, 1.0))); + lines.push_back(Linef(Vec2d(0.0, 1.0), Vec2d(0.0, 0.0))); + + auto tree = AABBTreeLines::build_aabb_tree_over_indexed_lines(lines); + + size_t hit_idx_out; + Vec2d hit_point_out; + auto sqr_dist = AABBTreeLines::squared_distance_to_indexed_lines(lines, tree, Vec2d(0.0, 0.0), hit_idx_out, + hit_point_out); + REQUIRE(sqr_dist == Approx(0.0)); + REQUIRE((hit_idx_out == 0 || hit_idx_out == 3)); + REQUIRE(hit_point_out.x() == Approx(0.0)); + REQUIRE(hit_point_out.y() == Approx(0.0)); + + sqr_dist = AABBTreeLines::squared_distance_to_indexed_lines(lines, tree, Vec2d(1.5, 0.5), hit_idx_out, + hit_point_out); + REQUIRE(sqr_dist == Approx(0.25)); + REQUIRE(hit_idx_out == 1); + REQUIRE(hit_point_out.x() == Approx(1.0)); + REQUIRE(hit_point_out.y() == Approx(0.5)); +} + +#if 0 +#include "libslic3r/EdgeGrid.hpp" +#include +#include +#include +#include +TEST_CASE("AABBTreeLines vs SignedDistanceGrid time Benchmark", "[AABBIndirect]") +{ + std::vector lines { Points { } }; + std::vector linesf { }; + Vec2d prevf { }; + + // NOTE: max coord value of the lines is approx 83 mm + for (int r = 1; r < 1000; ++r) { + lines[0].push_back(Point::new_scale(Vec2d(exp(0.005f * r) * cos(r), exp(0.005f * r) * cos(r)))); + linesf.emplace_back(prevf, Vec2d(exp(0.005f * r) * cos(r), exp(0.005f * r) * cos(r))); + prevf = linesf.back().b; + } + + int build_num = 10000; + using namespace std::chrono; + { + std::cout << "building the tree " << build_num << " times..." << std::endl; + high_resolution_clock::time_point t1 = high_resolution_clock::now(); + for (int i = 0; i < build_num; ++i) { + volatile auto tree = AABBTreeLines::build_aabb_tree_over_indexed_lines(linesf); + } + high_resolution_clock::time_point t2 = high_resolution_clock::now(); + + duration time_span = duration_cast>(t2 - t1); + std::cout << "It took " << time_span.count() << " seconds." << std::endl << std::endl; + + } + { + std::cout << "building the grid res 1mm ONLY " << build_num/100 << " !!! times..." << std::endl; + high_resolution_clock::time_point t1 = high_resolution_clock::now(); + for (int i = 0; i < build_num/100; ++i) { + EdgeGrid::Grid grid { }; + grid.create(lines, scaled(1.0), true); + grid.calculate_sdf(); + } + high_resolution_clock::time_point t2 = high_resolution_clock::now(); + duration time_span = duration_cast>(t2 - t1); + std::cout << "It took " << time_span.count() << " seconds." << std::endl << std::endl; + } + { + std::cout << "building the grid res 10mm " << build_num << " times..." << std::endl; + high_resolution_clock::time_point t1 = high_resolution_clock::now(); + for (int i = 0; i < build_num; ++i) { + EdgeGrid::Grid grid { }; + grid.create(lines, scaled(10.0), true); + grid.calculate_sdf(); + } + high_resolution_clock::time_point t2 = high_resolution_clock::now(); + duration time_span = duration_cast>(t2 - t1); + std::cout << "It took " << time_span.count() << " seconds." << std::endl << std::endl; + } + + EdgeGrid::Grid grid10 { }; + grid10.create(lines, scaled(10.0), true); + coord_t query10_res = scaled(10.0); + grid10.calculate_sdf(); + + EdgeGrid::Grid grid1 { }; + grid1.create(lines, scaled(1.0), true); + coord_t query1_res = scaled(1.0); + grid1.calculate_sdf(); + + auto tree = AABBTreeLines::build_aabb_tree_over_indexed_lines(linesf); + + int query_num = 10000; + Points query_points { }; + std::vector query_pointsf { }; + for (int x = 0; x < query_num; ++x) { + Vec2d qp { rand() / (double(RAND_MAX) + 1.0f) * 200.0 - 100.0, rand() / (double(RAND_MAX) + 1.0f) * 200.0 + - 100.0 }; + query_pointsf.push_back(qp); + query_points.push_back(Point::new_scale(qp)); + } + + { + std::cout << "querying tree " << query_num << " times..." << std::endl; + high_resolution_clock::time_point t1 = high_resolution_clock::now(); + for (const Vec2d &qp : query_pointsf) { + size_t hit_idx_out; + Vec2d hit_point_out; + AABBTreeLines::squared_distance_to_indexed_lines(linesf, tree, qp, hit_idx_out, hit_point_out); + } + high_resolution_clock::time_point t2 = high_resolution_clock::now(); + duration time_span = duration_cast>(t2 - t1); + std::cout << "It took " << time_span.count() << " seconds." << std::endl << std::endl; + } + + { + std::cout << "querying grid res 1mm " << query_num << " times..." << std::endl; + high_resolution_clock::time_point t1 = high_resolution_clock::now(); + for (const Point &qp : query_points) { + volatile auto dist = grid1.closest_point_signed_distance(qp, query1_res); + } + high_resolution_clock::time_point t2 = high_resolution_clock::now(); + duration time_span = duration_cast>(t2 - t1); + std::cout << "It took " << time_span.count() << " seconds." << std::endl << std::endl; + } + + { + std::cout << "querying grid res 10mm " << query_num << " times..." << std::endl; + high_resolution_clock::time_point t1 = high_resolution_clock::now(); + for (const Point &qp : query_points) { + volatile auto dist = grid10.closest_point_signed_distance(qp, query10_res); + } + high_resolution_clock::time_point t2 = high_resolution_clock::now(); + duration time_span = duration_cast>(t2 - t1); + std::cout << "It took " << time_span.count() << " seconds." << std::endl << std::endl; + } + + std::cout << "Test build and queries together - same number of contour points and query points" << std::endl << std::endl; + + std::vector point_counts { 100, 300, 500, 1000, 3000 }; + for (auto count : point_counts) { + + std::vector lines { Points { } }; + std::vector linesf { }; + Vec2d prevf { }; + Points query_points { }; + std::vector query_pointsf { }; + + for (int x = 0; x < count; ++x) { + Vec2d cp { rand() / (double(RAND_MAX) + 1.0f) * 200.0 - 100.0, rand() / (double(RAND_MAX) + 1.0f) * 200.0 + - 100.0 }; + lines[0].push_back(Point::new_scale(cp)); + linesf.emplace_back(prevf, cp); + prevf = linesf.back().b; + + Vec2d qp { rand() / (double(RAND_MAX) + 1.0f) * 200.0 - 100.0, rand() / (double(RAND_MAX) + 1.0f) * 200.0 + - 100.0 }; + query_pointsf.push_back(qp); + query_points.push_back(Point::new_scale(qp)); + } + + std::cout << "Test for point count: " << count << std::endl; + { + high_resolution_clock::time_point t1 = high_resolution_clock::now(); + auto tree = AABBTreeLines::build_aabb_tree_over_indexed_lines(linesf); + for (const Vec2d &qp : query_pointsf) { + size_t hit_idx_out; + Vec2d hit_point_out; + AABBTreeLines::squared_distance_to_indexed_lines(linesf, tree, qp, hit_idx_out, hit_point_out); + } + high_resolution_clock::time_point t2 = high_resolution_clock::now(); + duration time_span = duration_cast>(t2 - t1); + std::cout << " Tree took " << time_span.count() << " seconds." << std::endl; + } + + { + high_resolution_clock::time_point t1 = high_resolution_clock::now(); + EdgeGrid::Grid grid1 { }; + grid1.create(lines, scaled(1.0), true); + coord_t query1_res = scaled(1.0); + grid1.calculate_sdf(); + for (const Point &qp : query_points) { + volatile auto dist = grid1.closest_point_signed_distance(qp, query1_res); + } + high_resolution_clock::time_point t2 = high_resolution_clock::now(); + duration time_span = duration_cast>(t2 - t1); + std::cout << " Grid 1mm took " << time_span.count() << " seconds." << std::endl; + } + + { + high_resolution_clock::time_point t1 = high_resolution_clock::now(); + EdgeGrid::Grid grid10 { }; + grid10.create(lines, scaled(10.0), true); + coord_t query10_res = scaled(10.0); + grid10.calculate_sdf(); + for (const Point &qp : query_points) { + volatile auto dist = grid10.closest_point_signed_distance(qp, query10_res); + } + high_resolution_clock::time_point t2 = high_resolution_clock::now(); + duration time_span = duration_cast>(t2 - t1); + std::cout << " Grid 10mm took " << time_span.count() << " seconds." << std::endl; + } + + } + + + + std::cout << "Test build and queries together - same number of contour points and query points" << std::endl << + "And with limited contour edge length to 4mm " << std::endl; + for (auto count : point_counts) { + + std::vector lines { Points { } }; + std::vector linesf { }; + Vec2d prevf { }; + Points query_points { }; + std::vector query_pointsf { }; + + for (int x = 0; x < count; ++x) { + Vec2d cp { rand() / (double(RAND_MAX) + 1.0f) * 200.0 - 100.0, rand() / (double(RAND_MAX) + 1.0f) * 200.0 + - 100.0 }; + Vec2d contour = prevf + cp.normalized()*4.0; // limits the cnotour edge len to 4mm + lines[0].push_back(Point::new_scale(contour)); + linesf.emplace_back(prevf, contour); + prevf = linesf.back().b; + + Vec2d qp { rand() / (double(RAND_MAX) + 1.0f) * 200.0 - 100.0, rand() / (double(RAND_MAX) + 1.0f) * 200.0 + - 100.0 }; + query_pointsf.push_back(qp); + query_points.push_back(Point::new_scale(qp)); + } + + std::cout << "Test for point count: " << count << std::endl; + { + high_resolution_clock::time_point t1 = high_resolution_clock::now(); + auto tree = AABBTreeLines::build_aabb_tree_over_indexed_lines(linesf); + for (const Vec2d &qp : query_pointsf) { + size_t hit_idx_out; + Vec2d hit_point_out; + AABBTreeLines::squared_distance_to_indexed_lines(linesf, tree, qp, hit_idx_out, hit_point_out); + } + high_resolution_clock::time_point t2 = high_resolution_clock::now(); + duration time_span = duration_cast>(t2 - t1); + std::cout << " Tree took " << time_span.count() << " seconds." << std::endl; + } + + { + high_resolution_clock::time_point t1 = high_resolution_clock::now(); + EdgeGrid::Grid grid1 { }; + grid1.create(lines, scaled(1.0), true); + coord_t query1_res = scaled(1.0); + grid1.calculate_sdf(); + for (const Point &qp : query_points) { + volatile auto dist = grid1.closest_point_signed_distance(qp, query1_res); + } + high_resolution_clock::time_point t2 = high_resolution_clock::now(); + duration time_span = duration_cast>(t2 - t1); + std::cout << " Grid 1mm took " << time_span.count() << " seconds." << std::endl; + } + + { + high_resolution_clock::time_point t1 = high_resolution_clock::now(); + EdgeGrid::Grid grid10 { }; + grid10.create(lines, scaled(10.0), true); + coord_t query10_res = scaled(10.0); + grid10.calculate_sdf(); + for (const Point &qp : query_points) { + volatile auto dist = grid10.closest_point_signed_distance(qp, query10_res); + } + high_resolution_clock::time_point t2 = high_resolution_clock::now(); + duration time_span = duration_cast>(t2 - t1); + std::cout << " Grid 10mm took " << time_span.count() << " seconds." << std::endl; + } + + } + +} +#endif + diff --git a/tests/libslic3r/test_astar.cpp b/tests/libslic3r/test_astar.cpp index 176d7694a..867f5be47 100644 --- a/tests/libslic3r/test_astar.cpp +++ b/tests/libslic3r/test_astar.cpp @@ -7,12 +7,42 @@ using namespace Slic3r; -struct PointGridTracer { +TEST_CASE("Testing basic invariants of AStar", "[AStar]") { + struct DummyTracer { + using Node = int; + + int goal = 0; + + float distance(int a, int b) const { return a - b; } + + float goal_heuristic(int n) const { return n == goal ? -1.f : 0.f; } + + size_t unique_id(int n) const { return n; } + + void foreach_reachable(int, std::function) const {} + }; + + std::vector out; + + SECTION("Output is empty when source is also the destination") { + bool found = astar::search_route(DummyTracer{}, 0, std::back_inserter(out)); + REQUIRE(out.empty()); + REQUIRE(found); + } + + SECTION("Return false when there is no route to destination") { + bool found = astar::search_route(DummyTracer{}, 1, std::back_inserter(out)); + REQUIRE(!found); + REQUIRE(out.empty()); + } +} + +struct PointGridTracer3D { using Node = size_t; const PointGrid &grid; size_t final; - PointGridTracer(const PointGrid &g, size_t goal) : + PointGridTracer3D(const PointGrid &g, size_t goal) : grid{g}, final{goal} {} template @@ -49,6 +79,15 @@ struct PointGridTracer { size_t unique_id(size_t n) const { return n; } }; +template> +bool has_duplicates(const std::vector &res, Cmp cmp = {}) +{ + auto cpy = res; + std::sort(cpy.begin(), cpy.end(), cmp); + auto it = std::unique(cpy.begin(), cpy.end()); + return it != cpy.end(); +} + TEST_CASE("astar algorithm test over 3D point grid", "[AStar]") { auto vol = BoundingBox3Base{{0.f, 0.f, 0.f}, {1.f, 1.f, 1.f}}; @@ -56,16 +95,312 @@ TEST_CASE("astar algorithm test over 3D point grid", "[AStar]") { size_t target = pgrid.point_count() - 1; - std::cout << "Tracing route to " << pgrid.get_coord(target).transpose() << std::endl; - PointGridTracer pgt{pgrid, pgrid.point_count() - 1}; + PointGridTracer3D pgt{pgrid, target}; std::vector out; - bool found = astar::search_route(pgt, size_t(0), std::back_inserter(out)); - - std::cout << "Route taken: "; - for (size_t i : out) { - std::cout << "(" << pgrid.get_coord(i).transpose() << ") "; - } - std::cout << std::endl; + bool found = astar::search_route(pgt, 0, std::back_inserter(out)); REQUIRE(found); + REQUIRE(!out.empty()); + REQUIRE(out.front() == target); + +#ifndef NDEBUG + std::cout << "Route taken: "; + for (auto it = out.rbegin(); it != out.rend(); ++it) { + std::cout << "(" << pgrid.get_coord(*it).transpose() << ") "; + } + std::cout << std::endl; +#endif + + REQUIRE(!has_duplicates(out)); // No duplicates in output +} + +enum CellValue {ON, OFF}; + +struct CellGridTracer2D_AllDirs { + using Node = Vec2i; + + static constexpr auto Cols = size_t(5); + static constexpr auto Rows = size_t(8); + static constexpr size_t GridSize = Cols * Rows; + + const std::array, Rows> &grid; + Vec2i goal; + + CellGridTracer2D_AllDirs(const std::array, Rows> &g, + const Vec2i &goal_) + : grid{g}, goal{goal_} + {} + + template + void foreach_reachable(const Vec2i &src, Fn &&fn) const + { + auto is_inside = [](const Vec2i& v) { return v.x() >= 0 && v.x() < Cols && v.y() >= 0 && v.y() < Rows; }; + if (Vec2i crd = src + Vec2i{0, 1}; is_inside(crd) && grid[crd.y()] [crd.x()] == ON) fn(crd); + if (Vec2i crd = src + Vec2i{1, 0}; is_inside(crd) && grid[crd.y()] [crd.x()] == ON) fn(crd); + if (Vec2i crd = src + Vec2i{1, 1}; is_inside(crd) && grid[crd.y()] [crd.x()] == ON) fn(crd); + if (Vec2i crd = src + Vec2i{0, -1}; is_inside(crd) && grid[crd.y()] [crd.x()] == ON) fn(crd); + if (Vec2i crd = src + Vec2i{-1, 0}; is_inside(crd) && grid[crd.y()] [crd.x()] == ON) fn(crd); + if (Vec2i crd = src + Vec2i{-1, -1}; is_inside(crd) && grid[crd.y()] [crd.x()] == ON) fn(crd); + if (Vec2i crd = src + Vec2i{1, -1}; is_inside(crd) && grid[crd.y()] [crd.x()] == ON) fn(crd); + if (Vec2i crd = src + Vec2i{-1, 1}; is_inside(crd) && grid[crd.y()] [crd.x()] == ON) fn(crd); + } + + float distance(const Vec2i & a, const Vec2i & b) const { return (a - b).squaredNorm(); } + + float goal_heuristic(const Vec2i & n) const { return n == goal ? -1.f : (n - goal).squaredNorm(); } + + size_t unique_id(const Vec2i & n) const { return n.y() * Cols + n.x(); } +}; + +struct CellGridTracer2D_Axis { + using Node = Vec2i; + + static constexpr auto Cols = size_t(5); + static constexpr auto Rows = size_t(8); + static constexpr size_t GridSize = Cols * Rows; + + const std::array, Rows> &grid; + Vec2i goal; + + CellGridTracer2D_Axis( + const std::array, Rows> &g, + const Vec2i &goal_) + : grid{g}, goal{goal_} + {} + + template + void foreach_reachable(const Vec2i &src, Fn &&fn) const + { + auto is_inside = [](const Vec2i& v) { return v.x() >= 0 && v.x() < Cols && v.y() >= 0 && v.y() < Rows; }; + if (Vec2i crd = src + Vec2i{0, 1}; is_inside(crd) && grid[crd.y()] [crd.x()] == ON) fn(crd); + if (Vec2i crd = src + Vec2i{0, -1}; is_inside(crd) && grid[crd.y()] [crd.x()] == ON) fn(crd); + if (Vec2i crd = src + Vec2i{1, 0}; is_inside(crd) && grid[crd.y()] [crd.x()] == ON) fn(crd); + if (Vec2i crd = src + Vec2i{-1, 0}; is_inside(crd) && grid[crd.y()] [crd.x()] == ON) fn(crd); + } + + float distance(const Vec2i & a, const Vec2i & b) const { return (a - b).squaredNorm(); } + + float goal_heuristic(const Vec2i &n) const + { + int manhattan_dst = std::abs(n.x() - goal.x()) + + std::abs(n.y() - goal.y()); + + return n == goal ? -1.f : manhattan_dst; + } + + size_t unique_id(const Vec2i & n) const { return n.y() * Cols + n.x(); } +}; + +using TestClasses = std::tuple< CellGridTracer2D_AllDirs, CellGridTracer2D_Axis >; + +TEMPLATE_LIST_TEST_CASE("Astar should avoid simple barrier", "[AStar]", TestClasses) { + + std::array, 8> grid = {{ + {ON , ON , ON , ON , ON}, + {ON , ON , ON , ON , ON}, + {ON , ON , ON , ON , ON}, + {ON , ON , ON , ON , ON}, + {ON , ON , ON , ON , ON}, + {ON , OFF, OFF, OFF, ON}, + {ON , ON , ON , ON , ON}, + {ON , ON , ON , ON , ON} + }}; + + Vec2i dst = {2, 0}; + TestType cgt{grid, dst}; + + std::vector out; + bool found = astar::search_route(cgt, {2, 7}, std::back_inserter(out)); + + REQUIRE(found); + REQUIRE(!out.empty()); + REQUIRE(out.front() == dst); + REQUIRE(!has_duplicates(out, [](const Vec2i &a, const Vec2i &b) { + return a.x() == b.x() ? a.y() < b.y() : a.x() < b.x(); + })); + +#ifndef NDEBUG + std::cout << "Route taken: "; + for (auto it = out.rbegin(); it != out.rend(); ++it) { + std::cout << "(" << it->transpose() << ") "; + } + std::cout << std::endl; +#endif +} + +TEMPLATE_LIST_TEST_CASE("Astar should manage to avoid arbitrary barriers", "[AStar]", TestClasses) { + + std::array, 8> grid = {{ + {ON , ON , ON , ON , ON}, + {ON , ON , ON , OFF, ON}, + {OFF, OFF, ON , OFF, ON}, + {ON , ON , ON , OFF, ON}, + {ON , OFF, ON , OFF, ON}, + {ON , OFF, ON , ON , ON}, + {ON , OFF, ON , OFF, ON}, + {ON , ON , ON , ON , ON} + }}; + + Vec2i dst = {0, 0}; + TestType cgt{grid, dst}; + + std::vector out; + bool found = astar::search_route(cgt, {0, 7}, std::back_inserter(out)); + + REQUIRE(found); + REQUIRE(!out.empty()); + REQUIRE(out.front() == dst); + REQUIRE(!has_duplicates(out, [](const Vec2i &a, const Vec2i &b) { + return a.x() == b.x() ? a.y() < b.y() : a.x() < b.x(); + })); + +#ifndef NDEBUG + std::cout << "Route taken: "; + for (auto it = out.rbegin(); it != out.rend(); ++it) { + std::cout << "(" << it->transpose() << ") "; + } + std::cout << std::endl; +#endif +} + +TEMPLATE_LIST_TEST_CASE("Astar should find the way out of a labyrinth", "[AStar]", TestClasses) { + + std::array, 8> grid = {{ + {ON , ON , ON , ON , ON }, + {ON , OFF, OFF, OFF, OFF}, + {ON , ON , ON , ON , ON }, + {OFF, OFF, OFF, OFF, ON }, + {ON , ON , ON , ON , ON }, + {ON , OFF, OFF, OFF, OFF}, + {ON , ON , ON , ON , ON }, + {OFF, OFF, OFF, OFF, ON } + }}; + + Vec2i dst = {4, 0}; + TestType cgt{grid, dst}; + + std::vector out; + bool found = astar::search_route(cgt, {4, 7}, std::back_inserter(out)); + + REQUIRE(found); + REQUIRE(!out.empty()); + REQUIRE(out.front() == dst); + REQUIRE(!has_duplicates(out, [](const Vec2i &a, const Vec2i &b) { + return a.x() == b.x() ? a.y() < b.y() : a.x() < b.x(); + })); + +#ifndef NDEBUG + std::cout << "Route taken: "; + for (auto it = out.rbegin(); it != out.rend(); ++it) { + std::cout << "(" << it->transpose() << ") "; + } + std::cout << std::endl; +#endif +} + +TEST_CASE("Zero heuristic function should result in dijsktra's algo", "[AStar]") +{ + struct GraphTracer { + using Node = size_t; + using QNode = astar::QNode; + + struct Edge + { + size_t to_id = size_t(-1); + float cost = 0.f; + bool operator <(const Edge &e) const { return to_id < e.to_id; } + }; + + struct ENode: public QNode { + std::vector edges; + + ENode(size_t node_id, std::initializer_list edgelist) + : QNode{node_id}, edges(edgelist) + {} + + ENode &operator=(const QNode &q) + { + assert(node == q.node); + g = q.g; + h = q.h; + parent = q.parent; + queue_id = q.queue_id; + + return *this; + } + }; + + // Example graph from + // https://www.geeksforgeeks.org/dijkstras-shortest-path-algorithm-greedy-algo-7/?ref=lbp + std::vector nodes = { + {0, {{1, 4.f}, {7, 8.f}}}, + {1, {{0, 4.f}, {2, 8.f}, {7, 11.f}}}, + {2, {{1, 8.f}, {3, 7.f}, {5, 4.f}, {8, 2.f}}}, + {3, {{2, 7.f}, {4, 9.f}, {5, 14.f}}}, + {4, {{3, 9.f}, {5, 10.f}}}, + {5, {{2, 4.f}, {3, 14.f}, {4, 10.f}, {6, 2.f}}}, + {6, {{5, 2.f}, {7, 1.f}, {8, 6.f}}}, + {7, {{0, 8.f}, {1, 11.f}, {6, 1.f}, {8, 7.f}}}, + {8, {{2, 2.f}, {6, 6.f}, {7, 7.f}}} + }; + + float distance(size_t a, size_t b) const { + float ret = std::numeric_limits::infinity(); + if (a < nodes.size()) { + auto it = std::lower_bound(nodes[a].edges.begin(), + nodes[a].edges.end(), + Edge{b, 0.f}); + + if (it != nodes[a].edges.end()) { + ret = it->cost; + } + } + + return ret; + } + + float goal_heuristic(size_t) const { return 0.f; } + + size_t unique_id(size_t n) const { return n; } + + void foreach_reachable(size_t n, std::function fn) const + { + if (n < nodes.size()) { + for (const Edge &e : nodes[n].edges) + fn(e.to_id); + } + } + } graph; + + std::vector out; + + // 'graph.nodes' is able to be a node cache (it simulates an associative container) + bool found = astar::search_route(graph, size_t(0), std::back_inserter(out), graph.nodes); + + // But should not crash or loop infinitely. + REQUIRE(!found); + + // Without a destination, there is no output. But the algorithm should halt. + REQUIRE(out.empty()); + + // Source node should have it's parent unset + REQUIRE(graph.nodes[0].parent == astar::Unassigned); + + // All other nodes should have their parents set + for (size_t i = 1; i < graph.nodes.size(); ++i) + REQUIRE(graph.nodes[i].parent != astar::Unassigned); + + std::array ref_distances = {0.f, 4.f, 12.f, 19.f, 21.f, + 11.f, 9.f, 8.f, 14.f}; + + // Try to trace each node back to the source node. Each of them should + // arrive to the source within less hops than the full number of nodes. + for (size_t i = 0, k = 0; i < graph.nodes.size(); ++i, k = 0) { + GraphTracer::QNode *q = &graph.nodes[i]; + REQUIRE(q->g == Approx(ref_distances[i])); + while (k++ < graph.nodes.size() && q->parent != astar::Unassigned) + q = &graph.nodes[q->parent]; + + REQUIRE(q->parent == astar::Unassigned); + } } diff --git a/tests/libslic3r/test_marchingsquares.cpp b/tests/libslic3r/test_marchingsquares.cpp index 32b137175..0fbe6a5e3 100644 --- a/tests/libslic3r/test_marchingsquares.cpp +++ b/tests/libslic3r/test_marchingsquares.cpp @@ -325,9 +325,9 @@ static void recreate_object_from_rasters(const std::string &objname, float lh) { double disp_w = 120.96; double disp_h = 68.04; -#ifndef NDEBUG - size_t cntr = 0; -#endif +//#ifndef NDEBUG +// size_t cntr = 0; +//#endif for (ExPolygons &layer : layers) { auto rst = create_raster(res, disp_w, disp_h); @@ -335,11 +335,11 @@ static void recreate_object_from_rasters(const std::string &objname, float lh) { rst.draw(island); } -#ifndef NDEBUG - std::fstream out(objname + std::to_string(cntr) + ".png", std::ios::out); - out << rst.encode(sla::PNGRasterEncoder{}); - out.close(); -#endif +//#ifndef NDEBUG +// std::fstream out(objname + std::to_string(cntr) + ".png", std::ios::out); +// out << rst.encode(sla::PNGRasterEncoder{}); +// out.close(); +//#endif ExPolygons layer_ = sla::raster_to_polygons(rst); // float delta = scaled(std::min(rst.pixel_dimensions().h_mm, @@ -347,19 +347,19 @@ static void recreate_object_from_rasters(const std::string &objname, float lh) { // layer_ = expolygons_simplify(layer_, delta); -#ifndef NDEBUG - SVG svg(objname + std::to_string(cntr) + ".svg", BoundingBox(Point{0, 0}, Point{scaled(disp_w), scaled(disp_h)})); - svg.draw(layer_); - svg.draw(layer, "green"); - svg.Close(); -#endif +//#ifndef NDEBUG +// SVG svg(objname + std::to_string(cntr) + ".svg", BoundingBox(Point{0, 0}, Point{scaled(disp_w), scaled(disp_h)})); +// svg.draw(layer_); +// svg.draw(layer, "green"); +// svg.Close(); +//#endif double layera = 0., layera_ = 0.; for (auto &p : layer) layera += p.area(); for (auto &p : layer_) layera_ += p.area(); -#ifndef NDEBUG - std::cout << cntr++ << std::endl; -#endif +//#ifndef NDEBUG +// std::cout << cntr++ << std::endl; +//#endif double diff = std::abs(layera_ - layera); REQUIRE((diff <= 0.1 * layera || diff < scaled(1.) * scaled(1.))); diff --git a/tests/libslic3r/test_mutable_priority_queue.cpp b/tests/libslic3r/test_mutable_priority_queue.cpp index 78c97bc7e..7e31b57c7 100644 --- a/tests/libslic3r/test_mutable_priority_queue.cpp +++ b/tests/libslic3r/test_mutable_priority_queue.cpp @@ -4,6 +4,8 @@ #include "libslic3r/MutablePriorityQueue.hpp" +using namespace Slic3r; + // based on https://raw.githubusercontent.com/rollbear/prio_queue/master/self_test.cpp // original source Copyright Björn Fahller 2015, Boost Software License, Version 1.0, http://www.boost.org/LICENSE_1_0.txt @@ -346,25 +348,36 @@ TEST_CASE("Mutable priority queue - first pop", "[MutableSkipHeapPriorityQueue]" size_t id; float val; }; - size_t count = 50000; + static constexpr const size_t count = 50000; std::vector idxs(count, {0}); - std::vector dels(count, false); auto q = make_miniheap_mutable_priority_queue( - [&](MyValue &v, size_t idx) { - idxs[v.id] = idx; - }, + [&idxs](MyValue &v, size_t idx) { idxs[v.id] = idx; }, [](MyValue &l, MyValue &r) { return l.val < r.val; }); - q.reserve(count); - for (size_t id = 0; id < count; id++) { - MyValue mv{ id, rand() / 100.f }; - q.push(mv); + using QueueType = decltype(q); + THEN("Skip queue has 0th element unused, 1st element is the top of the queue.") { + CHECK(QueueType::address::is_padding(0)); + CHECK(!QueueType::address::is_padding(1)); + } + q.reserve(count); + for (size_t id = 0; id < count; ++ id) + q.push({ id, rand() / 100.f }); + MyValue v = q.top(); // copy + THEN("Element at the top of the queue has a valid ID.") { + CHECK(v.id >= 0); + CHECK(v.id < count); + } + THEN("Element at the top of the queue has its position stored in idxs.") { + CHECK(idxs[v.id] == 1); } - MyValue it = q.top(); // copy q.pop(); - // is valid id (no initial value default value) - CHECK(it.id != 0); - // is first item in queue valid value - CHECK(idxs[0] != std::numeric_limits::max()); + THEN("Element removed from the queue has its position in idxs reset to invalid.") { + CHECK(idxs[v.id] == q.invalid_id()); + } + THEN("Element was removed from the queue, new top of the queue has its index set correctly.") { + CHECK(q.top().id >= 0); + CHECK(q.top().id < count); + CHECK(idxs[q.top().id] == 1); + } } TEST_CASE("Mutable priority queue complex", "[MutableSkipHeapPriorityQueue]") @@ -382,23 +395,23 @@ TEST_CASE("Mutable priority queue complex", "[MutableSkipHeapPriorityQueue]") q.reserve(count); auto rand_val = [&]()->float { return (rand() % 53) / 10.f; }; - size_t ord = 0; - for (size_t id = 0; id < count; id++) { - MyValue mv; - mv.id = ord++; - mv.val = rand_val(); - q.push(mv); - } + for (size_t id = 0; id < count; ++ id) + q.push({ id, rand_val() }); + auto check = [&]()->bool{ for (size_t i = 0; i < idxs.size(); ++i) { - if (dels[i]) continue; - size_t qid = idxs[i]; - if (qid > 3*count) { - return false; - } - MyValue &mv = q[qid]; - if (mv.id != i) { - return false; // ERROR + if (dels[i]) { + if (idxs[i] != q.invalid_id()) + return false; // ERROR + } else { + size_t qid = idxs[i]; + if (qid >= q.heap_size()) { + return false; // ERROR + } + MyValue &mv = q[qid]; + if (mv.id != i) { + return false; // ERROR + } } } return true; @@ -406,6 +419,7 @@ TEST_CASE("Mutable priority queue complex", "[MutableSkipHeapPriorityQueue]") CHECK(check()); // initial check + // Generate an element ID of an elmenet, which was not yet deleted, thus it is still valid. auto get_valid_id = [&]()->int { int id = 0; do { @@ -413,23 +427,28 @@ TEST_CASE("Mutable priority queue complex", "[MutableSkipHeapPriorityQueue]") } while (dels[id]); return id; }; + + // Remove first 100 elements from the queue of 5000 elements, cross-validate indices. + // Re-enter every 20th element back to the queue. for (size_t i = 0; i < 100; i++) { - MyValue it = q.top(); // copy + MyValue v = q.top(); // copy q.pop(); - dels[it.id] = true; + dels[v.id] = true; CHECK(check()); if (i % 20 == 0) { - it.val = rand_val(); - q.push(it); - dels[it.id] = false; + v.val = rand_val(); + q.push(v); + dels[v.id] = false; CHECK(check()); continue; } - + // Remove some valid element from the queue. int id = get_valid_id(); + CHECK(idxs[id] != q.invalid_id()); q.remove(idxs[id]); dels[id] = true; CHECK(check()); + // and change 5 random elements and reorder them in the queue. for (size_t j = 0; j < 5; j++) { int id = get_valid_id(); size_t qid = idxs[id]; diff --git a/tests/sla_print/CMakeLists.txt b/tests/sla_print/CMakeLists.txt index a26d5f480..24e9552c5 100644 --- a/tests/sla_print/CMakeLists.txt +++ b/tests/sla_print/CMakeLists.txt @@ -5,7 +5,9 @@ add_executable(${_TEST_NAME}_tests ${_TEST_NAME}_tests_main.cpp sla_supptgen_tests.cpp sla_raycast_tests.cpp sla_archive_readwrite_tests.cpp) -target_link_libraries(${_TEST_NAME}_tests test_common libslic3r) + +# mold linker for successful linking needs also to link TBB library and link it before libslic3r. +target_link_libraries(${_TEST_NAME}_tests test_common TBB::tbb libslic3r) set_property(TARGET ${_TEST_NAME}_tests PROPERTY FOLDER "tests") if (WIN32) diff --git a/tests/slic3rutils/CMakeLists.txt b/tests/slic3rutils/CMakeLists.txt index 256e6efd6..e9fcf84f1 100644 --- a/tests/slic3rutils/CMakeLists.txt +++ b/tests/slic3rutils/CMakeLists.txt @@ -4,7 +4,8 @@ add_executable(${_TEST_NAME}_tests slic3r_jobs_tests.cpp ) -target_link_libraries(${_TEST_NAME}_tests test_common libslic3r_gui libslic3r) +# mold linker for successful linking needs also to link TBB library and link it before libslic3r. +target_link_libraries(${_TEST_NAME}_tests test_common TBB::tbb libslic3r_gui libslic3r) if (MSVC) target_link_libraries(${_TEST_NAME}_tests Setupapi.lib) endif () diff --git a/tests/slic3rutils/slic3r_jobs_tests.cpp b/tests/slic3rutils/slic3r_jobs_tests.cpp index d31b07349..d4b912b84 100644 --- a/tests/slic3rutils/slic3r_jobs_tests.cpp +++ b/tests/slic3rutils/slic3r_jobs_tests.cpp @@ -4,10 +4,9 @@ #include #include "slic3r/GUI/Jobs/BoostThreadWorker.hpp" +#include "slic3r/GUI/Jobs/UIThreadWorker.hpp" #include "slic3r/GUI/Jobs/ProgressIndicator.hpp" -//#include - struct Progress: Slic3r::ProgressIndicator { int range = 100; int pr = 0; @@ -19,17 +18,30 @@ struct Progress: Slic3r::ProgressIndicator { int get_range() const override { return range; } }; -TEST_CASE("nullptr job should be ignored", "[Jobs]") { - Slic3r::GUI::BoostThreadWorker worker{std::make_unique()}; +using TestClasses = std::tuple< Slic3r::GUI::UIThreadWorker, Slic3r::GUI::BoostThreadWorker >; + +TEMPLATE_LIST_TEST_CASE("Empty worker should not do anything", "[Jobs]", TestClasses) { + TestType worker{std::make_unique()}; + + REQUIRE(worker.is_idle()); + + worker.wait_for_current_job(); + worker.process_events(); + + REQUIRE(worker.is_idle()); +} + +TEMPLATE_LIST_TEST_CASE("nullptr job should be ignored", "[Jobs]", TestClasses) { + TestType worker{std::make_unique()}; worker.push(nullptr); REQUIRE(worker.is_idle()); } -TEST_CASE("State should not be idle while running a job", "[Jobs]") { +TEMPLATE_LIST_TEST_CASE("State should not be idle while running a job", "[Jobs]", TestClasses) { using namespace Slic3r; using namespace Slic3r::GUI; - BoostThreadWorker worker{std::make_unique(), "worker_thread"}; + TestType worker{std::make_unique(), "worker_thread"}; queue_job(worker, [&worker](Job::Ctl &ctl) { ctl.call_on_main_thread([&worker] { @@ -42,11 +54,11 @@ TEST_CASE("State should not be idle while running a job", "[Jobs]") { REQUIRE(worker.is_idle()); } -TEST_CASE("Status messages should be received by the main thread during job execution", "[Jobs]") { +TEMPLATE_LIST_TEST_CASE("Status messages should be received by the main thread during job execution", "[Jobs]", TestClasses) { using namespace Slic3r; using namespace Slic3r::GUI; auto pri = std::make_shared(); - BoostThreadWorker worker{pri}; + TestType worker{pri}; queue_job(worker, [](Job::Ctl &ctl){ for (int s = 0; s <= 100; ++s) { @@ -60,12 +72,12 @@ TEST_CASE("Status messages should be received by the main thread during job exec REQUIRE(pri->statustxt == "Running"); } -TEST_CASE("Cancellation should be recognized be the worker", "[Jobs]") { +TEMPLATE_LIST_TEST_CASE("Cancellation should be recognized be the worker", "[Jobs]", TestClasses) { using namespace Slic3r; using namespace Slic3r::GUI; auto pri = std::make_shared(); - BoostThreadWorker worker{pri}; + TestType worker{pri}; queue_job( worker, @@ -88,12 +100,12 @@ TEST_CASE("Cancellation should be recognized be the worker", "[Jobs]") { REQUIRE(pri->pr != 100); } -TEST_CASE("cancel_all should remove all pending jobs", "[Jobs]") { +TEMPLATE_LIST_TEST_CASE("cancel_all should remove all pending jobs", "[Jobs]", TestClasses) { using namespace Slic3r; using namespace Slic3r::GUI; auto pri = std::make_shared(); - BoostThreadWorker worker{pri}; + TestType worker{pri}; std::array jobres = {false, false, false, false}; @@ -125,12 +137,12 @@ TEST_CASE("cancel_all should remove all pending jobs", "[Jobs]") { REQUIRE(jobres[3] == false); } -TEST_CASE("Exception should be properly forwarded to finalize()", "[Jobs]") { +TEMPLATE_LIST_TEST_CASE("Exception should be properly forwarded to finalize()", "[Jobs]", TestClasses) { using namespace Slic3r; using namespace Slic3r::GUI; auto pri = std::make_shared(); - BoostThreadWorker worker{pri}; + TestType worker{pri}; queue_job( worker, [](Job::Ctl &) { throw std::runtime_error("test"); }, diff --git a/version.inc b/version.inc index 93b376323..3e1460a0e 100644 --- a/version.inc +++ b/version.inc @@ -3,7 +3,7 @@ set(SLIC3R_APP_NAME "PrusaSlicer") set(SLIC3R_APP_KEY "PrusaSlicer") -set(SLIC3R_VERSION "2.5.0-alpha0") +set(SLIC3R_VERSION "2.6.0-alpha0") set(SLIC3R_BUILD_ID "PrusaSlicer-${SLIC3R_VERSION}+UNKNOWN") -set(SLIC3R_RC_VERSION "2,5,0,0") -set(SLIC3R_RC_VERSION_DOTS "2.5.0.0") +set(SLIC3R_RC_VERSION "2,6,0,0") +set(SLIC3R_RC_VERSION_DOTS "2.6.0.0")