Support for forward compatibility of configurations, user and system

config bundles, project files (3MFs, AMFs). When loading these files,
the caller may decide whether to substitute some of the configuration
values the current PrusaSlicer version does not understand with
some reasonable default value, and whether to report it. If substitution
is disabled, an exception is being thrown as before this commit.
If substitution is enabled, list of substitutions is returned by the
API to be presented to the user. This allows us to introduce for example
new firmware flavor key in PrusaSlicer 2.4 while letting PrusaSlicer
2.3.2 to fall back to some default and to report it to the user.

When slicing from command line, substutions are performed by default
and reported into the console, however substitutions may be either
disabled or made silent with the new "config-compatibility" command
line option.

Substitute enums and bools only.  Allow booleans to be parsed as
    true: "1", "enabled", "on" case insensitive
    false: "0", "disabled", "off" case insensitive
This will allow us in the future for example to switch the draft_shield
boolean to an enum with the following values: "disabled" / "enabled" / "limited".

Added "enum_bitmask.hpp" - support for type safe sets of options.
See for example PresetBundle::load_configbundle(...
LoadConfigBundleAttributes flags) for an example of intended usage.

WIP: GUI for reporting the list of config substitutions needs to be
implemented by @YuSanka.
This commit is contained in:
Vojtech Bubnik 2021-06-27 16:04:23 +02:00
parent ad336e2cc0
commit 0f3cabb5d9
47 changed files with 643 additions and 314 deletions

View file

