From 606d3f1ef43d5f778a26f7fed31ccd388b965475 Mon Sep 17 00:00:00 2001 From: Filip Sykala - NTB T15p Date: Mon, 27 Jan 2025 16:24:22 +0100 Subject: [PATCH] SPE-2660: Add information message about possibly incompatible config, amended by @lukasmatena --- src/libslic3r/Format/3mf.cpp | 40 ++++++++++++++++++++++++++---------- src/libslic3r/Format/3mf.hpp | 2 +- src/slic3r/GUI/Plater.cpp | 27 ++++++++++++++++++++---- 3 files changed, 53 insertions(+), 16 deletions(-) diff --git a/src/libslic3r/Format/3mf.cpp b/src/libslic3r/Format/3mf.cpp index e8fde1fbb1..6d5640b46f 100644 --- a/src/libslic3r/Format/3mf.cpp +++ b/src/libslic3r/Format/3mf.cpp @@ -3940,34 +3940,52 @@ static void handle_legacy_project_loaded( } } -bool is_project_3mf(const std::string& filename) +// Project = either it contains our config OR it is stamped as being produced +// by PrusaSlicer (in which case the version is passed out). +std::pair> is_project_3mf(const std::string& filename) { + std::pair> out = std::make_pair(false, std::nullopt); + mz_zip_archive archive; mz_zip_zero_struct(&archive); if (!open_zip_reader(&archive, filename)) - return false; + return out; + ScopeGuard guard([&archive]() {close_zip_reader(&archive); }); 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)) { + if (mz_zip_reader_is_file_a_directory(&archive, i)) + continue; std::string name(stat.m_filename); std::replace(name.begin(), name.end(), '\\', '/'); - - if (boost::algorithm::iequals(name, PRINT_CONFIG_FILE)) { - config_found = true; - break; + if (boost::algorithm::iequals(name, PRINT_CONFIG_FILE)) + out.first = true; + if (boost::algorithm::iequals(name, MODEL_FILE)) { + if (auto* iter = mz_zip_reader_extract_iter_new(&archive, i, 0)) { + ScopeGuard g([&iter]() { mz_zip_reader_extract_iter_free(iter); }); + char buffer[1024]; + memset(buffer, 0, sizeof(buffer)); + if (size_t bytes_read = mz_zip_reader_extract_iter_read(iter, buffer, sizeof(buffer)); bytes_read > 0) { + std::string header(buffer); + boost::regex pattern(R"(^\s*$)"); + boost::sregex_iterator it(header.begin(), header.end(), pattern); + boost::sregex_iterator end; + if (it != end) { + Semver semver; + semver.parse((*it)[1].str()); + out.second = semver; + } + } + } } } } - - close_zip_reader(&archive); - - return config_found; + return out; } bool load_3mf( diff --git a/src/libslic3r/Format/3mf.hpp b/src/libslic3r/Format/3mf.hpp index b2e3746fd2..18ec814aca 100644 --- a/src/libslic3r/Format/3mf.hpp +++ b/src/libslic3r/Format/3mf.hpp @@ -47,7 +47,7 @@ namespace Slic3r { struct ThumbnailData; // Returns true if the 3mf file with the given filename is a PrusaSlicer project file (i.e. if it contains a config). - extern bool is_project_3mf(const std::string& filename); + extern std::pair> is_project_3mf(const std::string&); // Load the content of a 3mf file into the given model and preset bundle. extern bool load_3mf( diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 581ce81201..f40b858e8e 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -1376,6 +1376,21 @@ std::vector Plater::priv::load_files(const std::vector& input_ config.null_nullables(); // and place the loaded config over the base. config += std::move(config_loaded); + } else { // config_loaded.empty() + // Detection of possible breaking change in 3MF configuration loading sometimes in future. + if (prusaslicer_generator_version && // set only when loaded with configuration + *prusaslicer_generator_version > Semver(SLIC3R_VERSION)) { + wxString title = _L("Configuration was not loaded"); + const wxString url = "https://prusa.io/3mf-transfer"; + // TRN: %1% is filename of the project, %2% is url link. + wxString message = format_wxstr(_L("Unable to load configuration from project\n\nFile: %1%\n\n" + "This project was created in a newer version of PrusaSlicer. Only the geometry was loaded.\n" + "Update to the latest version for full compatibility.\nFor more info: %2%"), + from_path(filename), url); + HtmlCapableRichMessageDialog dialog(q, message ,title, wxOK, + [&url](const std::string&) { wxGetApp().open_browser_with_warning_dialog(url); }); + dialog.ShowModal(); + } } if (!config_substitutions.empty()) show_substitutions_info(config_substitutions.substitutions, filename.string()); @@ -5016,7 +5031,8 @@ bool Plater::preview_zip_archive(const boost::filesystem::path& archive_path) break; } // if 3mf - read archive headers to find project file - if ((boost::algorithm::iends_with(filename, ".3mf") && !is_project_3mf(final_path.string())) || + auto [is_project, ps_version] = is_project_3mf(final_path.string()); + if ((boost::algorithm::iends_with(filename, ".3mf") && !is_project) || (boost::algorithm::iends_with(filename, ".amf") && !boost::algorithm::iends_with(filename, ".zip.amf"))) { non_project_paths.emplace_back(final_path); break; @@ -5236,16 +5252,19 @@ bool Plater::load_files(const wxArrayString& filenames, bool delete_after_load/* std::string filename = (*it).filename().string(); bool handle_as_project = boost::algorithm::iends_with(filename, ".3mf"); - if (boost::algorithm::iends_with(filename, ".zip") && is_project_3mf(it->string())) { + auto [has_config, ps_version] = is_project_3mf(it->string()); + + if (boost::algorithm::iends_with(filename, ".zip") && has_config) { BOOST_LOG_TRIVIAL(warning) << "File with .zip extension is 3mf project, opening as it would have .3mf extension: " << *it; handle_as_project = true; } if (handle_as_project && load_just_one_file) { ProjectDropDialog::LoadType load_type = ProjectDropDialog::LoadType::Unknown; { - if (boost::algorithm::iends_with(filename, ".3mf") && !is_project_3mf(it->string())) + if (boost::algorithm::iends_with(filename, ".3mf") && (!has_config && !ps_version.has_value())) { + // Projects generated by PrusaSlicer is expected to have config. If absent, it will be reported later. load_type = ProjectDropDialog::LoadType::LoadGeometry; - else { + } else { if (wxGetApp().app_config->get_bool("show_drop_project_dialog")) { ProjectDropDialog dlg(filename); if (dlg.ShowModal() == wxID_OK) {