diff --git a/.gitignore b/.gitignore index c4df3f3f8e..704289a220 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 002cd34567..4267de2fb9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -43,14 +43,6 @@ set(SLIC3R_GTK "2" CACHE STRING "GTK version to use with wxWidgets on Linux") set(IS_CROSS_COMPILE FALSE) -if (SLIC3R_STATIC) - # Prefer config scripts over find modules. This is helpful when building with - # the static dependencies. Many libraries have their own export scripts - # while having a Find module in standard cmake installation. - # (e.g. CURL) - set(CMAKE_FIND_PACKAGE_PREFER_CONFIG ON) -endif () - if (APPLE) set(CMAKE_FIND_FRAMEWORK LAST) set(CMAKE_FIND_APPBUNDLE LAST) @@ -458,19 +450,10 @@ 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) - -if (TARGET EXPAT::EXPAT ) - target_link_libraries(libexpat INTERFACE EXPAT::EXPAT) -elseif(TARGET expat::expat) - target_link_libraries(libexpat INTERFACE expat::expat) -endif () - find_package(PNG REQUIRED) set(OpenGL_GL_PREFERENCE "LEGACY") diff --git a/cmake/modules/FindCURL.cmake b/cmake/modules/FindCURL.cmake index e0deafa45c..ced5911347 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/src/PrusaSlicer_app_msvc.cpp b/src/PrusaSlicer_app_msvc.cpp index 2ccf2f1ffd..83f24c3075 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/libslic3r/Fill/Lightning/DistanceField.cpp b/src/libslic3r/Fill/Lightning/DistanceField.cpp index 308ca41c60..ef407664c1 100644 --- a/src/libslic3r/Fill/Lightning/DistanceField.cpp +++ b/src/libslic3r/Fill/Lightning/DistanceField.cpp @@ -5,6 +5,8 @@ #include "../FillRectilinear.hpp" #include "../../ClipperUtils.hpp" +#include + namespace Slic3r::FillLightning { @@ -18,33 +20,42 @@ 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. 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); + 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()); @@ -96,12 +107,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 beb46c5c57..d4a142c056 100644 --- a/src/libslic3r/Fill/Lightning/DistanceField.hpp +++ b/src/libslic3r/Fill/Lightning/DistanceField.hpp @@ -38,11 +38,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 +83,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 +92,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 +104,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 < m_grid_size.y() * m_grid_size.x()); + return flat_idx; + } + }; + + UnsupportedPointsGrid m_unsupported_points_grid; /*! * Maps the point to the grid coordinates. diff --git a/src/libslic3r/Fill/Lightning/Generator.cpp b/src/libslic3r/Fill/Lightning/Generator.cpp index b4c07a338e..e226fbbab5 100644 --- a/src/libslic3r/Fill/Lightning/Generator.cpp +++ b/src/libslic3r/Fill/Lightning/Generator.cpp @@ -125,6 +125,8 @@ void Generator::generateTrees(const PrintObject &print_object, const std::functi if (const BoundingBox &outlines_locator_bbox = outlines_locator.bbox(); outlines_locator_bbox.defined) below_outlines_bbox.merge(outlines_locator_bbox); + 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/Layer.cpp b/src/libslic3r/Fill/Lightning/Layer.cpp index f3193afe49..0bd2a65c40 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) @@ -56,8 +60,9 @@ void Layer::generateNewTrees // 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); @@ -141,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 ? diff --git a/src/libslic3r/Fill/Lightning/TreeNode.hpp b/src/libslic3r/Fill/Lightning/TreeNode.hpp index fdb80d2e6f..81c63f7f66 100644 --- a/src/libslic3r/Fill/Lightning/TreeNode.hpp +++ b/src/libslic3r/Fill/Lightning/TreeNode.hpp @@ -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 4259562aa8..abe705dc49 100644 --- a/src/libslic3r/Format/3mf.cpp +++ b/src/libslic3r/Format/3mf.cpp @@ -1,3198 +1,3221 @@ -#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 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 diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp index 85bcd69ba5..e744d72ca7 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 2a5ce6839c..b36f6ed6e5 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/SLA/Rotfinder.cpp b/src/libslic3r/SLA/Rotfinder.cpp index 847e638e6f..16cd28130a 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/Technologies.hpp b/src/libslic3r/Technologies.hpp index 989b1dda82..da2e6f3f55 100644 --- a/src/libslic3r/Technologies.hpp +++ b/src/libslic3r/Technologies.hpp @@ -87,8 +87,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) -// Disable association to 3mf and stl files if the application is run on Windows 8 or later -#define ENABLE_REMOVE_ASSOCIATION_TO_FILE_FOR_WINDOWS_8_AND_LATER (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/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt index d6e2235a77..29b8b7e736 100644 --- a/src/slic3r/CMakeLists.txt +++ b/src/slic3r/CMakeLists.txt @@ -176,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 @@ -250,6 +251,8 @@ set(SLIC3R_GUI_SOURCES Utils/TCPConsole.hpp Utils/MKS.cpp Utils/MKS.hpp + Utils/WinRegistry.cpp + Utils/WinRegistry.hpp ) if (APPLE) diff --git a/src/slic3r/GUI/ConfigWizard.cpp b/src/slic3r/GUI/ConfigWizard.cpp index b9dcc1a4f6..080de997ee 100644 --- a/src/slic3r/GUI/ConfigWizard.cpp +++ b/src/slic3r/GUI/ConfigWizard.cpp @@ -1934,10 +1934,7 @@ void ConfigWizard::priv::load_pages() index->add_page(page_update); index->add_page(page_reload_from_disk); #ifdef _WIN32 -#if ENABLE_REMOVE_ASSOCIATION_TO_FILE_FOR_WINDOWS_8_AND_LATER - if (page_files_association != nullptr) -#endif // ENABLE_REMOVE_ASSOCIATION_TO_FILE_FOR_WINDOWS_8_AND_LATER - index->add_page(page_files_association); + index->add_page(page_files_association); #endif // _WIN32 index->add_page(page_mode); @@ -2750,32 +2747,20 @@ bool ConfigWizard::priv::apply_config(AppConfig *app_config, PresetBundle *prese app_config->set("export_sources_full_pathnames", page_reload_from_disk->full_pathnames ? "1" : "0"); #ifdef _WIN32 -#if ENABLE_REMOVE_ASSOCIATION_TO_FILE_FOR_WINDOWS_8_AND_LATER - if (page_files_association != nullptr) { -#endif // ENABLE_REMOVE_ASSOCIATION_TO_FILE_FOR_WINDOWS_8_AND_LATER - 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"); + 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(); -// } -#if ENABLE_REMOVE_ASSOCIATION_TO_FILE_FOR_WINDOWS_8_AND_LATER + 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 { - app_config->set("associate_3mf", "0"); - app_config->set("associate_stl", "0"); -// app_config->set("associate_gcode", "0"); - } -#endif // ENABLE_REMOVE_ASSOCIATION_TO_FILE_FOR_WINDOWS_8_AND_LATER - +// else { +// if (page_files_association->associate_gcode()) +// wxGetApp().associate_gcode_files(); +// } #endif // _WIN32 page_mode->serialize_mode(app_config); @@ -2795,6 +2780,10 @@ bool ConfigWizard::priv::apply_config(AppConfig *app_config, PresetBundle *prese 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); } @@ -2935,11 +2924,7 @@ ConfigWizard::ConfigWizard(wxWindow *parent) p->add_page(p->page_update = new PageUpdate(this)); p->add_page(p->page_reload_from_disk = new PageReloadFromDisk(this)); #ifdef _WIN32 -#if ENABLE_REMOVE_ASSOCIATION_TO_FILE_FOR_WINDOWS_8_AND_LATER - // file association is not possible anymore starting with Win 8 - if (wxPlatformInfo::Get().GetOSMajorVersion() < 8) -#endif // ENABLE_REMOVE_ASSOCIATION_TO_FILE_FOR_WINDOWS_8_AND_LATER - p->add_page(p->page_files_association = new PageFilesAssociation(this)); + 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)); diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index 2134a795ff..1e28d12871 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -991,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/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index af9b63acd4..b688e11f5a 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -59,6 +59,7 @@ #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" @@ -1203,17 +1204,10 @@ bool GUI_App::on_init_inner() if (is_editor()) { #ifdef __WXMSW__ -#if ENABLE_REMOVE_ASSOCIATION_TO_FILE_FOR_WINDOWS_8_AND_LATER - // file association is not possible anymore starting with Win 8 - if (wxPlatformInfo::Get().GetOSMajorVersion() < 8) { -#endif // ENABLE_REMOVE_ASSOCIATION_TO_FILE_FOR_WINDOWS_8_AND_LATER - if (app_config->get("associate_3mf") == "1") - associate_3mf_files(); - if (app_config->get("associate_stl") == "1") - associate_stl_files(); -#if ENABLE_REMOVE_ASSOCIATION_TO_FILE_FOR_WINDOWS_8_AND_LATER - } -#endif // ENABLE_REMOVE_ASSOCIATION_TO_FILE_FOR_WINDOWS_8_AND_LATER + 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(); @@ -1251,15 +1245,8 @@ bool GUI_App::on_init_inner() } else { #ifdef __WXMSW__ -#if ENABLE_REMOVE_ASSOCIATION_TO_FILE_FOR_WINDOWS_8_AND_LATER - // file association is not possible anymore starting with Win 8 - if (wxPlatformInfo::Get().GetOSMajorVersion() < 8) { -#endif // ENABLE_REMOVE_ASSOCIATION_TO_FILE_FOR_WINDOWS_8_AND_LATER - if (app_config->get("associate_gcode") == "1") - associate_gcode_files(); -#if ENABLE_REMOVE_ASSOCIATION_TO_FILE_FOR_WINDOWS_8_AND_LATER - } -#endif // ENABLE_REMOVE_ASSOCIATION_TO_FILE_FOR_WINDOWS_8_AND_LATER + if (app_config->get("associate_gcode") == "1") + associate_gcode_files(); #endif // __WXMSW__ } @@ -2432,23 +2419,16 @@ void GUI_App::open_preferences(const std::string& highlight_option /*= std::stri this->plater_->refresh_print(); #ifdef _WIN32 -#if ENABLE_REMOVE_ASSOCIATION_TO_FILE_FOR_WINDOWS_8_AND_LATER - // file association is not possible anymore starting with Win 8 - if (wxPlatformInfo::Get().GetOSMajorVersion() < 8) { -#endif // ENABLE_REMOVE_ASSOCIATION_TO_FILE_FOR_WINDOWS_8_AND_LATER - 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(); - } -#if ENABLE_REMOVE_ASSOCIATION_TO_FILE_FOR_WINDOWS_8_AND_LATER + 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 // ENABLE_REMOVE_ASSOCIATION_TO_FILE_FOR_WINDOWS_8_AND_LATER #endif // _WIN32 if (mainframe->preferences_dialog->settings_layout_changed()) { @@ -3156,119 +3136,22 @@ bool GUI_App::open_browser_with_warning_dialog(const wxString& url, wxWindow* pa #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); + associate_file_type(L".3mf", L"Prusa.Slicer.1", L"PrusaSlicer", true); } 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); + associate_file_type(L".stl", L"Prusa.Slicer.1", L"PrusaSlicer", true); } 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); + 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())); diff --git a/src/slic3r/GUI/GUI_Preview.cpp b/src/slic3r/GUI/GUI_Preview.cpp index 9f62fa0b85..88bd42a66f 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/Gizmos/GLGizmoRotate.cpp b/src/slic3r/GUI/Gizmos/GLGizmoRotate.cpp index b7da8a3529..8cd48a3f81 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoRotate.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoRotate.cpp @@ -879,7 +879,9 @@ 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) { diff --git a/src/slic3r/GUI/Jobs/PlaterWorker.hpp b/src/slic3r/GUI/Jobs/PlaterWorker.hpp index 5735902728..58bd1ec32b 100644 --- a/src/slic3r/GUI/Jobs/PlaterWorker.hpp +++ b/src/slic3r/GUI/Jobs/PlaterWorker.hpp @@ -40,6 +40,12 @@ class PlaterWorker: public Worker { { wxWakeUpIdle(); ctl.update_status(st, msg); + + // If the worker is not using additional threads, the UI + // is refreshed with this call. If the worker is running + // in it's own thread, the yield should not have any + // visible effects. + wxYieldIfNeeded(); } bool was_canceled() const override { return ctl.was_canceled(); } diff --git a/src/slic3r/GUI/Jobs/UIThreadWorker.hpp b/src/slic3r/GUI/Jobs/UIThreadWorker.hpp new file mode 100644 index 0000000000..610d205cf9 --- /dev/null +++ b/src/slic3r/GUI/Jobs/UIThreadWorker.hpp @@ -0,0 +1,125 @@ +#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 + { + return std::async(std::launch::deferred, [fn]{ fn(); }); + } + +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/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 192935b022..c9cc7fc225 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -1656,6 +1656,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; diff --git a/src/slic3r/GUI/Preferences.cpp b/src/slic3r/GUI/Preferences.cpp index 3da843f894..a4317d8f45 100644 --- a/src/slic3r/GUI/Preferences.cpp +++ b/src/slic3r/GUI/Preferences.cpp @@ -79,9 +79,11 @@ void PreferencesDialog::show(const std::string& highlight_opt_key /*= std::strin 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()); + 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(); } @@ -230,23 +232,16 @@ void PreferencesDialog::build() app_config->get("export_sources_full_pathnames") == "1"); #ifdef _WIN32 -#if ENABLE_REMOVE_ASSOCIATION_TO_FILE_FOR_WINDOWS_8_AND_LATER - // file association is not possible anymore starting with Win 8 - if (wxPlatformInfo::Get().GetOSMajorVersion() < 8) { -#endif // ENABLE_REMOVE_ASSOCIATION_TO_FILE_FOR_WINDOWS_8_AND_LATER - // 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"); + // 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"); -#if ENABLE_REMOVE_ASSOCIATION_TO_FILE_FOR_WINDOWS_8_AND_LATER - } -#endif // ENABLE_REMOVE_ASSOCIATION_TO_FILE_FOR_WINDOWS_8_AND_LATER + 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(); diff --git a/src/slic3r/GUI/Selection.cpp b/src/slic3r/GUI/Selection.cpp index edf5a3634e..c7cd707b1c 100644 --- a/src/slic3r/GUI/Selection.cpp +++ b/src/slic3r/GUI/Selection.cpp @@ -1925,12 +1925,10 @@ void Selection::render_sidebar_hints(const std::string& sidebar_field) #if ENABLE_GL_SHADERS_ATTRIBUTES const Transform3d base_matrix = Geometry::assemble_transform(get_bounding_box().center()); -#endif // ENABLE_GL_SHADERS_ATTRIBUTES -#if ENABLE_GL_SHADERS_ATTRIBUTES || ENABLE_WORLD_COORDINATE Transform3d orient_matrix = Transform3d::Identity(); #else glsafe(::glPushMatrix()); -#endif // ENABLE_GL_SHADERS_ATTRIBUTES || ENABLE_WORLD_COORDINATE +#endif // ENABLE_GL_SHADERS_ATTRIBUTES #if ENABLE_WORLD_COORDINATE const Vec3d center = get_bounding_box().center(); diff --git a/src/slic3r/Utils/WinRegistry.cpp b/src/slic3r/Utils/WinRegistry.cpp new file mode 100644 index 0000000000..068a7fff17 --- /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 0000000000..bc0875d32d --- /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/slic3rutils/slic3r_jobs_tests.cpp b/tests/slic3rutils/slic3r_jobs_tests.cpp index d31b07349a..d4b912b84f 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 93b3763239..3e1460a0ed 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")