Open .gcode files with their own settings, as .gcode.3mf files are (#16463)

This commit is contained in:
SoftFever
2026-10-14 14:13:58 +08:00
committed by GitHub
parent 46b3871f8d
commit e77ba5f672
8 changed files with 473 additions and 64 deletions
+3 -1
View File
@@ -3135,7 +3135,9 @@ std::pair<Preset*, bool> 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<ConfigOptionString >("print_settings_id", true)->value = new_name;
else if (m_type == Preset::TYPE_FILAMENT)
+78 -5
View File
@@ -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<const ConfigOptionVectorBase *>(opt)->size() == 0)
if (const ConfigOption *below = base.option(key); below != nullptr && static_cast<const ConfigOptionVectorBase *>(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<ConfigOptionFloats>(key))
std::fill(multiplier->values.begin(), multiplier->values.end(), 1.);
return config;
}
std::optional<PresetBundle::Selection> 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
+45
View File
@@ -397,6 +397,28 @@ public:
std::vector<Preset*> 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<bool> visible;
};
Collection prints, filaments, printers;
std::vector<std::string> filament_presets;
DynamicPrintConfig project_config;
std::vector<std::map<int, int>> extruder_ams_counts;
};
// Nothing while project-embedded presets exist: a restore drops them, which would lose a loaded project's presets.
std::optional<Selection> 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,
+3 -1
View File
@@ -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();
+139 -57
View File
@@ -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<ConfigOptionStrings>("filament_colour");
if (filament_color) {
size_t filament_count = filament_color->size();
// Sync filament map
ConfigOptionInts* filament_map = proj_cfg.opt<ConfigOptionInts>("filament_map", true);
if (filament_map->size() != filament_count) {
filament_map->values.resize(filament_count, 1);
}
ConfigOptionInts* filament_nozzle_map = proj_cfg.opt<ConfigOptionInts>("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<ConfigOptionInts>("filament_volume_map", true);
if (filament_volume_map->size() != filament_count) {
filament_volume_map->values.resize(filament_count, static_cast<int>(NozzleVolumeType::nvtStandard));
}
// Sync filament multi colour
ConfigOptionStrings* filament_multi_color = proj_cfg.opt<ConfigOptionStrings>("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<std::string> 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<ConfigOptionStrings>("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<ConfigOptionBool>("has_filament_switcher"))
has_switcher->value = false;
if (auto* dynamic_map = proj_cfg.opt<ConfigOptionBool>("enable_filament_dynamic_map"))
dynamic_map->value = false;
}
// BBS: backup & restore
std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_files,
LoadStrategy strategy,
@@ -9680,58 +9742,7 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
if (file_wipe_tower_y)
*wipe_tower_y = *file_wipe_tower_y;
ConfigOptionStrings* filament_color = proj_cfg.opt<ConfigOptionStrings>("filament_colour");
if (filament_color) {
size_t filament_count = filament_color->size();
// Sync filament map
ConfigOptionInts* filament_map = proj_cfg.opt<ConfigOptionInts>("filament_map", true);
if (filament_map->size() != filament_count) {
filament_map->values.resize(filament_count, 1);
}
ConfigOptionInts* filament_nozzle_map = proj_cfg.opt<ConfigOptionInts>("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<ConfigOptionInts>("filament_volume_map", true);
if (filament_volume_map->size() != filament_count) {
filament_volume_map->values.resize(filament_count, static_cast<int>(NozzleVolumeType::nvtStandard));
}
// Sync filament multi colour
ConfigOptionStrings* filament_multi_color = proj_cfg.opt<ConfigOptionStrings>("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<std::string> 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<ConfigOptionStrings>("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<ConfigOptionBool>("has_filament_switcher"))
has_switcher->value = false;
if (auto* dynamic_map = proj_cfg.opt<ConfigOptionBool>("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<DynamicPrintConfig> 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<PresetBundle::Selection> 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));
+5
View File
@@ -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 };
+44
View File
@@ -3,17 +3,25 @@
#include <catch2/catch_message.hpp>
#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 <fstream>
#include <ios>
#include <string>
#include <vector>
#include <catch2/matchers/catch_matchers.hpp>
#include <catch2/matchers/catch_matchers_floating_point.hpp>
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<TriangleMesh>{ 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<ConfigOptionEnum<PrimeVolumeMode>>("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<double> &matrix = bundle.project_config.option<ConfigOptionFloats>("flush_volumes_matrix")->values;
const std::vector<double> &multiplier = bundle.project_config.option<ConfigOptionFloats>("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<ConfigOptionEnum<PrimeVolumeMode>>("prime_volume_mode")->value == PrimeVolumeMode::pvmFast);
}
@@ -38,6 +38,7 @@
#include "libslic3r/Config.hpp"
#include <map>
#include <memory>
#include <optional>
#include <regex>
#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<ConfigOptionFloats>("filament_diameter")->values.assign(filament_count, 1.75);
config.opt<ConfigOptionStrings>("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<std::string> &filaments = config.option<ConfigOptionStrings>("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<const PresetCollection *>{ &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<const PresetCollection *>{ &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<std::string> filaments_before = bundle.filament_presets;
const DynamicPrintConfig project_before = bundle.project_config;
const std::optional<PresetBundle::Selection> 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<ConfigOptionStrings>("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<ConfigOptionStrings>("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<ConfigOptionStrings>("filament_multi_colour")->values.empty());
}