@ -64,7 +64,9 @@ bool Bundle::load(fs::path source_path, bool ais_in_resources, bool ais_prusa_bu
this->is_prusa_bundle = ais_prusa_bundle;
std::string path_string = source_path.string();
size_t presets_loaded = preset_bundle->load_configbundle(path_string, PresetBundle::LOAD_CFGBNDLE_SYSTEM);
auto [config_substitutions, presets_loaded] = preset_bundle->load_configbundle(path_string, PresetBundle::LoadConfigBundleAttribute::LoadSystem);
// No substitutions shall be reported when loading a system config bundle, no substitutions are allowed.
assert(config_substitutions.empty());
auto first_vendor = preset_bundle->vendors.begin();
if (first_vendor == preset_bundle->vendors.end()) {
BOOST_LOG_TRIVIAL(error) << boost::format("Vendor bundle: `%1%`: No vendor information defined, cannot install.") % path_string;
@ -2592,7 +2594,10 @@ void ConfigWizard::priv::apply_config(AppConfig *app_config, PresetBundle *prese
}
}
preset_bundle->load_presets(*app_config, preferred_model);
// Reloading the configs after some modifications were done to PrusaSlicer.ini.
// Just perform the substitutions silently, as the substitutions were already presented to the user on application start-up
// and the Wizard shall not create any new values that would require substitution.
PresetsConfigSubstitutions substitutions = preset_bundle->load_presets(*app_config, ForwardCompatibilitySubstitutionRule::EnableSilent, preferred_model);
if (page_custom->custom_wanted()) {
page_firmware->apply_custom_config(*custom_config);

View file

@ -624,6 +624,13 @@ void GUI_App::post_init()
this->plater()->load_gcode(wxString::FromUTF8(this->init_params->input_files[0].c_str()));
}
else {
if (! this->init_params->preset_substitutions.empty()) {
// TODO: Add list of changes from all_substitutions
show_error(nullptr, GUI::format(_L("Loading profiles found following incompatibilities."
" To recover these files, incompatible values were changed to default values."
" But data in files won't be changed until you save them in PrusaSlicer.")));
}
#if 0
// Load the cummulative config over the currently active profiles.
//FIXME if multiple configs are loaded, only the last one will have an effect.
@ -652,6 +659,24 @@ void GUI_App::post_init()
if (! this->init_params->extra_config.empty())
this->mainframe->load_config(this->init_params->extra_config);
}
// The extra CallAfter() is needed because of Mac, where this is the only way
// to popup a modal dialog on start without screwing combo boxes.
// This is ugly but I honestly found no better way to do it.
// Neither wxShowEvent nor wxWindowCreateEvent work reliably.
if (this->preset_updater) {
this->check_updates(false);
CallAfter([this] {
this->config_wizard_startup();
this->preset_updater->slic3r_update_notify();
this->preset_updater->sync(preset_bundle);
});
}
#ifdef _WIN32
// Sets window property to mainframe so other instances can indentify it.
OtherInstanceMessageHandler::init_windows_properties(mainframe, m_instance_hash_int);
#endif //WIN32
}
IMPLEMENT_APP(GUI_App)
@ -885,7 +910,7 @@ bool GUI_App::on_init_inner()
// Suppress the '- default -' presets.
preset_bundle->set_default_suppressed(app_config->get("no_defaults") == "1");
try {
preset_bundle->load_presets(*app_config);
init_params->preset_substitutions = preset_bundle->load_presets(*app_config, ForwardCompatibilitySubstitutionRule::Enable);
} catch (const std::exception &ex) {
show_error(nullptr, ex.what());
}
@ -948,7 +973,6 @@ bool GUI_App::on_init_inner()
if (! plater_)
return;
if (app_config->dirty() && app_config->get("autosave") == "1")
app_config->save();
@ -969,33 +993,6 @@ bool GUI_App::on_init_inner()
#endif
this->post_init();
}
// Preset updating & Configwizard are done after the above initializations,
// and after MainFrame is created & shown.
// The extra CallAfter() is needed because of Mac, where this is the only way
// to popup a modal dialog on start without screwing combo boxes.
// This is ugly but I honestly found no better way to do it.
// Neither wxShowEvent nor wxWindowCreateEvent work reliably.
static bool once = true;
if (once) {
once = false;
if (preset_updater != nullptr) {
check_updates(false);
CallAfter([this] {
config_wizard_startup();
preset_updater->slic3r_update_notify();
preset_updater->sync(preset_bundle);
});
}
#ifdef _WIN32
//sets window property to mainframe so other instances can indentify it
OtherInstanceMessageHandler::init_windows_properties(mainframe, m_instance_hash_int);
#endif //WIN32
}
});
m_initialized = true;
@ -1872,7 +1869,13 @@ void GUI_App::add_config_menu(wxMenuBar *menu)
Config::SnapshotDB::singleton().take_snapshot(*app_config, Config::Snapshot::SNAPSHOT_BEFORE_ROLLBACK);
try {
app_config->set("on_snapshot", Config::SnapshotDB::singleton().restore_snapshot(dlg.snapshot_to_activate(), *app_config).id);
preset_bundle->load_presets(*app_config);
if (PresetsConfigSubstitutions all_substitutions = preset_bundle->load_presets(*app_config, ForwardCompatibilitySubstitutionRule::Enable);
! all_substitutions.empty()) {
// TODO:
show_error(nullptr, GUI::format(_L("Loading profiles found following incompatibilities."
" To recover these files, incompatible values were changed to default values."
" But data in files won't be changed until you save them in PrusaSlicer.")));
}
// Load the currently selected preset into the GUI, update the preset selection box.
load_current_presets();
} catch (std::exception &ex) {

View file

@ -273,7 +273,7 @@ public:
NotificationManager* notification_manager();
// Parameters extracted from the command line to be passed to GUI after initialization.
const GUI_InitParams* init_params { nullptr };
GUI_InitParams* init_params { nullptr };
AppConfig* app_config{ nullptr };
PresetBundle* preset_bundle{ nullptr };
@ -281,7 +281,7 @@ public:
MainFrame* mainframe{ nullptr };
Plater* plater_{ nullptr };
PresetUpdater* get_preset_updater() { return preset_updater; }
PresetUpdater* get_preset_updater() { return preset_updater; }
wxBookCtrlBase* tab_panel() const ;
int extruders_cnt() const;

View file

@ -50,39 +50,8 @@ int GUI_Run(GUI_InitParams &params)
// gui->autosave = m_config.opt_string("autosave");
GUI::GUI_App::SetInstance(gui);
gui->init_params = &params;
/*
gui->CallAfter([gui, this, &load_configs, params.start_as_gcodeviewer] {
if (!gui->initialized()) {
return;
}
if (params.start_as_gcodeviewer) {
if (!m_input_files.empty())
gui->plater()->load_gcode(wxString::FromUTF8(m_input_files[0].c_str()));
} else {
#if 0
// Load the cummulative config over the currently active profiles.
//FIXME if multiple configs are loaded, only the last one will have an effect.
// We need to decide what to do about loading of separate presets (just print preset, just filament preset etc).
// As of now only the full configs are supported here.
if (!m_print_config.empty())
gui->mainframe->load_config(m_print_config);
#endif
if (!load_configs.empty())
// Load the last config to give it a name at the UI. The name of the preset may be later
// changed by loading an AMF or 3MF.
//FIXME this is not strictly correct, as one may pass a print/filament/printer profile here instead of a full config.
gui->mainframe->load_config_file(load_configs.back());
// If loading a 3MF file, the config is loaded from the last one.
if (!m_input_files.empty())
gui->plater()->load_files(m_input_files, true, true);
if (!m_extra_config.empty())
gui->mainframe->load_config(m_extra_config);
}
});
*/
int result = wxEntry(params.argc, params.argv);
return result;
return wxEntry(params.argc, params.argv);
} catch (const Slic3r::Exception &ex) {
boost::nowide::cerr << ex.what() << std::endl;
wxMessageBox(boost::nowide::widen(ex.what()), _L("PrusaSlicer GUI initialization failed"), wxICON_STOP);

View file

@ -1,6 +1,7 @@
#ifndef slic3r_GUI_Init_hpp_
#define slic3r_GUI_Init_hpp_
#include <libslic3r/Preset.hpp>
#include <libslic3r/PrintConfig.hpp>
namespace Slic3r {
@ -12,6 +13,9 @@ struct GUI_InitParams
int argc;
char **argv;
// Substitutions of unknown configuration values done during loading of user presets.
PresetsConfigSubstitutions preset_substitutions;
std::vector<std::string> load_configs;
DynamicPrintConfig extra_config;
std::vector<std::string> input_files;

View file

@ -140,22 +140,27 @@ void SLAImportJob::process()
if (p->path.empty()) return;
std::string path = p->path.ToUTF8().data();
ConfigSubstitutions config_substitutions;
try {
switch (p->sel) {
case Sel::modelAndProfile:
import_sla_archive(path, p->win, p->mesh, p->profile, progr);
config_substitutions = import_sla_archive(path, p->win, p->mesh, p->profile, progr);
break;
case Sel::modelOnly:
import_sla_archive(path, p->win, p->mesh, progr);
config_substitutions = import_sla_archive(path, p->win, p->mesh, progr);
break;
case Sel::profileOnly:
import_sla_archive(path, p->profile);
config_substitutions = import_sla_archive(path, p->profile);
break;
}
} catch (std::exception &ex) {
p->err = ex.what();
}
if (! config_substitutions.empty()) {
//FIXME Add reporting here "Loading profiles found following incompatibilities."
}
update_status(100, was_canceled() ? _(L("Importing canceled.")) :
_(L("Importing done.")));

View file

@ -1518,6 +1518,7 @@ void MainFrame::update_menubar()
m_changeable_menu_items[miPrinterTab] ->SetBitmap(create_menu_bitmap(is_fff ? "printer" : "sla_printer"));
}
#if 0
// To perform the "Quck Slice", "Quick Slice and Save As", "Repeat last Quick Slice" and "Slice to SVG".
void MainFrame::quick_slice(const int qs)
{
@ -1643,6 +1644,7 @@ void MainFrame::quick_slice(const int qs)
// };
// Slic3r::GUI::catch_error(this, []() { if (m_progress_dialog) m_progress_dialog->Destroy(); });
}
#endif
void MainFrame::reslice_now()
{
@ -1729,7 +1731,13 @@ void MainFrame::load_config_file()
bool MainFrame::load_config_file(const std::string &path)
{
try {
wxGetApp().preset_bundle->load_config_file(path);
ConfigSubstitutions config_substitutions = wxGetApp().preset_bundle->load_config_file(path, ForwardCompatibilitySubstitutionRule::Enable);
if (! config_substitutions.empty()) {
// TODO: Add list of changes from all_substitutions
show_error(nullptr, GUI::format(_L("Loading profiles found following incompatibilities."
" To recover these files, incompatible values were changed to default values."
" But data in files won't be changed until you save them in PrusaSlicer.")));
}
} catch (const std::exception &ex) {
show_error(this, ex.what());
return false;
@ -1793,14 +1801,22 @@ void MainFrame::load_configbundle(wxString file/* = wxEmptyString, const bool re
wxGetApp().app_config->update_config_dir(get_dir_name(file));
auto presets_imported = 0;
size_t presets_imported = 0;
PresetsConfigSubstitutions config_substitutions;
try {
presets_imported = wxGetApp().preset_bundle->load_configbundle(file.ToUTF8().data());
std::tie(config_substitutions, presets_imported) = wxGetApp().preset_bundle->load_configbundle(file.ToUTF8().data(), PresetBundle::LoadConfigBundleAttribute::SaveImported);
} catch (const std::exception &ex) {
show_error(this, ex.what());
return;
}
if (! config_substitutions.empty()) {
// TODO: Add list of changes from all_substitutions
show_error(nullptr, GUI::format(_L("Loading profiles found following incompatibilities."
" To recover these files, incompatible values were changed to default values."
" But data in files won't be changed until you save them in PrusaSlicer.")));
}
// Load the currently selected preset into the GUI, update the preset selection box.
wxGetApp().load_current_presets();

View file

@ -170,7 +170,7 @@ public:
bool is_last_input_file() const { return !m_qs_last_input_file.IsEmpty(); }
bool is_dlg_layout() const { return m_layout == ESettingsLayout::Dlg; }
void quick_slice(const int qs = qsUndef);
// void quick_slice(const int qs = qsUndef);
void reslice_now();
void repair_stl();
void export_config();

View file

@ -2237,7 +2237,8 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
DynamicPrintConfig config;
{
DynamicPrintConfig config_loaded;
model = Slic3r::Model::read_from_archive(path.string(), &config_loaded, false, load_config);
ConfigSubstitutionContext config_substitutions{ ForwardCompatibilitySubstitutionRule::Enable };
model = Slic3r::Model::read_from_archive(path.string(), &config_loaded, &config_substitutions, only_if(load_config, Model::LoadAttribute::CheckVersion));
if (load_config && !config_loaded.empty()) {
// Based on the printer technology field found in the loaded config, select the base for the config,
PrinterTechnology printer_technology = Preset::printer_technology(config_loaded);
@ -2261,6 +2262,12 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
// and place the loaded config over the base.
config += std::move(config_loaded);
}
if (! config_substitutions.empty()) {
// TODO:
show_error(nullptr, GUI::format(_L("Loading profiles found following incompatibilities."
" To recover these files, incompatible values were changed to default values."
" But data in files won't be changed until you save them in PrusaSlicer.")));
}
this->model.custom_gcode_per_print_z = model.custom_gcode_per_print_z;
}
@ -2330,7 +2337,7 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
}
}
else {
model = Slic3r::Model::read_from_file(path.string(), nullptr, false, load_config);
model = Slic3r::Model::read_from_file(path.string(), nullptr, nullptr, only_if(load_config, Model::LoadAttribute::CheckVersion));
for (auto obj : model.objects)
if (obj->name.empty())
obj->name = fs::path(obj->input_file).filename().string();
@ -3215,7 +3222,7 @@ void Plater::priv::replace_with_stl()
Model new_model;
try {
new_model = Model::read_from_file(path, nullptr, true, false);
new_model = Model::read_from_file(path, nullptr, nullptr, Model::LoadAttribute::AddDefaultInstances);
for (ModelObject* model_object : new_model.objects) {
model_object->center_around_origin();
model_object->ensure_on_bed();
@ -3388,7 +3395,7 @@ void Plater::priv::reload_from_disk()
Model new_model;
try
{
new_model = Model::read_from_file(path, nullptr, true, false);
new_model = Model::read_from_file(path, nullptr, nullptr, Model::LoadAttribute::AddDefaultInstances);
for (ModelObject* model_object : new_model.objects) {
model_object->center_around_origin();
model_object->ensure_on_bed();
@ -4599,7 +4606,9 @@ void Plater::priv::undo_redo_to(std::vector<UndoRedo::Snapshot>::const_iterator
// Switch to the other printer technology. Switch to the last printer active for that particular technology.
AppConfig *app_config = wxGetApp().app_config;
app_config->set("presets", "printer", (new_printer_technology == ptFFF) ? m_last_fff_printer_profile_name : m_last_sla_printer_profile_name);
wxGetApp().preset_bundle->load_presets(*app_config);
//FIXME Why are we reloading the whole preset bundle here? Please document. This is fishy and it is unnecessarily expensive.
// Anyways, don't report any config value substitutions, they have been already reported to the user at application start up.
wxGetApp().preset_bundle->load_presets(*app_config, ForwardCompatibilitySubstitutionRule::EnableSilent);
// load_current_presets() calls Tab::load_current_preset() -> TabPrint::update() -> Object_list::update_and_show_object_settings_item(),
// but the Object list still keeps pointer to the old Model. Avoid a crash by removing selection first.
this->sidebar->obj_list()->unselect_objects();