diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index a9ce8dd1f..643afe69e 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -94,8 +94,12 @@ set(SLIC3R_SOURCES Format/objparser.hpp Format/STL.cpp Format/STL.hpp - Format/SLAArchive.hpp - Format/SLAArchive.cpp + Format/SLAArchiveWriter.hpp + Format/SLAArchiveWriter.cpp + Format/SLAArchiveReader.hpp + Format/SLAArchiveReader.cpp + Format/ZipperArchiveImport.hpp + Format/ZipperArchiveImport.cpp Format/SL1.hpp Format/SL1.cpp Format/SL1_SVG.hpp diff --git a/src/libslic3r/Format/SL1.cpp b/src/libslic3r/Format/SL1.cpp index 2f324accd..a94711996 100644 --- a/src/libslic3r/Format/SL1.cpp +++ b/src/libslic3r/Format/SL1.cpp @@ -1,359 +1,37 @@ #include "SL1.hpp" -#include "GCode/ThumbnailData.hpp" -#include "libslic3r/Time.hpp" #include #include -#include "libslic3r/Zipper.hpp" -#include "libslic3r/SLAPrint.hpp" - #include +#include "libslic3r/Time.hpp" +#include "libslic3r/Zipper.hpp" +#include "libslic3r/SLAPrint.hpp" #include "libslic3r/Exception.hpp" -#include "libslic3r/SlicesToTriangleMesh.hpp" -#include "libslic3r/MarchingSquares.hpp" -#include "libslic3r/ClipperUtils.hpp" #include "libslic3r/MTUtils.hpp" #include "libslic3r/PrintConfig.hpp" -#include "libslic3r/SLA/RasterBase.hpp" + #include "libslic3r/miniz_extension.hpp" -#include "libslic3r/PNGReadWrite.hpp" #include "libslic3r/LocalesUtils.hpp" #include "libslic3r/GCode/ThumbnailData.hpp" +#include "SLAArchiveReader.hpp" +#include "ZipperArchiveImport.hpp" + +#include "libslic3r/MarchingSquares.hpp" +#include "libslic3r/PNGReadWrite.hpp" +#include "libslic3r/ClipperUtils.hpp" +#include "libslic3r/Execution/ExecutionTBB.hpp" + +#include "libslic3r/SLA/RasterBase.hpp" + #include #include #include -#include - -namespace marchsq { - -template<> struct _RasterTraits { - using Rst = Slic3r::png::ImageGreyscale; - - // The type of pixel cell in the raster - using ValueType = uint8_t; - - // Value at a given position - static uint8_t get(const Rst &rst, size_t row, size_t col) - { - return rst.get(row, col); - } - - // Number of rows and cols of the raster - static size_t rows(const Rst &rst) { return rst.rows; } - static size_t cols(const Rst &rst) { return rst.cols; } -}; - -} // namespace marchsq - namespace Slic3r { -namespace { - -struct PNGBuffer { std::vector buf; std::string fname; }; -struct ArchiveData { - boost::property_tree::ptree profile, config; - std::vector images; -}; - -static const constexpr char *CONFIG_FNAME = "config.ini"; -static const constexpr char *PROFILE_FNAME = "prusaslicer.ini"; - -boost::property_tree::ptree read_ini(const mz_zip_archive_file_stat &entry, - MZ_Archive & zip) -{ - std::string buf(size_t(entry.m_uncomp_size), '\0'); - - if (!mz_zip_reader_extract_file_to_mem(&zip.arch, entry.m_filename, - buf.data(), buf.size(), 0)) - throw Slic3r::FileIOError(zip.get_errorstr()); - - boost::property_tree::ptree tree; - std::stringstream ss(buf); - boost::property_tree::read_ini(ss, tree); - return tree; -} - -PNGBuffer read_png(const mz_zip_archive_file_stat &entry, - MZ_Archive & zip, - const std::string & name) -{ - std::vector buf(entry.m_uncomp_size); - - if (!mz_zip_reader_extract_file_to_mem(&zip.arch, entry.m_filename, - buf.data(), buf.size(), 0)) - throw Slic3r::FileIOError(zip.get_errorstr()); - - return {std::move(buf), (name.empty() ? entry.m_filename : name)}; -} - -ArchiveData extract_sla_archive(const std::string &zipfname, - const std::string &exclude) -{ - ArchiveData arch; - - // Little RAII - struct Arch: public MZ_Archive { - Arch(const std::string &fname) { - if (!open_zip_reader(&arch, fname)) - throw Slic3r::FileIOError(get_errorstr()); - } - - ~Arch() { close_zip_reader(&arch); } - } zip (zipfname); - - mz_uint num_entries = mz_zip_reader_get_num_files(&zip.arch); - - for (mz_uint i = 0; i < num_entries; ++i) - { - mz_zip_archive_file_stat entry; - - if (mz_zip_reader_file_stat(&zip.arch, i, &entry)) - { - std::string name = entry.m_filename; - boost::algorithm::to_lower(name); - - if (boost::algorithm::contains(name, exclude)) continue; - - if (name == CONFIG_FNAME) arch.config = read_ini(entry, zip); - if (name == PROFILE_FNAME) arch.profile = read_ini(entry, zip); - - if (boost::filesystem::path(name).extension().string() == ".png") { - auto it = std::lower_bound( - arch.images.begin(), arch.images.end(), PNGBuffer{{}, name}, - [](const PNGBuffer &r1, const PNGBuffer &r2) { - return std::less()(r1.fname, r2.fname); - }); - - arch.images.insert(it, read_png(entry, zip, name)); - } - } - } - - return arch; -} - -ExPolygons rings_to_expolygons(const std::vector &rings, - double px_w, double px_h) -{ - auto polys = reserve_vector(rings.size()); - - for (const marchsq::Ring &ring : rings) { - Polygon poly; Points &pts = poly.points; - pts.reserve(ring.size()); - - for (const marchsq::Coord &crd : ring) - pts.emplace_back(scaled(crd.c * px_w), scaled(crd.r * px_h)); - - polys.emplace_back(poly); - } - - // TODO: Is a union necessary? - return union_ex(polys); -} - -template void foreach_vertex(ExPolygon &poly, Fn &&fn) -{ - for (auto &p : poly.contour.points) fn(p); - for (auto &h : poly.holes) - for (auto &p : h.points) fn(p); -} - -void invert_raster_trafo(ExPolygons & expolys, - const sla::RasterBase::Trafo &trafo, - coord_t width, - coord_t height) -{ - if (trafo.flipXY) std::swap(height, width); - - for (auto &expoly : expolys) { - if (trafo.mirror_y) - foreach_vertex(expoly, [height](Point &p) {p.y() = height - p.y(); }); - - if (trafo.mirror_x) - foreach_vertex(expoly, [width](Point &p) {p.x() = width - p.x(); }); - - expoly.translate(-trafo.center_x, -trafo.center_y); - - if (trafo.flipXY) - foreach_vertex(expoly, [](Point &p) { std::swap(p.x(), p.y()); }); - - if ((trafo.mirror_x + trafo.mirror_y + trafo.flipXY) % 2) { - expoly.contour.reverse(); - for (auto &h : expoly.holes) h.reverse(); - } - } -} - -struct RasterParams { - sla::RasterBase::Trafo trafo; // Raster transformations - coord_t width, height; // scaled raster dimensions (not resolution) - double px_h, px_w; // pixel dimesions - marchsq::Coord win; // marching squares window size -}; - -RasterParams get_raster_params(const DynamicPrintConfig &cfg) -{ - auto *opt_disp_cols = cfg.option("display_pixels_x"); - auto *opt_disp_rows = cfg.option("display_pixels_y"); - auto *opt_disp_w = cfg.option("display_width"); - auto *opt_disp_h = cfg.option("display_height"); - auto *opt_mirror_x = cfg.option("display_mirror_x"); - auto *opt_mirror_y = cfg.option("display_mirror_y"); - auto *opt_orient = cfg.option>("display_orientation"); - - if (!opt_disp_cols || !opt_disp_rows || !opt_disp_w || !opt_disp_h || - !opt_mirror_x || !opt_mirror_y || !opt_orient) - throw MissingProfileError("Invalid SL1 / SL1S file"); - - RasterParams rstp; - - rstp.px_w = opt_disp_w->value / (opt_disp_cols->value - 1); - rstp.px_h = opt_disp_h->value / (opt_disp_rows->value - 1); - - rstp.trafo = sla::RasterBase::Trafo{opt_orient->value == sladoLandscape ? - sla::RasterBase::roLandscape : - sla::RasterBase::roPortrait, - {opt_mirror_x->value, opt_mirror_y->value}}; - - rstp.height = scaled(opt_disp_h->value); - rstp.width = scaled(opt_disp_w->value); - - return rstp; -} - -struct SliceParams { double layerh = 0., initial_layerh = 0.; }; - -SliceParams get_slice_params(const DynamicPrintConfig &cfg) -{ - auto *opt_layerh = cfg.option("layer_height"); - auto *opt_init_layerh = cfg.option("initial_layer_height"); - - if (!opt_layerh || !opt_init_layerh) - throw MissingProfileError("Invalid SL1 / SL1S file"); - - return SliceParams{opt_layerh->getFloat(), opt_init_layerh->getFloat()}; -} - -std::vector extract_slices_from_sla_archive( - ArchiveData & arch, - const RasterParams & rstp, - std::function progr) -{ - auto jobdir = arch.config.get("jobDir"); - for (auto &c : jobdir) c = std::tolower(c); - - std::vector slices(arch.images.size()); - - struct Status - { - double incr, val, prev; - bool stop = false; - tbb::spin_mutex mutex = {}; - } st {100. / slices.size(), 0., 0.}; - - tbb::parallel_for(size_t(0), arch.images.size(), - [&arch, &slices, &st, &rstp, progr](size_t i) { - // Status indication guarded with the spinlock - { - std::lock_guard lck(st.mutex); - if (st.stop) return; - - st.val += st.incr; - double curr = std::round(st.val); - if (curr > st.prev) { - st.prev = curr; - st.stop = !progr(int(curr)); - } - } - - png::ImageGreyscale img; - png::ReadBuf rb{arch.images[i].buf.data(), arch.images[i].buf.size()}; - if (!png::decode_png(rb, img)) return; - - uint8_t isoval = 128; - auto rings = marchsq::execute(img, isoval, rstp.win); - ExPolygons expolys = rings_to_expolygons(rings, rstp.px_w, rstp.px_h); - - // Invert the raster transformations indicated in the profile metadata - invert_raster_trafo(expolys, rstp.trafo, rstp.width, rstp.height); - - slices[i] = std::move(expolys); - }); - - if (st.stop) slices = {}; - - return slices; -} - -} // namespace - -ConfigSubstitutions import_sla_archive(const std::string &zipfname, DynamicPrintConfig &out) -{ - ArchiveData arch = extract_sla_archive(zipfname, "png"); - return out.load(arch.profile, ForwardCompatibilitySubstitutionRule::Enable); -} - -// If the profile is missing from the archive (older PS versions did not have -// it), profile_out's initial value will be used as fallback. profile_out will be empty on -// function return if the archive did not contain any profile. -ConfigSubstitutions import_sla_archive( - const std::string & zipfname, - Vec2i windowsize, - indexed_triangle_set & out, - DynamicPrintConfig & profile_out, - std::function progr) -{ - // Ensure minimum window size for marching squares - windowsize.x() = std::max(2, windowsize.x()); - windowsize.y() = std::max(2, windowsize.y()); - - std::string exclude_entries{"thumbnail"}; - ArchiveData arch = extract_sla_archive(zipfname, exclude_entries); - DynamicPrintConfig profile_in, profile_use; - ConfigSubstitutions config_substitutions = - profile_in.load(arch.profile, - ForwardCompatibilitySubstitutionRule::Enable); - - if (profile_in.empty()) { // missing profile... do guess work - // try to recover the layer height from the config.ini which was - // present in all versions of sl1 files. - if (auto lh_opt = arch.config.find("layerHeight"); - lh_opt != arch.config.not_found()) - { - auto lh_str = lh_opt->second.data(); - - size_t pos; - double lh = string_to_double_decimal_point(lh_str, &pos); - if (pos) { // TODO: verify that pos is 0 when parsing fails - profile_out.set("layer_height", lh); - profile_out.set("initial_layer_height", lh); - } - } - } - - // If the archive contains an empty profile, use the one that was passed as output argument - // then replace it with the readed profile to report that it was empty. - profile_use = profile_in.empty() ? profile_out : profile_in; - profile_out = profile_in; - - RasterParams rstp = get_raster_params(profile_use); - rstp.win = {windowsize.y(), windowsize.x()}; - - SliceParams slicp = get_slice_params(profile_use); - - std::vector slices = - extract_slices_from_sla_archive(arch, rstp, progr); - - if (!slices.empty()) - out = slices_to_mesh(slices, 0, slicp.layerh, slicp.initial_layerh); - - return config_substitutions; -} - using ConfMap = std::map; namespace { @@ -558,3 +236,207 @@ void SL1Archive::export_print(const std::string fname, } } // namespace Slic3r + +// ///////////////////////////////////////////////////////////////////////////// +// Reader implementation +// ///////////////////////////////////////////////////////////////////////////// + +namespace marchsq { + +template<> struct _RasterTraits { + using Rst = Slic3r::png::ImageGreyscale; + + // The type of pixel cell in the raster + using ValueType = uint8_t; + + // Value at a given position + static uint8_t get(const Rst &rst, size_t row, size_t col) + { + return rst.get(row, col); + } + + // Number of rows and cols of the raster + static size_t rows(const Rst &rst) { return rst.rows; } + static size_t cols(const Rst &rst) { return rst.cols; } +}; + +} // namespace marchsq + +namespace Slic3r { + +template static void foreach_vertex(ExPolygon &poly, Fn &&fn) +{ + for (auto &p : poly.contour.points) fn(p); + for (auto &h : poly.holes) + for (auto &p : h.points) fn(p); +} + +void invert_raster_trafo(ExPolygons & expolys, + const sla::RasterBase::Trafo &trafo, + coord_t width, + coord_t height) +{ + if (trafo.flipXY) std::swap(height, width); + + for (auto &expoly : expolys) { + if (trafo.mirror_y) + foreach_vertex(expoly, [height](Point &p) {p.y() = height - p.y(); }); + + if (trafo.mirror_x) + foreach_vertex(expoly, [width](Point &p) {p.x() = width - p.x(); }); + + expoly.translate(-trafo.center_x, -trafo.center_y); + + if (trafo.flipXY) + foreach_vertex(expoly, [](Point &p) { std::swap(p.x(), p.y()); }); + + if ((trafo.mirror_x + trafo.mirror_y + trafo.flipXY) % 2) { + expoly.contour.reverse(); + for (auto &h : expoly.holes) h.reverse(); + } + } +} + +RasterParams get_raster_params(const DynamicPrintConfig &cfg) +{ + auto *opt_disp_cols = cfg.option("display_pixels_x"); + auto *opt_disp_rows = cfg.option("display_pixels_y"); + auto *opt_disp_w = cfg.option("display_width"); + auto *opt_disp_h = cfg.option("display_height"); + auto *opt_mirror_x = cfg.option("display_mirror_x"); + auto *opt_mirror_y = cfg.option("display_mirror_y"); + auto *opt_orient = cfg.option>("display_orientation"); + + if (!opt_disp_cols || !opt_disp_rows || !opt_disp_w || !opt_disp_h || + !opt_mirror_x || !opt_mirror_y || !opt_orient) + throw MissingProfileError("Invalid SL1 / SL1S file"); + + RasterParams rstp; + + rstp.px_w = opt_disp_w->value / (opt_disp_cols->value - 1); + rstp.px_h = opt_disp_h->value / (opt_disp_rows->value - 1); + + rstp.trafo = sla::RasterBase::Trafo{opt_orient->value == sladoLandscape ? + sla::RasterBase::roLandscape : + sla::RasterBase::roPortrait, + {opt_mirror_x->value, opt_mirror_y->value}}; + + rstp.height = scaled(opt_disp_h->value); + rstp.width = scaled(opt_disp_w->value); + + return rstp; +} + +namespace { + +ExPolygons rings_to_expolygons(const std::vector &rings, + double px_w, double px_h) +{ + auto polys = reserve_vector(rings.size()); + + for (const marchsq::Ring &ring : rings) { + Polygon poly; Points &pts = poly.points; + pts.reserve(ring.size()); + + for (const marchsq::Coord &crd : ring) + pts.emplace_back(scaled(crd.c * px_w), scaled(crd.r * px_h)); + + polys.emplace_back(poly); + } + + // TODO: Is a union necessary? + return union_ex(polys); +} + +std::vector extract_slices_from_sla_archive( + ZipperArchive &arch, + const RasterParams &rstp, + const marchsq::Coord &win, + std::function progr) +{ + std::vector slices(arch.entries.size()); + + struct Status + { + double incr, val, prev; + bool stop = false; + execution::SpinningMutex mutex = {}; + } st{100. / slices.size(), 0., 0.}; + + execution::for_each( + ex_tbb, size_t(0), arch.entries.size(), + [&arch, &slices, &st, &rstp, &win, progr](size_t i) { + // Status indication guarded with the spinlock + { + std::lock_guard lck(st.mutex); + if (st.stop) return; + + st.val += st.incr; + double curr = std::round(st.val); + if (curr > st.prev) { + st.prev = curr; + st.stop = !progr(int(curr)); + } + } + + png::ImageGreyscale img; + png::ReadBuf rb{arch.entries[i].buf.data(), + arch.entries[i].buf.size()}; + if (!png::decode_png(rb, img)) return; + + constexpr uint8_t isoval = 128; + auto rings = marchsq::execute(img, isoval, win); + ExPolygons expolys = rings_to_expolygons(rings, rstp.px_w, + rstp.px_h); + + // Invert the raster transformations indicated in the profile metadata + invert_raster_trafo(expolys, rstp.trafo, rstp.width, rstp.height); + + slices[i] = std::move(expolys); + }, + execution::max_concurrency(ex_tbb)); + + if (st.stop) slices = {}; + + return slices; +} + +} // namespace + +ConfigSubstitutions SL1Reader::read(std::vector &slices, + DynamicPrintConfig &profile_out) +{ + Vec2i windowsize; + + switch(m_quality) + { + case SLAImportQuality::Fast: windowsize = {8, 8}; break; + case SLAImportQuality::Balanced: windowsize = {4, 4}; break; + default: + case SLAImportQuality::Accurate: + windowsize = {2, 2}; break; + }; + + // Ensure minimum window size for marching squares + windowsize.x() = std::max(2, windowsize.x()); + windowsize.y() = std::max(2, windowsize.y()); + + std::vector includes = { "ini", "png"}; + std::vector excludes = { "thumbnail" }; + ZipperArchive arch = read_zipper_archive(m_fname, includes, excludes); + auto [profile_use, config_substitutions] = extract_profile(arch, profile_out); + + RasterParams rstp = get_raster_params(profile_use); + marchsq::Coord win = {windowsize.y(), windowsize.x()}; + slices = extract_slices_from_sla_archive(arch, rstp, win, m_progr); + + return std::move(config_substitutions); +} + +ConfigSubstitutions SL1Reader::read(DynamicPrintConfig &out) +{ + ZipperArchive arch = read_zipper_archive(m_fname, {}, {"png"}); + return out.load(arch.profile, ForwardCompatibilitySubstitutionRule::Enable); +} + +} // namespace Slic3r diff --git a/src/libslic3r/Format/SL1.hpp b/src/libslic3r/Format/SL1.hpp index 0a662cc1e..6c7a903a6 100644 --- a/src/libslic3r/Format/SL1.hpp +++ b/src/libslic3r/Format/SL1.hpp @@ -3,16 +3,15 @@ #include -#include "SLAArchive.hpp" +#include "SLAArchiveWriter.hpp" +#include "SLAArchiveReader.hpp" #include "libslic3r/Zipper.hpp" #include "libslic3r/PrintConfig.hpp" -struct indexed_triangle_set; - namespace Slic3r { -class SL1Archive: public SLAArchive { +class SL1Archive: public SLAArchiveWriter { SLAPrinterConfig m_cfg; protected: @@ -38,27 +37,41 @@ public: const ThumbnailsList &thumbnails, const std::string &projectname = "") override; }; - -ConfigSubstitutions import_sla_archive(const std::string &zipfname, DynamicPrintConfig &out); -ConfigSubstitutions import_sla_archive( - const std::string & zipfname, - Vec2i windowsize, - indexed_triangle_set & out, - DynamicPrintConfig & profile, - std::function progr = [](int) { return true; }); +class SL1Reader: public SLAArchiveReader { + SLAImportQuality m_quality = SLAImportQuality::Balanced; + std::function m_progr; + std::string m_fname; -inline ConfigSubstitutions import_sla_archive( - const std::string & zipfname, - Vec2i windowsize, - indexed_triangle_set & out, - std::function progr = [](int) { return true; }) -{ - DynamicPrintConfig profile; - return import_sla_archive(zipfname, windowsize, out, profile, progr); -} +public: + // If the profile is missing from the archive (older PS versions did not have + // it), profile_out's initial value will be used as fallback. profile_out will be empty on + // function return if the archive did not contain any profile. + ConfigSubstitutions read(std::vector &slices, + DynamicPrintConfig &profile_out) override; -class MissingProfileError : public RuntimeError { using RuntimeError::RuntimeError; }; + ConfigSubstitutions read(DynamicPrintConfig &profile) override; + + SL1Reader() = default; + SL1Reader(const std::string &fname, + SLAImportQuality quality, + std::function progr) + : m_quality(quality), m_progr(progr), m_fname(fname) + {} +}; + +struct RasterParams { + sla::RasterBase::Trafo trafo; // Raster transformations + coord_t width, height; // scaled raster dimensions (not resolution) + double px_h, px_w; // pixel dimesions +}; + +RasterParams get_raster_params(const DynamicPrintConfig &cfg); + +void invert_raster_trafo(ExPolygons & expolys, + const sla::RasterBase::Trafo &trafo, + coord_t width, + coord_t height); } // namespace Slic3r::sla diff --git a/src/libslic3r/Format/SL1_SVG.cpp b/src/libslic3r/Format/SL1_SVG.cpp index 844385aec..f8b93a8ba 100644 --- a/src/libslic3r/Format/SL1_SVG.cpp +++ b/src/libslic3r/Format/SL1_SVG.cpp @@ -3,6 +3,10 @@ #include "libslic3r/LocalesUtils.hpp" #include "libslic3r/ClipperUtils.hpp" #include "libslic3r/BoundingBox.hpp" +#include "libslic3r/Format/ZipperArchiveImport.hpp" + +#define NANOSVG_IMPLEMENTATION +#include "nanosvg/nanosvg.h" #include #include @@ -16,6 +20,7 @@ namespace { size_t constexpr coord_t_bufsize = 40; +// A fast and locale independent implementation of int=>str char const* decimal_from(coord_t snumber, char* buffer) { std::make_unsigned_t number = 0; @@ -50,6 +55,7 @@ inline std::string coord2str(coord_t crd) return decimal_from(crd, buf); } +// Apply the sla::RasterBase::Trafo onto an ExPolygon void transform(ExPolygon &ep, const sla::RasterBase::Trafo &tr, const BoundingBox &bb) { if (tr.flipXY) { @@ -71,12 +77,13 @@ void transform(ExPolygon &ep, const sla::RasterBase::Trafo &tr, const BoundingBo } } +// Append the svg string representation of a Polygon to the input 'buf' void append_svg(std::string &buf, const Polygon &poly) { if (poly.points.empty()) return; - auto c = poly.points.front(); + Point c = poly.points.front(); char intbuf[coord_t_bufsize]; @@ -91,7 +98,7 @@ void append_svg(std::string &buf, const Polygon &poly) if (d.x() == 0 && d.y() == 0) continue; buf += " "sv; - buf += decimal_from(p.x() - c.x(), intbuf); + buf += decimal_from(d.x(), intbuf); buf += " "sv; buf += decimal_from(d.y(), intbuf); c = p; @@ -139,9 +146,6 @@ public: "\n"; - - // Add black background; - m_svg += "\n"; } void draw(const ExPolygon& poly) override @@ -171,6 +175,8 @@ public: Trafo trafo() const override { return m_trafo; } + // The encoder is ignored here, the svg text does not need any further + // encoding. sla::EncodedRaster encode(sla::RasterEncoder /*encoder*/) const override { std::vector data; @@ -190,13 +196,11 @@ std::unique_ptr SL1_SVGArchive::create_raster() const auto w = cfg().display_width.getFloat(); auto h = cfg().display_height.getFloat(); -// auto res_x = size_t(cfg().display_pixels_x.getInt()); -// auto res_y = size_t(cfg().display_pixels_y.getInt()); float precision_nm = scaled(cfg().sla_output_precision.getFloat()); - size_t res_x = std::round(scaled(w) / precision_nm); - size_t res_y = std::round(scaled(h) / precision_nm); + auto res_x = size_t(std::round(scaled(w) / precision_nm)); + auto res_y = size_t(std::round(scaled(h) / precision_nm)); - std::array mirror; + std::array mirror; mirror[X] = cfg().display_mirror_x.getBool(); mirror[Y] = cfg().display_mirror_y.getBool(); @@ -220,6 +224,7 @@ std::unique_ptr SL1_SVGArchive::create_raster() const return std::make_unique(svgarea, sla::Resolution{res_x, res_y}, tr); } +// SVG does not need additional binary encoding. sla::RasterEncoder SL1_SVGArchive::get_encoder() const { return nullptr; @@ -230,9 +235,83 @@ void SL1_SVGArchive::export_print(const std::string fname, const ThumbnailsList &thumbnails, const std::string &projectname) { + // Export code is completely identical to SL1, only the compression level + // is elevated, as the SL1 has already compressed PNGs with deflate, + // but the svg is just text. Zipper zipper{fname, Zipper::TIGHT_COMPRESSION}; SL1Archive::export_print(zipper, print, thumbnails, projectname); } +struct NanoSVGParser { + NSVGimage *image; + static constexpr const char *Units = "mm"; // Denotes user coordinate system + static constexpr float Dpi = 1.f; // Not needed + explicit NanoSVGParser(char* input): image{nsvgParse(input, Units, Dpi)} {} + ~NanoSVGParser() { nsvgDelete(image); } +}; + +ConfigSubstitutions SL1_SVGReader::read(std::vector &slices, + DynamicPrintConfig &profile_out) +{ + std::vector includes = { CONFIG_FNAME, PROFILE_FNAME, "svg"}; + ZipperArchive arch = read_zipper_archive(m_fname, includes, {}); + auto [profile_use, config_substitutions] = extract_profile(arch, profile_out); + + RasterParams rstp = get_raster_params(profile_use); + + struct Status + { + double incr, val, prev; + bool stop = false; + } st{100. / arch.entries.size(), 0., 0.}; + + for (const EntryBuffer &entry : arch.entries) { + if (st.stop) break; + + st.val += st.incr; + double curr = std::round(st.val); + if (curr > st.prev) { + st.prev = curr; + st.stop = !m_progr(int(curr)); + } + + // Don't want to use dirty casts for the buffer to be usable in + // the NanoSVGParser until performance is not a bottleneck here. + auto svgtxt = reserve_vector(entry.buf.size() + 1); + std::copy(entry.buf.begin(), entry.buf.end(), std::back_inserter(svgtxt)); + svgtxt.emplace_back('\0'); + NanoSVGParser svgp(svgtxt.data()); + + Polygons polys; + for (NSVGshape *shape = svgp.image->shapes; shape != nullptr; shape = shape->next) { + for (NSVGpath *path = shape->paths; path != nullptr; path = path->next) { + Polygon p; + for (int i = 0; i < path->npts; ++i) { + size_t c = 2 * i; + p.points.emplace_back(scaled(Vec2f(path->pts[c], path->pts[c + 1]))); + } + polys.emplace_back(p); + } + } + + // Create the slice from the read polygons. Here, the fill rule has to + // be the same as stated in the svg file which is `nonzero` when exported + // using SL1_SVGArchive. Would be better to parse it from the svg file, + // but if it's different, the file is probably corrupted anyways. + ExPolygons expolys = union_ex(polys, ClipperLib::pftNonZero); + invert_raster_trafo(expolys, rstp.trafo, rstp.width, rstp.height); + slices.emplace_back(expolys); + } + + // Compile error without the move + return std::move(config_substitutions); +} + +ConfigSubstitutions SL1_SVGReader::read(DynamicPrintConfig &out) +{ + ZipperArchive arch = read_zipper_archive(m_fname, {"prusaslicer.ini"}, {}); + return out.load(arch.profile, ForwardCompatibilitySubstitutionRule::Enable); +} + } // namespace Slic3r diff --git a/src/libslic3r/Format/SL1_SVG.hpp b/src/libslic3r/Format/SL1_SVG.hpp index a764f1a4c..d94a5153a 100644 --- a/src/libslic3r/Format/SL1_SVG.hpp +++ b/src/libslic3r/Format/SL1_SVG.hpp @@ -22,6 +22,27 @@ public: using SL1Archive::SL1Archive; }; +class SL1_SVGReader: public SLAArchiveReader { + std::function m_progr; + std::string m_fname; + +public: + // If the profile is missing from the archive (older PS versions did not have + // it), profile_out's initial value will be used as fallback. profile_out will be empty on + // function return if the archive did not contain any profile. + ConfigSubstitutions read(std::vector &slices, + DynamicPrintConfig &profile_out) override; + + ConfigSubstitutions read(DynamicPrintConfig &profile) override; + + SL1_SVGReader() = default; + SL1_SVGReader(const std::string &fname, + SLAImportQuality /*quality*/, + const ProgrFn & progr) + : m_progr(progr), m_fname(fname) + {} +}; + } // namespace Slic3r #endif // SL1_SVG_HPP diff --git a/src/libslic3r/Format/SLAArchiveReader.cpp b/src/libslic3r/Format/SLAArchiveReader.cpp new file mode 100644 index 000000000..09c157059 --- /dev/null +++ b/src/libslic3r/Format/SLAArchiveReader.cpp @@ -0,0 +1,179 @@ +#include "SLAArchiveReader.hpp" +#include "SL1.hpp" +#include "SL1_SVG.hpp" + +#include "libslic3r/SlicesToTriangleMesh.hpp" + +#include +#include + +constexpr const char * L(const char * str) { return str; } + +#include +#include + +namespace Slic3r { + +namespace { + +// Factory function that returns an implementation of SLAArchiveReader. +using ArchiveFactory = std::function< + std::unique_ptr(const std::string &fname, + SLAImportQuality quality, + const ProgrFn & progr)>; + +// Entry in the global registry of readable archive formats. +struct ArchiveEntry { + const char *descr; + std::vector extensions; + ArchiveFactory factoryfn; +}; + +// This is where the readable archive formats are registered. +static const std::map REGISTERED_ARCHIVES { + { + "SL1", + { L("SL1 / SL1S archive files"), {"sl1", "sl1s", "zip"}, + [] (const std::string &fname, SLAImportQuality quality, const ProgrFn &progr) { return std::make_unique(fname, quality, progr); } } + }, + { + "SL2", + { L("SL2 archive files"), {"sl2", "sl1_svg"/*, "zip"*/}, // also a zip but unnecessary hassle to implement single extension for multiple archives + [] (const std::string &fname, SLAImportQuality quality, const ProgrFn &progr) { return std::make_unique(fname, quality, progr); }} + }, + // TODO: pwmx and future others. +}; + +} // namespace + +std::unique_ptr SLAArchiveReader::create( + const std::string &fname, + const std::string &format_id, + SLAImportQuality quality, + const ProgrFn & progr) +{ + // Create an instance of SLAArchiveReader using the registered archive + // reader implementations. + // If format_id is specified and valid, that archive format will be + // preferred. When format_id is emtpy, the file extension is compared + // with the advertised extensions of registered readers and the first + // match will be used. + + std::string ext = boost::filesystem::path(fname).extension().string(); + boost::algorithm::to_lower(ext); + + std::unique_ptr ret; + + auto arch_from = REGISTERED_ARCHIVES.begin(); + auto arch_to = REGISTERED_ARCHIVES.end(); + + auto arch_it = REGISTERED_ARCHIVES.find(format_id); + if (arch_it != REGISTERED_ARCHIVES.end()) { + arch_from = arch_it; + arch_to = arch_it; + } + + if (!ext.empty()) { + if (ext.front() == '.') + ext.erase(ext.begin()); + + auto extcmp = [&ext](const auto &e) { return e == ext; }; + + for (auto it = arch_from; it != arch_to; ++it) { + const auto &[format_id, entry] = *it; + if (std::any_of(entry.extensions.begin(), entry.extensions.end(), extcmp)) + ret = entry.factoryfn(fname, quality, progr); + } + } + + return ret; +} + +const std::vector &SLAArchiveReader::registered_archives() +{ + static std::vector archnames; + + if (archnames.empty()) { + archnames.reserve(REGISTERED_ARCHIVES.size()); + + for (auto &[name, _] : REGISTERED_ARCHIVES) + archnames.emplace_back(name.c_str()); + } + + return archnames; +} + +std::vector SLAArchiveReader::get_extensions(const char *archtype) +{ + auto it = REGISTERED_ARCHIVES.find(archtype); + + if (it != REGISTERED_ARCHIVES.end()) + return it->second.extensions; + + return {}; +} + +const char *SLAArchiveReader::get_description(const char *archtype) +{ + auto it = REGISTERED_ARCHIVES.find(archtype); + + if (it != REGISTERED_ARCHIVES.end()) + return it->second.descr; + + return nullptr; +} + +struct SliceParams { double layerh = 0., initial_layerh = 0.; }; + +static SliceParams get_slice_params(const DynamicPrintConfig &cfg) +{ + auto *opt_layerh = cfg.option("layer_height"); + auto *opt_init_layerh = cfg.option("initial_layer_height"); + + if (!opt_layerh || !opt_init_layerh) + throw MissingProfileError("Invalid SL1 / SL1S file"); + + return SliceParams{opt_layerh->getFloat(), opt_init_layerh->getFloat()}; +} + +ConfigSubstitutions import_sla_archive(const std::string &zipfname, + const std::string &format_id, + indexed_triangle_set &out, + DynamicPrintConfig &profile, + SLAImportQuality quality, + const ProgrFn & progr) +{ + ConfigSubstitutions ret; + + if (auto reader = SLAArchiveReader::create(zipfname, format_id, quality, progr)) { + std::vector slices; + ret = reader->read(slices, profile); + + SliceParams slicp = get_slice_params(profile); + + if (!slices.empty()) + out = slices_to_mesh(slices, 0, slicp.layerh, slicp.initial_layerh); + + } else { + throw ReaderUnimplementedError("Reader unimplemented"); + } + + return ret; +} + +ConfigSubstitutions import_sla_archive(const std::string &zipfname, + const std::string &format_id, + DynamicPrintConfig &out) +{ + ConfigSubstitutions ret; + + if (auto reader = SLAArchiveReader::create(zipfname, format_id)) { + ret = reader->read(out); + } else { + throw ReaderUnimplementedError("Reader unimplemented"); + } + + return ret; +} + +} // namespace Slic3r diff --git a/src/libslic3r/Format/SLAArchiveReader.hpp b/src/libslic3r/Format/SLAArchiveReader.hpp new file mode 100644 index 000000000..e7a99b043 --- /dev/null +++ b/src/libslic3r/Format/SLAArchiveReader.hpp @@ -0,0 +1,85 @@ +#ifndef SLAARCHIVEREADER_HPP +#define SLAARCHIVEREADER_HPP + +#include "libslic3r/PrintConfig.hpp" +#include "libslic3r/ExPolygon.hpp" + +struct indexed_triangle_set; + +namespace Slic3r { + +// A generic indicator for the quality of an imported model. Obviously, the +// original cannot be fully reconstructed. +enum class SLAImportQuality { Accurate, Balanced, Fast }; + +// Raised when the needed metadata cannot be retrieved or guessed from an archive +class MissingProfileError : public RuntimeError +{ + using RuntimeError::RuntimeError; +}; + +// A shortname for status indication function. +// The argument is the status (from <0, 100>) +// Returns false if cancel was requested. +using ProgrFn = std::function; + +// Abstract interface for an archive reader. This needs to be implemented for +// every supported archive format. +class SLAArchiveReader { +public: + + virtual ~SLAArchiveReader() = default; + + // Read the profile and reconstruct the slices + virtual ConfigSubstitutions read(std::vector &slices, + DynamicPrintConfig &profile) = 0; + + // Overload for reading only the profile contained in the archive (if present) + virtual ConfigSubstitutions read(DynamicPrintConfig &profile) = 0; + + // Creates a reader instance based on the provided file path. + // format_id can be one of the archive type identifiers returned by + // registered_archives(). If left empty, only the file extension will + // be considered. If more archive types have the same extension (like *.zip) + // The first match is used. + static std::unique_ptr create( + const std::string &fname, + const std::string &format_id, + SLAImportQuality quality = SLAImportQuality::Balanced, + const ProgrFn &progr = [](int) { return false; }); + + // Get the names of currently known archive reader implementations + static const std::vector & registered_archives(); + + // Get the understood file extensions belonging to an archive format + static std::vector get_extensions(const char *archtype); + + // Generic description (usable in GUI) about an archive format + static const char * get_description(const char *archtype); +}; + +// Raised in import_sla_archive when a nullptr reader is returned by +// SLAArchiveReader::create() +class ReaderUnimplementedError : public RuntimeError +{ + using RuntimeError::RuntimeError; +}; + +// Helper free functions to import an archive using the above interface. +// Can throw ReaderUnimplementedError or MissingProfileError +ConfigSubstitutions import_sla_archive( + const std::string &zipfname, + const std::string &format_id, + indexed_triangle_set &out, + DynamicPrintConfig &profile, + SLAImportQuality quality = SLAImportQuality::Balanced, + const ProgrFn &progr = [](int) { return true; }); + +// Only reads the profile, doesn't reconstruct the model. +ConfigSubstitutions import_sla_archive(const std::string &zipfname, + const std::string &format_id, + DynamicPrintConfig &out); + +} // namespace Slic3r + +#endif // SLAARCHIVEREADER_HPP diff --git a/src/libslic3r/Format/SLAArchive.cpp b/src/libslic3r/Format/SLAArchiveWriter.cpp similarity index 75% rename from src/libslic3r/Format/SLAArchive.cpp rename to src/libslic3r/Format/SLAArchiveWriter.cpp index 8e2bf2824..babf92d0d 100644 --- a/src/libslic3r/Format/SLAArchive.cpp +++ b/src/libslic3r/Format/SLAArchiveWriter.cpp @@ -1,4 +1,4 @@ -#include "SLAArchive.hpp" +#include "SLAArchiveWriter.hpp" #include "SL1.hpp" #include "SL1_SVG.hpp" @@ -13,7 +13,7 @@ namespace Slic3r { -using ArchiveFactory = std::function(const SLAPrinterConfig&)>; +using ArchiveFactory = std::function(const SLAPrinterConfig&)>; struct ArchiveEntry { const char *ext; @@ -35,8 +35,8 @@ static const std::map REGISTERED_ARCHIVES { } }; -std::unique_ptr -SLAArchive::create(const std::string &archtype, const SLAPrinterConfig &cfg) +std::unique_ptr +SLAArchiveWriter::create(const std::string &archtype, const SLAPrinterConfig &cfg) { auto entry = REGISTERED_ARCHIVES.find(archtype); @@ -46,7 +46,7 @@ SLAArchive::create(const std::string &archtype, const SLAPrinterConfig &cfg) return nullptr; } -const std::vector& SLAArchive::registered_archives() +const std::vector& SLAArchiveWriter::registered_archives() { static std::vector archnames; @@ -60,9 +60,9 @@ const std::vector& SLAArchive::registered_archives() return archnames; } -const char *SLAArchive::get_extension(const char *archtype) +const char *SLAArchiveWriter::get_extension(const char *archtype) { - static const char* DEFAULT_EXT = "zip"; + constexpr const char* DEFAULT_EXT = "zip"; auto entry = REGISTERED_ARCHIVES.find(archtype); if (entry != REGISTERED_ARCHIVES.end()) diff --git a/src/libslic3r/Format/SLAArchive.hpp b/src/libslic3r/Format/SLAArchiveWriter.hpp similarity index 88% rename from src/libslic3r/Format/SLAArchive.hpp rename to src/libslic3r/Format/SLAArchiveWriter.hpp index 971d7ba7b..86132cceb 100644 --- a/src/libslic3r/Format/SLAArchive.hpp +++ b/src/libslic3r/Format/SLAArchiveWriter.hpp @@ -12,7 +12,7 @@ namespace Slic3r { class SLAPrint; class SLAPrinterConfig; -class SLAArchive { +class SLAArchiveWriter { protected: std::vector m_layers; @@ -20,7 +20,7 @@ protected: virtual sla::RasterEncoder get_encoder() const = 0; public: - virtual ~SLAArchive() = default; + virtual ~SLAArchiveWriter() = default; // Fn have to be thread safe: void(sla::RasterBase& raster, size_t lyrid); template @@ -44,14 +44,15 @@ public: execution::max_concurrency(ep)); } - // Export the print into an archive using the provided filename. + // Export the print into an archive using the provided filename. virtual void export_print(const std::string fname, const SLAPrint &print, const ThumbnailsList &thumbnails, const std::string &projectname = "") = 0; // Factory method to create an archiver instance - static std::unique_ptr create(const std::string &archtype, const SLAPrinterConfig&); + static std::unique_ptr create( + const std::string &archtype, const SLAPrinterConfig &); // Get the names of currently known archiver implementations static const std::vector & registered_archives(); diff --git a/src/libslic3r/Format/ZipperArchiveImport.cpp b/src/libslic3r/Format/ZipperArchiveImport.cpp new file mode 100644 index 000000000..5b0ecff6e --- /dev/null +++ b/src/libslic3r/Format/ZipperArchiveImport.cpp @@ -0,0 +1,136 @@ +#include "ZipperArchiveImport.hpp" + +#include "libslic3r/miniz_extension.hpp" +#include "libslic3r/Exception.hpp" +#include "libslic3r/PrintConfig.hpp" + +#include +#include +#include + +namespace Slic3r { + +namespace { + +// Read an ini file into boost property tree +boost::property_tree::ptree read_ini(const mz_zip_archive_file_stat &entry, + MZ_Archive &zip) +{ + std::string buf(size_t(entry.m_uncomp_size), '\0'); + + if (!mz_zip_reader_extract_file_to_mem(&zip.arch, entry.m_filename, + buf.data(), buf.size(), 0)) + throw Slic3r::FileIOError(zip.get_errorstr()); + + boost::property_tree::ptree tree; + std::stringstream ss(buf); + boost::property_tree::read_ini(ss, tree); + return tree; +} + +// Read an arbitrary file into EntryBuffer +EntryBuffer read_entry(const mz_zip_archive_file_stat &entry, + MZ_Archive &zip, + const std::string &name) +{ + std::vector buf(entry.m_uncomp_size); + + if (!mz_zip_reader_extract_file_to_mem(&zip.arch, entry.m_filename, + buf.data(), buf.size(), 0)) + throw Slic3r::FileIOError(zip.get_errorstr()); + + return {std::move(buf), (name.empty() ? entry.m_filename : name)}; +} + +} // namespace + +ZipperArchive read_zipper_archive(const std::string &zipfname, + const std::vector &includes, + const std::vector &excludes) +{ + ZipperArchive arch; + + // Little RAII + struct Arch : public MZ_Archive + { + Arch(const std::string &fname) + { + if (!open_zip_reader(&arch, fname)) + throw Slic3r::FileIOError(get_errorstr()); + } + + ~Arch() { close_zip_reader(&arch); } + } zip(zipfname); + + mz_uint num_entries = mz_zip_reader_get_num_files(&zip.arch); + + for (mz_uint i = 0; i < num_entries; ++i) { + mz_zip_archive_file_stat entry; + + if (mz_zip_reader_file_stat(&zip.arch, i, &entry)) { + std::string name = entry.m_filename; + boost::algorithm::to_lower(name); + + if (!std::any_of(includes.begin(), includes.end(), + [&name](const std::string &incl) { + return boost::algorithm::contains(name, incl); + })) + continue; + + if (std::any_of(excludes.begin(), excludes.end(), + [&name](const std::string &excl) { + return boost::algorithm::contains(name, excl); + })) + continue; + + if (name == CONFIG_FNAME) { arch.config = read_ini(entry, zip); continue; } + if (name == PROFILE_FNAME) { arch.profile = read_ini(entry, zip); continue; } + + auto it = std::lower_bound( + arch.entries.begin(), arch.entries.end(), + EntryBuffer{{}, name}, + [](const EntryBuffer &r1, const EntryBuffer &r2) { + return std::less()(r1.fname, r2.fname); + }); + + arch.entries.insert(it, read_entry(entry, zip, name)); + } + } + + return arch; +} + +std::pair extract_profile( + const ZipperArchive &arch, DynamicPrintConfig &profile_out) +{ + DynamicPrintConfig profile_in, profile_use; + ConfigSubstitutions config_substitutions = + profile_in.load(arch.profile, + ForwardCompatibilitySubstitutionRule::Enable); + + if (profile_in.empty()) { // missing profile... do guess work + // try to recover the layer height from the config.ini which was + // present in all versions of sl1 files. + if (auto lh_opt = arch.config.find("layerHeight"); + lh_opt != arch.config.not_found()) { + auto lh_str = lh_opt->second.data(); + + size_t pos = 0; + double lh = string_to_double_decimal_point(lh_str, &pos); + if (pos) { // TODO: verify that pos is 0 when parsing fails + profile_out.set("layer_height", lh); + profile_out.set("initial_layer_height", lh); + } + } + } + + // If the archive contains an empty profile, use the one that was passed + // as output argument then replace it with the readed profile to report + // that it was empty. + profile_use = profile_in.empty() ? profile_out : profile_in; + profile_out = profile_in; + + return {profile_use, std::move(config_substitutions)}; +} + +} // namespace Slic3r diff --git a/src/libslic3r/Format/ZipperArchiveImport.hpp b/src/libslic3r/Format/ZipperArchiveImport.hpp new file mode 100644 index 000000000..adddcacd4 --- /dev/null +++ b/src/libslic3r/Format/ZipperArchiveImport.hpp @@ -0,0 +1,54 @@ +#ifndef ZIPPERARCHIVEIMPORT_HPP +#define ZIPPERARCHIVEIMPORT_HPP + +#include +#include +#include + +#include + +#include "libslic3r/PrintConfig.hpp" + +namespace Slic3r { + +// Buffer for arbitraryfiles inside a zipper archive. +struct EntryBuffer +{ + std::vector buf; + std::string fname; +}; + +// Structure holding the data read from a zipper archive. +struct ZipperArchive +{ + boost::property_tree::ptree profile, config; + std::vector entries; +}; + +// Names of the files containing metadata inside the archive. +const constexpr char *CONFIG_FNAME = "config.ini"; +const constexpr char *PROFILE_FNAME = "prusaslicer.ini"; + +// Read an archive that was written using the Zipper class. +// The includes parameter is a set of file name substrings that the entries +// must contain to be included in ZipperArchive. +// The excludes parameter may contain substrings that filenames must not +// contain. +// Every file in the archive is read into ZipperArchive::entries +// except the files CONFIG_FNAME, and PROFILE_FNAME which are read into +// ZipperArchive::config and ZipperArchive::profile structures. +ZipperArchive read_zipper_archive(const std::string &zipfname, + const std::vector &includes, + const std::vector &excludes); + +// Extract the print profile form the archive into 'out'. +// Returns a profile that has correct parameters to use for model reconstruction +// even if the needed parameters were not fully found in the archive's metadata. +// The inout argument shall be a usable fallback profile if the archive +// has totally corrupted metadata. +std::pair extract_profile( + const ZipperArchive &arch, DynamicPrintConfig &inout); + +} // namespace Slic3r + +#endif // ZIPPERARCHIVEIMPORT_HPP diff --git a/src/libslic3r/Format/pwmx.hpp b/src/libslic3r/Format/pwmx.hpp index 65fb19910..6d667fab7 100644 --- a/src/libslic3r/Format/pwmx.hpp +++ b/src/libslic3r/Format/pwmx.hpp @@ -3,13 +3,13 @@ #include -#include "SLAArchive.hpp" +#include "SLAArchiveWriter.hpp" #include "libslic3r/PrintConfig.hpp" namespace Slic3r { -class PwmxArchive: public SLAArchive { +class PwmxArchive: public SLAArchiveWriter { SLAPrinterConfig m_cfg; protected: diff --git a/src/libslic3r/SLAPrint.cpp b/src/libslic3r/SLAPrint.cpp index 203393e9c..c4494c3c2 100644 --- a/src/libslic3r/SLAPrint.cpp +++ b/src/libslic3r/SLAPrint.cpp @@ -246,7 +246,7 @@ SLAPrint::ApplyStatus SLAPrint::apply(const Model &model, DynamicPrintConfig con m_default_object_config.apply_only(config, object_diff, true); if (!m_archiver || !printer_diff.empty()) - m_archiver = SLAArchive::create(m_printer_config.sla_archive_format.value.c_str(), m_printer_config); + m_archiver = SLAArchiveWriter::create(m_printer_config.sla_archive_format.value.c_str(), m_printer_config); struct ModelObjectStatus { enum Status { diff --git a/src/libslic3r/SLAPrint.hpp b/src/libslic3r/SLAPrint.hpp index 6d8dc058b..56309b30b 100644 --- a/src/libslic3r/SLAPrint.hpp +++ b/src/libslic3r/SLAPrint.hpp @@ -9,7 +9,7 @@ #include "Point.hpp" #include "MTUtils.hpp" #include "Zipper.hpp" -#include "Format/SLAArchive.hpp" +#include "Format/SLAArchiveWriter.hpp" #include "GCode/ThumbnailData.hpp" #include "libslic3r/Execution/ExecutionTBB.hpp" @@ -524,7 +524,7 @@ private: std::vector m_printer_input; // The archive object which collects the raster images after slicing - std::unique_ptr m_archiver; + std::unique_ptr m_archiver; // Estimated print time, material consumed. SLAPrintStatistics m_print_statistics; diff --git a/src/slic3r/GUI/BitmapCache.cpp b/src/slic3r/GUI/BitmapCache.cpp index e23591fb6..b585798d3 100644 --- a/src/slic3r/GUI/BitmapCache.cpp +++ b/src/slic3r/GUI/BitmapCache.cpp @@ -6,6 +6,8 @@ #include "GUI_Utils.hpp" #include +#include +#include #ifdef __WXGTK2__ // Broken alpha workaround @@ -13,7 +15,7 @@ #include #endif /* __WXGTK2__ */ -#define NANOSVG_IMPLEMENTATION +//#define NANOSVG_IMPLEMENTATION #include "nanosvg/nanosvg.h" #define NANOSVGRAST_IMPLEMENTATION #include "nanosvg/nanosvgrast.h" diff --git a/src/slic3r/GUI/Jobs/SLAImportDialog.hpp b/src/slic3r/GUI/Jobs/SLAImportDialog.hpp index 15cab9ed1..5477e51e7 100644 --- a/src/slic3r/GUI/Jobs/SLAImportDialog.hpp +++ b/src/slic3r/GUI/Jobs/SLAImportDialog.hpp @@ -10,17 +10,67 @@ #include #include "libslic3r/AppConfig.hpp" +#include "libslic3r/Format/SLAArchiveReader.hpp" + #include "slic3r/GUI/I18N.hpp" #include "slic3r/GUI/GUI.hpp" #include "slic3r/GUI/GUI_App.hpp" #include "slic3r/GUI/Plater.hpp" +#include + //#include "libslic3r/Model.hpp" //#include "libslic3r/PresetBundle.hpp" namespace Slic3r { namespace GUI { +std::string get_readers_wildcard() +{ + std::string ret; + + for (const char *archtype : SLAArchiveReader::registered_archives()) { + ret += _utf8(SLAArchiveReader::get_description(archtype)); + ret += " ("; + auto extensions = SLAArchiveReader::get_extensions(archtype); + for (const char * ext : extensions) { + ret += "*."; + ret += ext; + ret += ", "; + } + // remove last ", " + if (!extensions.empty()) { + ret.pop_back(); + ret.pop_back(); + } + + ret += ")|"; + + for (std::string ext : extensions) { + boost::algorithm::to_lower(ext); + ret += "*."; + ret += ext; + ret += ";"; + + boost::algorithm::to_upper(ext); + ret += "*."; + ret += ext; + ret += ";"; + } + + // remove last ';' + if (!extensions.empty()) + ret.pop_back(); + + ret += "|"; + } + + if (ret.back() == '|') + ret.pop_back(); + + return ret; +} + class SLAImportDialog: public wxDialog, public SLAImportJobView { wxFilePickerCtrl *m_filepicker; wxComboBox *m_import_dropdown, *m_quality_dropdown; @@ -34,7 +84,7 @@ public: m_filepicker = new wxFilePickerCtrl(this, wxID_ANY, from_u8(wxGetApp().app_config->get_last_dir()), _(L("Choose SLA archive:")), - "SL1 / SL1S archive files (*.sl1, *.sl1s, *.zip)|*.sl1;*.SL1;*.sl1s;*.SL1S;*.zip;*.ZIP", + get_readers_wildcard(), wxDefaultPosition, wxDefaultSize, wxFLP_DEFAULT_STYLE | wxFD_OPEN | wxFD_FILE_MUST_EXIST); szfilepck->Add(new wxStaticText(this, wxID_ANY, _L("Import file") + ": "), 0, wxALIGN_CENTER); @@ -64,7 +114,7 @@ public: }; m_quality_dropdown = new wxComboBox( - this, wxID_ANY, qual_choices[0], wxDefaultPosition, wxDefaultSize, + this, wxID_ANY, qual_choices[1], wxDefaultPosition, wxDefaultSize, qual_choices.size(), qual_choices.data(), wxCB_READONLY | wxCB_DROPDOWN); szchoices->Add(m_quality_dropdown, 1); @@ -96,17 +146,15 @@ public: return Sel(std::min(int(Sel::modelOnly), std::max(0, sel))); } - Vec2i get_marchsq_windowsize() const override + SLAImportQuality get_quality() const override { - enum { Accurate, Balanced, Fast}; - switch(m_quality_dropdown->GetSelection()) { - case Fast: return {8, 8}; - case Balanced: return {4, 4}; + case 2: return SLAImportQuality::Fast; + case 1: return SLAImportQuality::Balanced; + case 0: return SLAImportQuality::Accurate; default: - case Accurate: - return {2, 2}; + return SLAImportQuality::Balanced; } } @@ -114,6 +162,14 @@ public: { return m_filepicker->GetPath().ToUTF8().data(); } + + std::string get_archive_format() const override + { + // TODO: the choosen format is inside the file dialog which is not + // accessible from the file picker object. The file picker could be + // changed to a custom file dialog. + return {}; + } }; }} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/Jobs/SLAImportJob.cpp b/src/slic3r/GUI/Jobs/SLAImportJob.cpp index b7dc64d52..c258c9c95 100644 --- a/src/slic3r/GUI/Jobs/SLAImportJob.cpp +++ b/src/slic3r/GUI/Jobs/SLAImportJob.cpp @@ -2,6 +2,7 @@ #include "libslic3r/SLAPrint.hpp" #include "libslic3r/Format/SL1.hpp" +#include "libslic3r/Format/SLAArchiveReader.hpp" #include "slic3r/GUI/GUI.hpp" #include "slic3r/GUI/Plater.hpp" @@ -24,7 +25,7 @@ public: indexed_triangle_set mesh; DynamicPrintConfig profile; wxString path; - Vec2i win = {2, 2}; + Quality quality = Quality::Balanced; std::string err; ConfigSubstitutions config_substitutions; @@ -55,20 +56,30 @@ void SLAImportJob::process(Ctl &ctl) if (p->path.empty()) return; std::string path = p->path.ToUTF8().data(); + std::string format_id = p->import_dlg->get_archive_format(); + try { switch (p->sel) { case Sel::modelAndProfile: case Sel::modelOnly: - p->config_substitutions = import_sla_archive(path, p->win, p->mesh, p->profile, progr); + p->config_substitutions = import_sla_archive(path, + format_id, + p->mesh, + p->profile, + p->quality, progr); break; case Sel::profileOnly: - p->config_substitutions = import_sla_archive(path, p->profile); + p->config_substitutions = import_sla_archive(path, format_id, + p->profile); break; } } catch (MissingProfileError &) { - p->err = _L("The SLA archive doesn't contain any presets. " - "Please activate some SLA printer preset first before importing that SLA archive.").ToStdString(); - } catch (std::exception &ex) { + p->err = _u8L("The SLA archive doesn't contain any presets. " + "Please activate some SLA printer preset first before " + "importing that SLA archive."); + } catch (ReaderUnimplementedError &) { + p->err = _u8L("Import is unavailable for this archive format."); + }catch (std::exception &ex) { p->err = ex.what(); } @@ -81,7 +92,7 @@ void SLAImportJob::reset() p->sel = Sel::modelAndProfile; p->mesh = {}; p->profile = p->plater->sla_print().full_print_config(); - p->win = {2, 2}; + p->quality = SLAImportQuality::Balanced; p->path.Clear(); } @@ -89,11 +100,12 @@ void SLAImportJob::prepare() { reset(); - auto path = p->import_dlg->get_path(); - auto nm = wxFileName(path); - p->path = !nm.Exists(wxFILE_EXISTS_REGULAR) ? "" : nm.GetFullPath(); - p->sel = p->import_dlg->get_selection(); - p->win = p->import_dlg->get_marchsq_windowsize(); + auto path = p->import_dlg->get_path(); + auto nm = wxFileName(path); + p->path = !nm.Exists(wxFILE_EXISTS_REGULAR) ? "" : nm.GetFullPath(); + p->sel = p->import_dlg->get_selection(); + p->quality = p->import_dlg->get_quality(); + p->config_substitutions.clear(); } @@ -115,8 +127,8 @@ void SLAImportJob::finalize(bool canceled, std::exception_ptr &eptr) p->plater->get_notification_manager()->push_notification( NotificationType::CustomNotification, NotificationManager::NotificationLevel::WarningNotificationLevel, - _L("The imported SLA archive did not contain any presets. " - "The current SLA presets were used as fallback.").ToStdString()); + _u8L("The imported SLA archive did not contain any presets. " + "The current SLA presets were used as fallback.")); } if (p->sel != Sel::modelOnly) { @@ -138,15 +150,27 @@ void SLAImportJob::finalize(bool canceled, std::exception_ptr &eptr) config.apply(SLAFullPrintConfig::defaults()); config += std::move(p->profile); - wxGetApp().preset_bundle->load_config_model(name, std::move(config)); - p->plater->check_selected_presets_visibility(ptSLA); - wxGetApp().load_current_presets(); + if (Preset::printer_technology(config) == ptSLA) { + wxGetApp().preset_bundle->load_config_model(name, std::move(config)); + p->plater->check_selected_presets_visibility(ptSLA); + wxGetApp().load_current_presets(); + } else { + p->plater->get_notification_manager()->push_notification( + NotificationType::CustomNotification, + NotificationManager::NotificationLevel::WarningNotificationLevel, + _u8L("The profile in the imported archive is corrupt and will not be loaded.")); + } } if (!p->mesh.empty()) { bool is_centered = false; p->plater->sidebar().obj_list()->load_mesh_object(TriangleMesh{std::move(p->mesh)}, name, is_centered); + } else if (p->sel == Sel::modelOnly || p->sel == Sel::modelAndProfile) { + p->plater->get_notification_manager()->push_notification( + NotificationType::CustomNotification, + NotificationManager::NotificationLevel::WarningNotificationLevel, + _u8L("No object could be retrieved from the archive. The slices might be corrupted or missing.")); } if (! p->config_substitutions.empty()) diff --git a/src/slic3r/GUI/Jobs/SLAImportJob.hpp b/src/slic3r/GUI/Jobs/SLAImportJob.hpp index b2aea8bf8..3a578855c 100644 --- a/src/slic3r/GUI/Jobs/SLAImportJob.hpp +++ b/src/slic3r/GUI/Jobs/SLAImportJob.hpp @@ -3,7 +3,7 @@ #include "Job.hpp" -#include "libslic3r/Point.hpp" +#include "libslic3r/Format/SLAArchiveReader.hpp" namespace Slic3r { namespace GUI { @@ -14,8 +14,9 @@ public: virtual ~SLAImportJobView() = default; virtual Sel get_selection() const = 0; - virtual Vec2i get_marchsq_windowsize() const = 0; + virtual SLAImportQuality get_quality() const = 0; virtual std::string get_path() const = 0; + virtual std::string get_archive_format() const { return ""; } }; class Plater; @@ -25,6 +26,7 @@ class SLAImportJob : public Job { std::unique_ptr p; using Sel = SLAImportJobView::Sel; + using Quality = SLAImportQuality; public: void prepare(); diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index af285e46e..570877716 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -1212,7 +1212,7 @@ void MainFrame::init_menubar_as_editor() [this](wxCommandEvent&) { if (m_plater) m_plater->add_model(true); }, "import_plater", nullptr, [this](){return m_plater != nullptr; }, this); - append_menu_item(import_menu, wxID_ANY, _L("Import SL1 / SL1S Archive") + dots, _L("Load an SL1 / Sl1S archive"), + append_menu_item(import_menu, wxID_ANY, _L("Import SLA Archive") + dots, _L("Load an SLA archive"), [this](wxCommandEvent&) { if (m_plater) m_plater->import_sl1_archive(); }, "import_plater", nullptr, [this](){return m_plater != nullptr && m_plater->get_ui_job_worker().is_idle(); }, this); diff --git a/tests/sla_print/CMakeLists.txt b/tests/sla_print/CMakeLists.txt index 88c1cd186..a26d5f480 100644 --- a/tests/sla_print/CMakeLists.txt +++ b/tests/sla_print/CMakeLists.txt @@ -4,7 +4,7 @@ add_executable(${_TEST_NAME}_tests ${_TEST_NAME}_tests_main.cpp sla_test_utils.hpp sla_test_utils.cpp sla_supptgen_tests.cpp sla_raycast_tests.cpp - sla_archive_export_tests.cpp) + sla_archive_readwrite_tests.cpp) target_link_libraries(${_TEST_NAME}_tests test_common libslic3r) set_property(TARGET ${_TEST_NAME}_tests PROPERTY FOLDER "tests") diff --git a/tests/sla_print/sla_archive_export_tests.cpp b/tests/sla_print/sla_archive_export_tests.cpp deleted file mode 100644 index 9dbe5bc64..000000000 --- a/tests/sla_print/sla_archive_export_tests.cpp +++ /dev/null @@ -1,40 +0,0 @@ -#include -#include - -#include "libslic3r/SLAPrint.hpp" -#include "libslic3r/Format/SLAArchive.hpp" - -#include - -using namespace Slic3r; - -TEST_CASE("Archive export test", "[sla_archives]") { - constexpr const char *PNAME = "20mm_cube"; - - for (auto &archname : SLAArchive::registered_archives()) { - INFO(std::string("Testing archive type: ") + archname); - SLAPrint print; - SLAFullPrintConfig fullcfg; - - auto m = Model::read_from_file(TEST_DATA_DIR PATH_SEPARATOR + std::string(PNAME) + ".obj", nullptr); - - fullcfg.set("sla_archive_format", archname); - fullcfg.set("supports_enable", false); - fullcfg.set("pad_enable", false); - - DynamicPrintConfig cfg; - cfg.apply(fullcfg); - - print.set_status_callback([](const PrintBase::SlicingStatus&) {}); - print.apply(m, cfg); - print.process(); - - ThumbnailsList thumbnails; - auto outputfname = std::string("output.") + SLAArchive::get_extension(archname); - - print.export_print(outputfname, thumbnails, PNAME); - - // Not much can be checked about the archives... - REQUIRE(boost::filesystem::exists(outputfname)); - } -} diff --git a/tests/sla_print/sla_archive_readwrite_tests.cpp b/tests/sla_print/sla_archive_readwrite_tests.cpp new file mode 100644 index 000000000..a7ed7f0a4 --- /dev/null +++ b/tests/sla_print/sla_archive_readwrite_tests.cpp @@ -0,0 +1,71 @@ +#include +#include + +#include "libslic3r/SLAPrint.hpp" +#include "libslic3r/TriangleMesh.hpp" +#include "libslic3r/Format/SLAArchiveWriter.hpp" +#include "libslic3r/Format/SLAArchiveReader.hpp" + +#include + +using namespace Slic3r; + +TEST_CASE("Archive export test", "[sla_archives]") { + for (const char * pname : {"20mm_cube", "extruder_idler"}) + for (auto &archname : SLAArchiveWriter::registered_archives()) { + INFO(std::string("Testing archive type: ") + archname + " -- writing..."); + SLAPrint print; + SLAFullPrintConfig fullcfg; + + auto m = Model::read_from_file(TEST_DATA_DIR PATH_SEPARATOR + std::string(pname) + ".obj", nullptr); + + fullcfg.printer_technology.setInt(ptSLA); // FIXME this should be ensured + fullcfg.set("sla_archive_format", archname); + fullcfg.set("supports_enable", false); + fullcfg.set("pad_enable", false); + + DynamicPrintConfig cfg; + cfg.apply(fullcfg); + + print.set_status_callback([](const PrintBase::SlicingStatus&) {}); + print.apply(m, cfg); + print.process(); + + ThumbnailsList thumbnails; + auto outputfname = std::string("output_") + pname + "." + SLAArchiveWriter::get_extension(archname); + + print.export_print(outputfname, thumbnails, pname); + + // Not much can be checked about the archives... + REQUIRE(boost::filesystem::exists(outputfname)); + + double vol_written = m.mesh().volume(); + + auto readable_formats = SLAArchiveReader::registered_archives(); + if (std::any_of(readable_formats.begin(), readable_formats.end(), + [&archname](const std::string &a) { return a == archname; })) { + + INFO(std::string("Testing archive type: ") + archname + " -- reading back..."); + + indexed_triangle_set its; + DynamicPrintConfig cfg; + + try { + // Leave format_id deliberetaly empty, guessing should always + // work here. + import_sla_archive(outputfname, "", its, cfg); + } catch (...) { + REQUIRE(false); + } + + // its_write_obj(its, (outputfname + ".obj").c_str()); + + REQUIRE(!cfg.empty()); + REQUIRE(!its.empty()); + + double vol_read = its_volume(its); + double rel_err = std::abs(vol_written - vol_read) / vol_written; + REQUIRE(rel_err < 0.1); + } + } +}