diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index eb30763ce6..4108ee210d 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -3135,7 +3135,9 @@ std::pair PresetCollection::load_external_preset( } // Insert a new profile. //BBS: add project embedded preset logic - bool from_project = boost::algorithm::iends_with(name, ".3mf"); + // A G-code's settings are shown like a project's, so a preset made from them lives with the project + // rather than in the user's library. + bool from_project = boost::algorithm::iends_with(name, ".3mf") || is_gcode_file(name); if (m_type == Preset::TYPE_PRINT) cfg.option("print_settings_id", true)->value = new_name; else if (m_type == Preset::TYPE_FILAMENT) diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp index a817ebdde9..711501aecf 100644 --- a/src/libslic3r/PresetBundle.cpp +++ b/src/libslic3r/PresetBundle.cpp @@ -3898,6 +3898,8 @@ void PresetBundle::load_selections(AppConfig &config, const PresetPreferences& p // BBS: change directories by design void PresetBundle::export_selections(AppConfig &config) { + if (m_selection_export_blockers > 0) + return; assert(this->printers.get_edited_preset().printer_technology() != ptFFF || filament_presets.size() >= 1); //assert(this->printers.get_edited_preset().printer_technology() != ptFFF || filament_presets.size() > 1 || filaments.get_selected_preset_name() == filament_presets.front()); config.clear_section("presets"); @@ -5524,12 +5526,10 @@ DynamicPrintConfig PresetBundle::full_sla_config() const ConfigSubstitutions PresetBundle::load_config_file(const std::string &path, ForwardCompatibilitySubstitutionRule compatibility_rule) { if (is_gcode_file(path)) { - DynamicPrintConfig config; //BBS: add config related logs BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" enter, gcodefile %1%, compatibility_rule %2%")%path %compatibility_rule; - config.apply(FullPrintConfig::defaults()); - ConfigSubstitutions config_substitutions = config.load_from_gcode_file(path, compatibility_rule); - Preset::normalize(config); + ConfigSubstitutions config_substitutions; + DynamicPrintConfig config = config_from_gcode(path, compatibility_rule, config_substitutions); load_config_file_config(path, true, std::move(config)); return config_substitutions; } @@ -5537,10 +5537,83 @@ ConfigSubstitutions PresetBundle::load_config_file(const std::string &path, Forw //BBS: add config related logs BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" can not load config file %1% not from gcode")%path ; throw Slic3r::RuntimeError((boost::format(L("Unknown configuration file: %1%")) % path).str()); - + return ConfigSubstitutions{}; } +DynamicPrintConfig PresetBundle::config_from_gcode(const std::string &path, ForwardCompatibilitySubstitutionRule compatibility_rule, + ConfigSubstitutions &substitutions) const +{ + DynamicPrintConfig config; + config.apply(FullPrintConfig::defaults()); + // The config block leaves a few project settings out (filament_colour_type, prime_volume_mode, + // flush_multiplier_fast); those keep this bundle's values rather than falling back to the defaults. + config.apply(this->project_config); + const DynamicPrintConfig base = config; + substitutions = config.load_from_gcode_file(path, compatibility_rule); + // An empty list of choices sets nothing: G-code written by a tool rather than from presets leaves per-extruder + // lists such as nozzle_volume_type empty, and the settings tabs expect a choice per extruder. An empty text + // list, such as filament_multi_colour, is a value of its own and stays. + for (const std::string &key : config.keys()) + if (const ConfigOption *opt = config.option(key); opt->type() == coEnums && static_cast(opt)->size() == 0) + if (const ConfigOption *below = base.option(key); below != nullptr && static_cast(below)->size() > 0) + config.set_key_value(key, below->clone()); + Preset::normalize(config); + // The block holds flush_volumes_matrix already multiplied by the multiplier the slice used, so a + // multiplier of 1 keeps the purge volumes the file was sliced with. G-code from before 2.3.2 held the + // plain matrix and loses a multiplier other than 1. + for (const char *key : { "flush_multiplier", "flush_multiplier_fast" }) + if (auto *multiplier = config.option(key)) + std::fill(multiplier->values.begin(), multiplier->values.end(), 1.); + return config; +} + +std::optional PresetBundle::selection() const +{ + auto capture = [](const PresetCollection &presets, Selection::Collection &out) { + if (std::any_of(presets.m_presets.begin(), presets.m_presets.end(), [](const Preset &preset) { return preset.is_project_embedded; })) + return false; + out.selected = presets.get_selected_preset_name(); + out.edited = presets.get_edited_preset().config; + out.saved = presets.m_saved_preset.config; + out.visible.reserve(presets.m_presets.size()); + for (const Preset &preset : presets.m_presets) + out.visible.push_back(preset.is_visible); + return true; + }; + Selection selection; + if (!capture(this->prints, selection.prints) || !capture(this->filaments, selection.filaments) || !capture(this->printers, selection.printers)) + return std::nullopt; + selection.filament_presets = this->filament_presets; + selection.project_config = this->project_config; + selection.extruder_ams_counts = this->extruder_ams_counts; + return selection; +} + +void PresetBundle::restore_selection(const Selection &selection) +{ + auto restore = [](PresetCollection &presets, const Selection::Collection &saved) { + presets.reset_project_embedded_presets(); + // Before selecting: select_preset_by_name() passes over a hidden preset. + assert(presets.m_presets.size() == saved.visible.size()); + for (size_t i = 0; i < std::min(presets.m_presets.size(), saved.visible.size()); ++i) + presets.m_presets[i].is_visible = saved.visible[i]; + presets.select_preset_by_name(saved.selected, true); + presets.m_saved_preset.config = saved.saved; + presets.get_edited_preset().config = saved.edited; + presets.update_dirty(); + }; + restore(this->printers, selection.printers); + restore(this->prints, selection.prints); + restore(this->filaments, selection.filaments); + this->update_compatible(PresetSelectCompatibleType::Never); + this->update_multi_material_filament_presets(); + // Last, as update_multi_material_filament_presets() resizes filament_presets and the flush data in project_config. + this->filament_presets = selection.filament_presets; + this->project_config = selection.project_config; + this->extruder_ams_counts = selection.extruder_ams_counts; +} + //some filament presets split from one to sperate ones //following map recording these filament presets diff --git a/src/libslic3r/PresetBundle.hpp b/src/libslic3r/PresetBundle.hpp index 8da0e0e196..9d41746aee 100644 --- a/src/libslic3r/PresetBundle.hpp +++ b/src/libslic3r/PresetBundle.hpp @@ -397,6 +397,28 @@ public: std::vector get_current_project_embedded_presets(); void reset_project_embedded_presets(); + // What importing a file's settings replaces, so that an import which fails part-way, or whose + // file is then rejected, can be undone without copying every preset. + struct Selection + { + struct Collection + { + std::string selected; + DynamicPrintConfig edited; + // The baseline saved_is_dirty() compares against, which feeds the project dirty state. + DynamicPrintConfig saved; + // By index: an import only inserts project-embedded presets, which a restore drops first. + std::vector visible; + }; + Collection prints, filaments, printers; + std::vector filament_presets; + DynamicPrintConfig project_config; + std::vector> extruder_ams_counts; + }; + // Nothing while project-embedded presets exist: a restore drops them, which would lose a loaded project's presets. + std::optional selection() const; + void restore_selection(const Selection &selection); + //BBS: find printer model std::string get_texture_for_printer_model(std::string model_name); std::string get_stl_model_for_printer_model(std::string model_name); @@ -405,6 +427,21 @@ public: // Export selections (current print, current filaments, current printer) into config.ini void export_selections(AppConfig &config); + // While one lives, export_selections() writes nothing, so a selection only shown for a while (the + // settings of a G-code being previewed) is not remembered as the user's. It also covers the + // exports the GUI makes on its own while refreshing. + class SelectionExportBlocker + { + public: + explicit SelectionExportBlocker(PresetBundle &bundle) : m_bundle(bundle) { ++m_bundle.m_selection_export_blockers; } + ~SelectionExportBlocker() { --m_bundle.m_selection_export_blockers; } + SelectionExportBlocker(const SelectionExportBlocker &) = delete; + SelectionExportBlocker &operator=(const SelectionExportBlocker &) = delete; + + private: + PresetBundle &m_bundle; + }; + // n is the total slot count, and growth appends at the raw tail - which is where the mixed // slots live. A caller adding physical filaments has to add num_mixed_filaments() on top and // then move the new slots ahead of the mixed tail, as Sidebar::add_custom_filament does. @@ -546,6 +583,11 @@ public: // If the file is loaded successfully, its print / filament / printer profiles will be activated. ConfigSubstitutions load_config_file(const std::string &path, ForwardCompatibilitySubstitutionRule compatibility_rule); + // The settings an OrcaSlicer G-code was sliced with, from its config block, ready for load_config_model(). + // Throws when the file carries no such block (another slicer, or the block was left out). + DynamicPrintConfig config_from_gcode(const std::string &path, ForwardCompatibilitySubstitutionRule compatibility_rule, + ConfigSubstitutions &substitutions) const; + // Load a config bundle file, into presets and store the loaded presets into separate files // of the local configuration directory. // Load settings into the provided settings instance. @@ -896,6 +938,9 @@ private: std::string vendor_to_validate = ""; int m_errors = 0; + // The live SelectionExportBlockers. + int m_selection_export_blockers = 0; + // Helper function: save preset to bundle directory with common logic bool save_preset_to_bundle_dir(Preset& preset, PresetCollection* collection, const std::string& bundle_id, const std::string& type_subdir, diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index 19bae7c0dd..fbfd44a475 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -1797,7 +1797,9 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const } else {*/ //BBS: add only gcode mode - if (m_only_gcode_in_preview) { + // When the file's own settings were loaded into the presets, the bed already is the file's, with the + // model and texture of a user or custom printer too, which the lookup below cannot resolve. + if (m_only_gcode_in_preview && !wxGetApp().plater()->gcode_settings_loaded()) { Pointfs printable_area; //BBS: add bed exclude area Pointfs bed_exclude_area = Pointfs(); diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 303f50d8d4..cf9dddeee7 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -6013,7 +6013,8 @@ void Sidebar::add_custom_filament(wxColour new_col, const std::string& preset_na bool Sidebar::is_new_project_in_gcode3mf() { - if (p->plater->using_exported_file()) { + // A .gcode previewed with its own settings shows them here too, and is closed the same way. + if (p->plater->using_exported_file() || p->plater->gcode_settings_loaded()) { auto filename = p->plater->get_preview_only_filename(); auto text = wxString::Format(_L("After completing your operation, %s project will be closed and create a new project."), filename); MessageDialog dlg(wxGetApp().plater(), text, _L("Warning"), @@ -7367,6 +7368,9 @@ struct Plater::priv void enable_sidebar(bool enabled); void collapse_sidebar(bool collapse); void update_sidebar(bool force_update = false); + // The sidebar shows the selected presets, which a G-code previewed without its own settings has nothing to do + // with, so it stays hidden then. With the file's settings loaded it shows them, as for a .gcode.3mf. + bool gcode_preview_without_settings() const { return q->m_only_gcode && !q->m_gcode_settings_loaded; } void reset_window_layout(); Sidebar::DockingState get_sidebar_docking_state(); void add_dock_pane(wxWindow* window, const std::string& name, const wxString& caption, const std::string& dock, @@ -8592,7 +8596,7 @@ void Plater::priv::select_next_view_3D() void Plater::priv::enable_sidebar(bool enabled) { - if (q->m_only_gcode) + if (gcode_preview_without_settings()) enabled = false; sidebar_layout.is_enabled = enabled; @@ -8601,7 +8605,7 @@ void Plater::priv::enable_sidebar(bool enabled) void Plater::priv::collapse_sidebar(bool collapse) { - if (q->m_only_gcode) + if (gcode_preview_without_settings()) return; sidebar_layout.is_collapsed = collapse; @@ -8864,6 +8868,64 @@ static void log_substitutions(const PresetsConfigSubstitutions& substitutions, c log_substitutions(preset.substitutions, source + " (preset " + preset.preset_name + ")"); } +// After the settings of a project or a G-code were loaded into the presets: size the per-filament project +// lists to the loaded filament count, and drop state only a connected printer may set. +static void sync_loaded_project_filament_config(DynamicConfig& proj_cfg) +{ + ConfigOptionStrings* filament_color = proj_cfg.opt("filament_colour"); + if (filament_color) { + size_t filament_count = filament_color->size(); + + // Sync filament map + ConfigOptionInts* filament_map = proj_cfg.opt("filament_map", true); + if (filament_map->size() != filament_count) { + filament_map->values.resize(filament_count, 1); + } + + ConfigOptionInts* filament_nozzle_map = proj_cfg.opt("filament_nozzle_map", true); + if (filament_nozzle_map->size() != filament_count) { + filament_nozzle_map->values.resize(filament_count, 0); + } + + ConfigOptionInts* filament_volume_map = proj_cfg.opt("filament_volume_map", true); + if (filament_volume_map->size() != filament_count) { + filament_volume_map->values.resize(filament_count, static_cast(NozzleVolumeType::nvtStandard)); + } + + // Sync filament multi colour + ConfigOptionStrings* filament_multi_color = proj_cfg.opt("filament_multi_colour", true); + if (filament_multi_color->size() != filament_count) { + filament_multi_color->values.resize(filament_count); + } + // If there is no multi-color data or color is not match, use single color as default value + for (size_t i = 0; i < filament_count; i++) { + std::vector colors = Slic3r::split_string(filament_multi_color->values[i], ' '); + if (i >= filament_multi_color->values.size() || colors.empty() || colors[0] != filament_color->values[i] ) { + filament_multi_color->values[i] = filament_color->values[i]; + } + } + // Sync filament colour type + ConfigOptionStrings* filament_color_type = proj_cfg.opt("filament_colour_type", true); + if (filament_color_type && filament_color_type->size() != filament_count) { + filament_color_type->values.resize(filament_count); + + for (size_t i = 0; i < filament_count; i++) { + if (i >= filament_color_type->values.size() || filament_color_type->values[i].empty()) { + filament_color_type->values[i] = "1"; + } + } + } + } + + // Filament Track Switch state is derived live from the connected + // printer; a loaded project must not carry a stale installed/active + // flag, so clear both and let device sync re-derive them. + if (auto* has_switcher = proj_cfg.opt("has_filament_switcher")) + has_switcher->value = false; + if (auto* dynamic_map = proj_cfg.opt("enable_filament_dynamic_map")) + dynamic_map->value = false; +} + // BBS: backup & restore std::vector Plater::priv::load_files(const std::vector& input_files, LoadStrategy strategy, @@ -9680,58 +9742,7 @@ std::vector Plater::priv::load_files(const std::vector& input_ if (file_wipe_tower_y) *wipe_tower_y = *file_wipe_tower_y; - ConfigOptionStrings* filament_color = proj_cfg.opt("filament_colour"); - if (filament_color) { - size_t filament_count = filament_color->size(); - - // Sync filament map - ConfigOptionInts* filament_map = proj_cfg.opt("filament_map", true); - if (filament_map->size() != filament_count) { - filament_map->values.resize(filament_count, 1); - } - - ConfigOptionInts* filament_nozzle_map = proj_cfg.opt("filament_nozzle_map", true); - if (filament_nozzle_map->size() != filament_count) { - filament_nozzle_map->values.resize(filament_count, 0); - } - - ConfigOptionInts* filament_volume_map = proj_cfg.opt("filament_volume_map", true); - if (filament_volume_map->size() != filament_count) { - filament_volume_map->values.resize(filament_count, static_cast(NozzleVolumeType::nvtStandard)); - } - - // Sync filament multi colour - ConfigOptionStrings* filament_multi_color = proj_cfg.opt("filament_multi_colour", true); - if (filament_multi_color->size() != filament_count) { - filament_multi_color->values.resize(filament_count); - } - // If there is no multi-color data or color is not match, use single color as default value - for (size_t i = 0; i < filament_count; i++) { - std::vector colors = Slic3r::split_string(filament_multi_color->values[i], ' '); - if (i >= filament_multi_color->values.size() || colors.empty() || colors[0] != filament_color->values[i] ) { - filament_multi_color->values[i] = filament_color->values[i]; - } - } - // Sync filament colour type - ConfigOptionStrings* filament_color_type = proj_cfg.opt("filament_colour_type", true); - if (filament_color_type && filament_color_type->size() != filament_count) { - filament_color_type->values.resize(filament_count); - - for (size_t i = 0; i < filament_count; i++) { - if (i >= filament_color_type->values.size() || filament_color_type->values[i].empty()) { - filament_color_type->values[i] = "1"; - } - } - } - } - - // Filament Track Switch state is derived live from the connected - // printer; a loaded project must not carry a stale installed/active - // flag, so clear both and let device sync re-derive them. - if (auto* has_switcher = proj_cfg.opt("has_filament_switcher")) - has_switcher->value = false; - if (auto* dynamic_map = proj_cfg.opt("enable_filament_dynamic_map")) - dynamic_map->value = false; + sync_loaded_project_filament_config(proj_cfg); } // Update filament combobox after loading config if (published_config.published) { @@ -12560,7 +12571,7 @@ void Plater::priv::set_current_panel(wxPanel* panel, bool no_slice) // Add sidebar and toolbar collapse logic if (panel == view3D || panel == preview) { - this->enable_sidebar(!q->only_gcode_mode()); + this->enable_sidebar(true); } if (panel == preview) { if (q->only_gcode_mode()) { @@ -18067,11 +18078,80 @@ void Plater::load_gcode(const wxString& filename) m_last_loaded_gcode = filename; + // An OrcaSlicer G-code carries the settings it was sliced with. They replace the selected presets as a + // .gcode.3mf's do, so unsaved preset changes get the save-or-discard choice of opening a project rather + // than the new project's choice to keep them. + PresetBundle* preset_bundle = wxGetApp().preset_bundle; + const std::string path = into_u8(filename); + std::optional file_config; + ConfigSubstitutions substitutions; + { + wxBusyCursor wait; + try { + file_config = preset_bundle->config_from_gcode(path, ForwardCompatibilitySubstitutionRule::Enable, substitutions); + } catch (const std::exception& ex) { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": no settings to load from " << path << ": " << ex.what(); + } + } + // BSS: create a new project when load_gcode, force close previous one - if (new_project(false, true) != wxID_YES) + if (file_config) { + auto check = [](bool yes_or_no) { + return yes_or_no || wxGetApp().check_and_save_current_preset_changes(_L("Load project"), _L("Some presets are modified.")); + }; + if (close_with_confirm(check) == wxID_CANCEL || new_project(true, true) != wxID_YES) + return; + } else if (new_project(false, true) != wxID_YES) return; m_only_gcode = true; + m_gcode_settings_loaded = false; + // A previewed G-code's settings are not the selection the app should remember. + PresetBundle::SelectionExportBlocker no_selection_export(*preset_bundle); + // What loading the file's settings replaces, to put back if that load fails or the file is rejected. + std::optional previous; + if (file_config) { + previous = preset_bundle->selection(); + if (!previous) + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": project presets are still loaded, not loading the settings of " << path; + } + auto unload_file_settings = [this, preset_bundle, &previous]() { + if (!previous) + return; + preset_bundle->restore_selection(*previous); + wxGetApp().load_current_presets(false, false); + // Switching back to the previous printer re-derives some project settings (TabPrinter::on_preset_loaded), + // and only the printer refresh shows them in the sidebar's flow selectors. + preset_bundle->project_config = std::move(previous->project_config); + sidebar().update_presets(Preset::TYPE_PRINTER); + reset_project_dirty_initial_presets(); + update_project_dirty_from_presets(); + m_gcode_settings_loaded = false; + // The rejections after the switch to the preview have already shown the sidebar with the file's settings. + enable_sidebar(false); + previous.reset(); + }; + if (previous) { + try { + preset_bundle->load_config_model(path, std::move(*file_config)); + m_gcode_settings_loaded = true; + } catch (const std::exception& ex) { + unload_file_settings(); + get_notification_manager()->push_notification(NotificationType::CustomNotification, + NotificationManager::NotificationLevel::WarningNotificationLevel, + GUI::format(_L("Failed loading file \"%1%\". An invalid configuration was found."), path) + + "\n" + ex.what()); + } + } + if (m_gcode_settings_loaded) { + wxGetApp().load_current_presets(false, false); + sync_loaded_project_filament_config(preset_bundle->project_config); + sidebar().update_presets(Preset::TYPE_FILAMENT); + reset_project_dirty_initial_presets(); + update_project_dirty_from_presets(); + if (!substitutions.empty()) + show_substitutions_info(substitutions, path); + } // cleanup view before to start loading/processing //BBS: update gcode to current partplate's @@ -18099,6 +18179,7 @@ void Plater::load_gcode(const wxString& filename) } catch (const std::exception& ex) { + unload_file_settings(); show_error(this, ex.what()); return; } @@ -18146,6 +18227,7 @@ void Plater::load_gcode(const wxString& filename) p->preview->get_canvas3d()->zoom_to_plate(0); if (p->preview->get_canvas3d()->get_gcode_layers_zs().empty()) { + unload_file_settings(); MessageDialog(this, format_wxstr(_L("The selected file:\n%1%\ndoes not contain valid G-code."), filename), wxString(GCODEVIEWER_APP_NAME) + " - " + _L("An Error has occurred while loading the G-code file."), wxCLOSE | wxICON_WARNING | wxCENTRE).ShowModal(); set_project_filename(_L(DEFAULT_PROJECT_NAME)); diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp index b3a2954b33..0bb2d65add 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -418,6 +418,9 @@ public: bool is_gcode_3mf() { return m_exported_file; } bool only_gcode_mode() { return m_only_gcode; } void set_only_gcode(bool only_gcode) { m_only_gcode = only_gcode; } + // The previewed G-code's own settings were loaded into the presets, as a .gcode.3mf's are: the bed already is the + // one it was sliced for, and the sidebar shows them. + bool gcode_settings_loaded() const { return m_only_gcode && m_gcode_settings_loaded; } //BBS: add only gcode mode bool using_exported_file() { return m_exported_file; } @@ -1082,6 +1085,8 @@ private: wxString m_last_loaded_gcode; //BBS: add only gcode mode bool m_only_gcode { false };//just for .gcode file not for .gcode.3mf + // Set by load_gcode() for the file it previews, so it means nothing once m_only_gcode is cleared. + bool m_gcode_settings_loaded { false }; bool m_exported_file { false }; bool skip_thumbnail_invalid { false }; bool m_loading_project { false }; diff --git a/tests/fff_print/test_gcode.cpp b/tests/fff_print/test_gcode.cpp index 7bd36481db..f63e30a07f 100644 --- a/tests/fff_print/test_gcode.cpp +++ b/tests/fff_print/test_gcode.cpp @@ -3,17 +3,25 @@ #include #include "libslic3r/BoundingBox.hpp" +#include "libslic3r/Config.hpp" #include "libslic3r/Model.hpp" #include "libslic3r/ModelArrange.hpp" #include "libslic3r/Print.hpp" #include "libslic3r/PrintConfig.hpp" +#include "libslic3r/Preset.hpp" +#include "libslic3r/PresetBundle.hpp" #include "test_helpers.hpp" +#include "test_utils.hpp" #include "libslic3r/TriangleMesh.hpp" #include "libslic3r/Point.hpp" +#include +#include #include #include +#include +#include using namespace Slic3r; @@ -41,3 +49,39 @@ TEST_CASE("Klipper object labels name each copy without the characters Klipper c CHECK(gcode.find("EXCLUDE_OBJECT_START NAME=" + instance_label + "\n") != std::string::npos); } } + +TEST_CASE("An exported G-code's settings load back as the settings it was sliced with", "[GCode]") +{ + // Two filaments, so the config block carries the flush matrix already multiplied by flush_multiplier. + const DynamicPrintConfig config = Test::multifilament_config(2, { + { "flush_multiplier", "0.5" }, + { "printable_area", "0x0,250x0,250x210,0x210" }, + { "printable_height", "180" }, + }); + Print print; + Model model; + Test::init_print(std::vector{ Test::cube(20.) }, print, model, config, nullptr, false); + ScopedTemporaryFile file(".gcode"); + { + std::ofstream os(file.string(), std::ios::binary); + os << Test::gcode(print); + REQUIRE(os.good()); + } + + PresetBundle bundle; + // The config block leaves prime_volume_mode out, so the receiver's choice has to survive the load. + bundle.project_config.option>("prime_volume_mode")->value = PrimeVolumeMode::pvmFast; + ConfigSubstitutions substitutions; + bundle.load_config_model(file.string(), bundle.config_from_gcode(file.string(), ForwardCompatibilitySubstitutionRule::Enable, substitutions)); + + const DynamicPrintConfig &printer = bundle.printers.get_edited_preset().config; + CHECK(printer.opt_serialize("printable_area") == config.opt_serialize("printable_area")); + CHECK_THAT(printer.opt_float("printable_height"), Catch::Matchers::WithinAbs(180., 1e-6)); + // The purge between the two filaments is what the slice used: 280 at a 0.5 multiplier. + const std::vector &matrix = bundle.project_config.option("flush_volumes_matrix")->values; + const std::vector &multiplier = bundle.project_config.option("flush_multiplier")->values; + REQUIRE(matrix.size() == 4); + REQUIRE(multiplier.size() == 1); + CHECK_THAT(matrix[1] * multiplier[0], Catch::Matchers::WithinAbs(140., 1e-6)); + CHECK(bundle.project_config.option>("prime_volume_mode")->value == PrimeVolumeMode::pvmFast); +} diff --git a/tests/libslic3r/test_preset_bundle_loading.cpp b/tests/libslic3r/test_preset_bundle_loading.cpp index 2bc87ba51b..a8ff462d37 100644 --- a/tests/libslic3r/test_preset_bundle_loading.cpp +++ b/tests/libslic3r/test_preset_bundle_loading.cpp @@ -38,6 +38,7 @@ #include "libslic3r/Config.hpp" #include #include +#include #include #include "libslic3r/libslic3r.h" #include "libslic3r/PublishSettings.hpp" @@ -7492,3 +7493,158 @@ TEST_CASE("Migrating the selected user filament keeps a printer list edit pendin // What marks the project as having unsaved preset changes. CHECK(filaments.saved_is_dirty() == expected_dirty); } + +namespace { + +// The settings a file sliced on another machine carries: print, printer and filament presets this +// bundle does not have installed. +DynamicPrintConfig sender_config(size_t filament_count) +{ + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.opt("filament_diameter")->values.assign(filament_count, 1.75); + config.opt("filament_colour")->values.assign(filament_count, "#FF0000"); + Preset::normalize(config); + config.opt_string("print_settings_id", true) = "Sender Process"; + config.opt_string("printer_settings_id", true) = "Sender Printer"; + std::vector &filaments = config.option("filament_settings_id", true)->values; + for (size_t i = 0; i < filament_count; ++i) + filaments.push_back("Sender Filament " + std::to_string(i + 1)); + return config; +} + +size_t project_preset_count(const PresetBundle &bundle) +{ + size_t count = 0; + for (const PresetCollection *presets : std::initializer_list{ &bundle.prints, &bundle.filaments, &bundle.printers }) + count += std::count_if(presets->begin(), presets->end(), [](const Preset &preset) { return preset.is_project_embedded; }); + return count; +} + +size_t files_under(const fs::path &dir) +{ + size_t count = 0; + for (const fs::directory_entry &entry : fs::recursive_directory_iterator(dir)) + count += fs::is_regular_file(entry.path()); + return count; +} + +} // namespace + +TEST_CASE("Presets a G-code names but the receiver lacks load as temporary project presets", "[Preset][Bundle]") +{ + // A G-code's settings select presets the way a .gcode.3mf's do, without adding to the user's library. + const std::string source = GENERATE("part.gcode", "part.gcode.3mf"); + CAPTURE(source); + ScopedTemporaryDir dir; + ScopedDataDir data(dir.path()); + PresetBundle bundle; + bundle.update_user_presets_directory(DEFAULT_USER_FOLDER_NAME); + + bundle.load_config_model(source, sender_config(1)); + + for (const PresetCollection *presets : std::initializer_list{ &bundle.prints, &bundle.filaments, &bundle.printers }) { + CAPTURE(presets->get_selected_preset_name()); + CHECK(presets->get_selected_preset().is_project_embedded); + } + CHECK(files_under(dir.path()) == 0); + + bundle.reset_project_embedded_presets(); + CHECK(project_preset_count(bundle) == 0); +} + +TEST_CASE("Restoring a selection undoes an import that failed part-way", "[Preset][Bundle]") +{ + ScopedTemporaryDir dir; + ScopedDataDir data(dir.path()); + PresetBundle bundle; + bundle.update_user_presets_directory(DEFAULT_USER_FOLDER_NAME); + add_inmemory_preset(bundle.prints, "My Process"); + add_inmemory_preset(bundle.prints, "Sender Process"); + // The file names an installed but hidden process, which the import makes visible. + bundle.prints.find_preset("Sender Process", false, true)->is_visible = false; + REQUIRE(bundle.prints.select_preset_by_name("My Process", true)); + // An unsaved edit that the saved baseline already holds, as new_project leaves it. + bundle.prints.get_edited_preset().config.opt_float("layer_height") = 0.12; + bundle.prints.update_dirty(); + bundle.prints.update_saved_preset_from_current_preset(); + REQUIRE(bundle.prints.get_edited_preset().is_dirty); + REQUIRE_FALSE(bundle.prints.saved_is_dirty()); + + const std::string printer_before = bundle.printers.get_selected_preset_name(); + const std::vector filaments_before = bundle.filament_presets; + const DynamicPrintConfig project_before = bundle.project_config; + const std::optional previous = bundle.selection(); + REQUIRE(previous); + + // Two filaments, one with a per-filament list the filament split cannot read: the load throws after + // the print and printer presets are selected. + DynamicPrintConfig config = sender_config(2); + config.option("filament_dev_ams_drying_ams_limitations", true)->values.clear(); + REQUIRE_THROWS_AS(bundle.load_config_model("part.gcode", std::move(config)), ConfigurationError); + REQUIRE(bundle.prints.get_selected_preset_name() == "Sender Process"); + + bundle.restore_selection(*previous); + + CHECK(bundle.prints.get_selected_preset_name() == "My Process"); + CHECK_THAT(bundle.prints.get_edited_preset().config.opt_float("layer_height"), Catch::Matchers::WithinAbs(0.12, 1e-9)); + CHECK(bundle.prints.get_edited_preset().is_dirty); + CHECK_FALSE(bundle.prints.saved_is_dirty()); + CHECK_FALSE(bundle.prints.find_preset("Sender Process", false, true)->is_visible); + CHECK(bundle.printers.get_selected_preset_name() == printer_before); + CHECK_FALSE(bundle.printers.saved_is_dirty()); + CHECK(bundle.filament_presets == filaments_before); + CHECK(bundle.project_config == project_before); + CHECK(project_preset_count(bundle) == 0); +} + +TEST_CASE("A selection is not taken while project presets are loaded", "[Preset][Bundle]") +{ + // Restoring drops every project-embedded preset, which would lose those of the project loaded now. + ScopedTemporaryDir dir; + ScopedDataDir data(dir.path()); + PresetBundle bundle; + bundle.update_user_presets_directory(DEFAULT_USER_FOLDER_NAME); + bundle.load_config_model("project.3mf", sender_config(1)); + REQUIRE(project_preset_count(bundle) > 0); + + CHECK_FALSE(bundle.selection()); +} + +TEST_CASE("Selections reach the app config only while no export blocker lives", "[Preset][Bundle]") +{ + PresetBundle bundle; + AppConfig app_config; + { + PresetBundle::SelectionExportBlocker blocker(bundle); + bundle.export_selections(app_config); + CHECK(app_config.get("presets", PRESET_PRINTER_NAME).empty()); + } + bundle.export_selections(app_config); + CHECK(app_config.get("presets", PRESET_PRINTER_NAME) == bundle.printers.get_selected_preset_name()); +} + +TEST_CASE("An empty choice list in a G-code's settings keeps the value it had before the file was read", "[Preset][Bundle]") +{ + // A G-code written by a tool rather than from presets can leave nozzle_volume_type empty, and the settings + // tabs index it per extruder. A blank text list is a value of its own and stays blank. + const DynamicPrintConfig block = DynamicPrintConfig::full_print_config(); + ScopedTemporaryFile file(".gcode"); + { + std::ofstream os(file.string()); + os << "; generated by OrcaSlicer\n; CONFIG_BLOCK_START\n"; + for (const std::string &key : block.keys()) + if (key != "nozzle_volume_type" && key != "filament_multi_colour") + os << "; " << key << " = " << block.opt_serialize(key) << '\n'; + os << "; nozzle_volume_type = \n; filament_multi_colour = \n; CONFIG_BLOCK_END\n"; + REQUIRE(os.good()); + } + + PresetBundle bundle; + bundle.project_config.set_deserialize_strict("nozzle_volume_type", "High Flow"); + bundle.project_config.option("filament_multi_colour", true)->values = { "#FF0000 #0000FF" }; + ConfigSubstitutions substitutions; + const DynamicPrintConfig config = bundle.config_from_gcode(file.string(), ForwardCompatibilitySubstitutionRule::Enable, substitutions); + + CHECK(config.opt_serialize("nozzle_volume_type") == "High Flow"); + CHECK(config.option("filament_multi_colour")->values.empty()); +}