From 96f5a2338711e891c3a7270c76f991f1797109ca Mon Sep 17 00:00:00 2001 From: Lam Wei Lun Date: Fri, 14 Aug 2026 12:53:44 +0800 Subject: [PATCH] Only serialize selected published settings. Minor cleanup --- src/libslic3r/Format/bbs_3mf.cpp | 6 +- src/libslic3r/Format/bbs_3mf.hpp | 1 + src/libslic3r/PublishSettings.cpp | 57 ++++++++++++++++++ src/libslic3r/PublishSettings.hpp | 8 +++ src/slic3r/GUI/ConfigValueFormatter.cpp | 6 +- src/slic3r/GUI/Plater.cpp | 17 ++++-- src/slic3r/GUI/Plater.hpp | 2 +- src/slic3r/GUI/PublishSettingsDialog.cpp | 32 ++++++++-- src/slic3r/GUI/PublishSettingsDialog.hpp | 2 + tests/libslic3r/test_3mf.cpp | 77 ++++++++++++++++++++++++ 10 files changed, 190 insertions(+), 18 deletions(-) diff --git a/src/libslic3r/Format/bbs_3mf.cpp b/src/libslic3r/Format/bbs_3mf.cpp index 3000adb441..a5d20807b7 100644 --- a/src/libslic3r/Format/bbs_3mf.cpp +++ b/src/libslic3r/Format/bbs_3mf.cpp @@ -5943,6 +5943,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) bool m_save_gcode { false }; // whether to save gcode for normal save bool m_skip_model { false }; // skip model when exporting .gcode.3mf bool m_skip_auxiliary { false }; // skip normal axuiliary files + bool m_minimal_published { false }; // published 3MF: omit embedded preset files bool m_use_loaded_id { false }; // whether to use loaded id for identify_id bool m_share_mesh { false }; // whether to share mesh between objects std::string m_thumbnail_middle = PRINTER_THUMBNAIL_MIDDLE_FILE; @@ -6042,6 +6043,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) m_skip_auxiliary = store_params.strategy & SaveStrategy::SkipAuxiliary; m_share_mesh = store_params.strategy & SaveStrategy::ShareMesh; m_from_backup_save = store_params.strategy & SaveStrategy::Backup; + m_minimal_published = store_params.strategy & SaveStrategy::MinimalPublished; m_use_loaded_id = store_params.strategy & SaveStrategy::UseLoadedId; @@ -6464,8 +6466,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) if (cb_cancel) return false; } - // BBS: add project config - if (project_presets.size() > 0) { + // BBS: add project config (omitted for minimal published 3MF) + if (!m_minimal_published && project_presets.size() > 0) { // BBS: add project embedded preset files _add_project_embedded_presets_to_archive(archive, model, project_presets); diff --git a/src/libslic3r/Format/bbs_3mf.hpp b/src/libslic3r/Format/bbs_3mf.hpp index 9c697a14fc..0addf23385 100644 --- a/src/libslic3r/Format/bbs_3mf.hpp +++ b/src/libslic3r/Format/bbs_3mf.hpp @@ -145,6 +145,7 @@ enum class SaveStrategy SkipAuxiliary = 1 << 9, UseLoadedId = 1 << 10, ShareMesh = 1 << 11, + MinimalPublished = 1 << 12, SplitModel = 0x1000 | ProductionExt, Encrypted = SecureContentExt | SplitModel, diff --git a/src/libslic3r/PublishSettings.cpp b/src/libslic3r/PublishSettings.cpp index 4c0a851696..9ac0c49472 100644 --- a/src/libslic3r/PublishSettings.cpp +++ b/src/libslic3r/PublishSettings.cpp @@ -2,6 +2,7 @@ #include "PresetBundle.hpp" #include "Preset.hpp" +#include "PrintConfig.hpp" #include @@ -102,4 +103,60 @@ std::vector collect_dirty_settings_keys(const PresetBundle& bundle) return keys; } +DynamicPrintConfig filter_published_config( + const DynamicPrintConfig &full_config, + const std::vector &published_keys, + const std::vector &material_keys) +{ + DynamicPrintConfig filtered; + + std::set base_keys_to_include; + + // 1. Mandatory material identity & slot count keys for 3MF validation/normalization + static const std::vector s_material_identity_keys = { + "filament_colour", + "filament_type", + "filament_vendor", + "filament_ids", + "filament_diameter", + "filament_self_index", + "filament_extruder_variant" + }; + for (const std::string &key : s_material_identity_keys) + base_keys_to_include.insert(key); + + // 2. Published plate / bed geometry keys (wipe tower positioning) + static const std::vector s_plate_geometry_keys = { + "wipe_tower_x", + "wipe_tower_y", + "wipe_tower_rotation_angle" + }; + for (const std::string &key : s_plate_geometry_keys) + base_keys_to_include.insert(key); + + // 3. Process and printer published keys + for (const std::string &key : published_keys) { + const std::string base_key = key.substr(0, key.find('#')); + if (!base_key.empty()) + base_keys_to_include.insert(base_key); + } + + // 4. Material-specific published keys + for (const PublishedMaterialEntry &entry : material_keys) { + for (const std::string &key : entry.keys) { + const std::string base_key = key.substr(0, key.find('#')); + if (!base_key.empty()) + base_keys_to_include.insert(base_key); + } + } + + // Copy selected options from full_config into filtered config + for (const std::string &key : base_keys_to_include) { + if (const ConfigOption *opt = full_config.option(key)) + filtered.set_key_value(key, opt->clone()); + } + + return filtered; +} + } // namespace Slic3r diff --git a/src/libslic3r/PublishSettings.hpp b/src/libslic3r/PublishSettings.hpp index ecc939d883..6525fbb66e 100644 --- a/src/libslic3r/PublishSettings.hpp +++ b/src/libslic3r/PublishSettings.hpp @@ -53,4 +53,12 @@ struct PublishedMaterialEntry { int slot{-1}; std::vector keys; }; + +// Constructs a minimal DynamicPrintConfig for a published 3MF export containing only the +// author-selected published keys, material keys, material identity fields, and plate geometry keys. +class DynamicPrintConfig; +DynamicPrintConfig filter_published_config( + const DynamicPrintConfig &full_config, + const std::vector &published_keys, + const std::vector &material_keys); } diff --git a/src/slic3r/GUI/ConfigValueFormatter.cpp b/src/slic3r/GUI/ConfigValueFormatter.cpp index f3ea539d4f..6f9128841b 100644 --- a/src/slic3r/GUI/ConfigValueFormatter.cpp +++ b/src/slic3r/GUI/ConfigValueFormatter.cpp @@ -73,7 +73,7 @@ wxString get_string_value(const std::string& opt_key, const DynamicPrintConfig& std::string temp_str = opt_key; if (pos > 0) { boost::erase_head(temp_str, pos + 1); - orig_opt_idx = static_cast(atoi(temp_str.c_str())); + orig_opt_idx = std::atoi(temp_str.c_str()); } opt_idx = orig_opt_idx >= 0 ? orig_opt_idx : 0; const std::string pure_key = get_pure_opt_key(opt_key); @@ -83,8 +83,8 @@ wxString get_string_value(const std::string& opt_key, const DynamicPrintConfig& } auto opt_vector = dynamic_cast(option); - if (option->is_scalar() && config.option(pure_key)->is_nil() || - option->is_vector() && opt_vector && opt_idx >= 0 && opt_idx < opt_vector->size() && opt_vector->is_nil(opt_idx)) + if ((option->is_scalar() && option->is_nil()) || + (option->is_vector() && opt_vector && opt_idx >= 0 && opt_idx < opt_vector->size() && opt_vector->is_nil(opt_idx))) return _L("N/A"); wxString out; diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index fd307d32d2..571d7350b9 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -7324,7 +7324,7 @@ std::vector Plater::priv::load_files(const std::vector& input_ } auto choise = wxGetApp().app_config->get("no_warn_when_modified_gcodes"); - if (choise.empty() || choise != "true") { + if (!published_config.published && (choise.empty() || choise != "true")) { // BBS: first validate the printer // validate the system profiles std::set modified_gcodes; @@ -16204,16 +16204,21 @@ int Plater::export_published_3mf(const std::vector& published_keys, model.model_info->metadata_items["published_keys"] = j.dump(); model.model_info->metadata_items["published_material_keys"] = jm.dump(); - // Same file layout save_project() uses for its project files, plus SaveStrategy::Silence: + // Minimal published export: filter full_config to only the published keys, material keys, + // identity fields, and plate geometry keys, and omit project-embedded preset dumps. + DynamicPrintConfig full_cfg = wxGetApp().preset_bundle->full_config_secure(); + DynamicPrintConfig filtered_cfg = filter_published_config(full_cfg, published_keys, material_keys); + + // Same file layout save_project() uses for its project files, plus SaveStrategy::Silence and SaveStrategy::MinimalPublished: // without it export_3mf() calls set_project_filename() on success, which would make this // pure export the current project file. Silence keeps the project state untouched, exactly // like export_core_3mf(). - auto save_strategy = SaveStrategy::SplitModel | SaveStrategy::ShareMesh | SaveStrategy::Silence; + auto save_strategy = SaveStrategy::SplitModel | SaveStrategy::ShareMesh | SaveStrategy::Silence | SaveStrategy::MinimalPublished; bool full_pathnames = wxGetApp().app_config->get_bool("export_sources_full_pathnames"); if (full_pathnames) save_strategy = save_strategy | SaveStrategy::FullPathSources; - const int ret = export_3mf(into_path(path), save_strategy); + const int ret = export_3mf(into_path(path), save_strategy, -1, nullptr, &filtered_cfg); // Restore the previous metadata state (both on success and on failure). if (!had_model_info) { @@ -16737,7 +16742,7 @@ void publish(Model &model, SaveStrategy strategy) { } // BBS: backup -int Plater::export_3mf(const boost::filesystem::path& output_path, SaveStrategy strategy, int export_plate_idx, Export3mfProgressFn proFn) +int Plater::export_3mf(const boost::filesystem::path& output_path, SaveStrategy strategy, int export_plate_idx, Export3mfProgressFn proFn, const DynamicPrintConfig* override_config) { int ret = 0; //if (p->model.objects.empty()) { @@ -16759,7 +16764,7 @@ int Plater::export_3mf(const boost::filesystem::path& output_path, SaveStrategy // modify model publish(p->model, strategy); - DynamicPrintConfig cfg = wxGetApp().preset_bundle->full_config_secure(); + DynamicPrintConfig cfg = override_config ? *override_config : wxGetApp().preset_bundle->full_config_secure(); const std::string path_u8 = into_u8(path); wxBusyCursor wait; diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp index 5a83e5e4e1..d29e86a4fa 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -505,7 +505,7 @@ public: //void export_amf(); //BBS add extra param for exporting 3mf silence // BBS: backup - int export_3mf(const boost::filesystem::path& output_path = boost::filesystem::path(), SaveStrategy strategy = SaveStrategy::Default, int export_plate_idx = -1, Export3mfProgressFn proFn = nullptr); + int export_3mf(const boost::filesystem::path& output_path = boost::filesystem::path(), SaveStrategy strategy = SaveStrategy::Default, int export_plate_idx = -1, Export3mfProgressFn proFn = nullptr, const DynamicPrintConfig* override_config = nullptr); //BBS void publish_project(); diff --git a/src/slic3r/GUI/PublishSettingsDialog.cpp b/src/slic3r/GUI/PublishSettingsDialog.cpp index 36b7372d4a..90ad2e0dc3 100644 --- a/src/slic3r/GUI/PublishSettingsDialog.cpp +++ b/src/slic3r/GUI/PublishSettingsDialog.cpp @@ -321,8 +321,8 @@ void PublishSettingsDialog::build_option_model() cat.filament_id = identity.id; cat.filament_slot = slot; if (!icon_name.empty()) { - ScalableBitmap icon_bmp(m_scroll, icon_name, 18); - cat.icon = new wxStaticBitmap(m_scroll, wxID_ANY, icon_bmp.bmp()); + cat.icon_bmp = ScalableBitmap(m_scroll, icon_name, 18); + cat.icon = new wxStaticBitmap(m_scroll, wxID_ANY, cat.icon_bmp.bmp()); } if (section == Section::Material) { // Material header: [master (title)][slim tri-state select-all]. @@ -667,8 +667,8 @@ size_t PublishSettingsDialog::section_group_for(Section kind) } if (!section.icon_name.empty()) { - ScalableBitmap icon_bmp(m_scroll, section.icon_name, 18); - section.icon = new wxStaticBitmap(m_scroll, wxID_ANY, icon_bmp.bmp()); + section.icon_bmp = ScalableBitmap(m_scroll, section.icon_name, 18); + section.icon = new wxStaticBitmap(m_scroll, wxID_ANY, section.icon_bmp.bmp()); } section.chevron = create_chevron(m_scroll, wxEVT_LEFT_DOWN, [this, new_index] { toggle_section(new_index); }); @@ -1062,12 +1062,32 @@ std::vector PublishSettingsDialog::GetPublishedM void PublishSettingsDialog::on_dpi_changed(const wxRect& suggested_rect) { - // The remaining bitmap icons are rescaled; the collapse chevrons are - // vector-drawn and repaint themselves. + // Rescale toolbar bitmaps and icons; collapse chevrons are vector-drawn and repaint themselves. m_search.msw_rescale(); m_menu.msw_rescale(); m_filter_box->SetIcon(m_search.bmp()); m_menu_button->SetBitmap(m_menu.bmp()); + + for (SectionGroup& section : m_sections) { + if (section.icon != nullptr && section.icon_bmp.bmp().IsOk()) { + section.icon_bmp.msw_rescale(); + section.icon->SetBitmap(section.icon_bmp.bmp()); + } + if (section.header_line != nullptr) + section.header_line->Rescale(); + } + + for (Category& cat : m_categories) { + if (cat.icon != nullptr && cat.icon_bmp.bmp().IsOk()) { + cat.icon_bmp.msw_rescale(); + cat.icon->SetBitmap(cat.icon_bmp.bmp()); + } + for (Subcategory& sub : cat.subs) { + if (sub.header != nullptr) + sub.header->Rescale(); + } + } + SetMinSize(FromDIP(wxSize(600, 500))); m_scroll->FitInside(); m_list_sizer->Layout(); diff --git a/src/slic3r/GUI/PublishSettingsDialog.hpp b/src/slic3r/GUI/PublishSettingsDialog.hpp index ff9cb88e4b..4767b9a29e 100644 --- a/src/slic3r/GUI/PublishSettingsDialog.hpp +++ b/src/slic3r/GUI/PublishSettingsDialog.hpp @@ -91,6 +91,7 @@ private: Section section{Section::Print}; size_t group{0}; // index into m_sections std::string icon_name; // bitmap name; empty = no icon + ScalableBitmap icon_bmp; // scalable bitmap for DPI changes wxStaticBitmap* icon{nullptr}; // 18px category icon (null when icon_name empty) wxCheckBox* header{nullptr}; // select-all tri-state // Material opt-in: the master checkbox carries the material title and @@ -117,6 +118,7 @@ private: wxString title; // _L("Printer") / _L("Filament") / _L("Process") Section kind{Section::Print}; // maps 1:1 to the display group std::string icon_name; // "printer" / "filament" / "process" + ScalableBitmap icon_bmp; // scalable bitmap for DPI changes wxStaticBitmap* icon{nullptr}; // 18px, like Category::icon wxCheckBox* header{nullptr}; // tri-state select-all; nullptr for the Filament group ::StaticLine* header_line{nullptr}; // Filament group's clickable title diff --git a/tests/libslic3r/test_3mf.cpp b/tests/libslic3r/test_3mf.cpp index 4eccf0e9fa..462324ea13 100644 --- a/tests/libslic3r/test_3mf.cpp +++ b/tests/libslic3r/test_3mf.cpp @@ -8,6 +8,7 @@ #include "libslic3r/Preset.hpp" #include "libslic3r/MultiNozzleUtils.hpp" #include "libslic3r/ProjectTask.hpp" +#include "libslic3r/PublishSettings.hpp" #include "test_utils.hpp" @@ -674,3 +675,79 @@ SCENARIO("Published 3MF round-trips the published_material_keys metadata", "[3mf } } } + +SCENARIO("Minimal published 3MF serialization filters config and omits embedded presets", "[3mf]") { + GIVEN("a full print configuration and published keys") { + Model model; + std::string src_file = std::string(TEST_DATA_DIR) + "/test_3mf/Prusa.stl"; + REQUIRE(load_stl(src_file.c_str(), &model)); + model.add_default_instances(); + + DynamicPrintConfig full_cfg = DynamicPrintConfig::full_print_config(); + full_cfg.set_key_value("layer_height", new ConfigOptionFloat(0.24)); + full_cfg.set_key_value("retraction_length", new ConfigOptionFloats({ 1.2 })); + + const std::vector published_keys = { "layer_height", "retraction_length" }; + const std::vector material_keys = { + { "PLA", "Generic", "GFL99", 0, { "filament_retraction_length" } } + }; + + DynamicPrintConfig filtered_cfg = filter_published_config(full_cfg, published_keys, material_keys); + + // Filtered config must contain the published keys and identity keys + REQUIRE(filtered_cfg.option("layer_height") != nullptr); + REQUIRE(filtered_cfg.option("retraction_length") != nullptr); + REQUIRE(filtered_cfg.option("filament_colour") != nullptr); + REQUIRE(filtered_cfg.option("filament_type") != nullptr); + REQUIRE(filtered_cfg.option("wipe_tower_x") != nullptr); + + // Non-published settings should NOT be in filtered_cfg + REQUIRE(filtered_cfg.option("sparse_infill_density") == nullptr); + REQUIRE(filtered_cfg.option("machine_start_gcode") == nullptr); + + model.model_info = std::make_shared(); + model.model_info->metadata_items["published"] = "1"; + model.model_info->metadata_items["published_keys"] = R"(["layer_height","retraction_length"])"; + + ScopedTemporaryDir backup_dir("orca_min_pub"); + model.set_backup_path(backup_dir.string()); + + WHEN("stored using SaveStrategy::MinimalPublished") { + ScopedTemporaryFile temp(".3mf"); + const std::string test_file = temp.string(); + + // Create a fake project preset to verify it gets omitted with MinimalPublished + Preset preset(Preset::TYPE_PRINT, "TestPrintPreset"); + preset.config = full_cfg; + std::vector project_presets = { &preset }; + + StoreParams store_params; + store_params.path = test_file.c_str(); + store_params.model = &model; + store_params.config = &filtered_cfg; + store_params.project_presets = project_presets; + store_params.strategy = SaveStrategy::Zip64 | SaveStrategy::Silence | SaveStrategy::MinimalPublished; + REQUIRE(store_bbs_3mf(store_params)); + + Model dst_model; + DynamicPrintConfig dst_config; + ConfigSubstitutionContext ctxt{ ForwardCompatibilitySubstitutionRule::Enable }; + PlateDataPtrs dst_plates; + std::vector loaded_presets; + bool is_bbl_3mf = false, is_orca_3mf = false; + Semver file_version; + bool loaded = load_bbs_3mf(test_file.c_str(), &dst_config, &ctxt, &dst_model, &dst_plates, + &loaded_presets, &is_bbl_3mf, &is_orca_3mf, &file_version, nullptr, + LoadStrategy::LoadModel | LoadStrategy::LoadConfig); + THEN("the 3MF loads successfully without project embedded presets") { + REQUIRE(loaded); + REQUIRE(loaded_presets.empty()); + REQUIRE(dst_config.option("layer_height") != nullptr); + REQUIRE(dst_config.opt_float("layer_height") == 0.24); + REQUIRE(dst_config.option("sparse_infill_density") == nullptr); + } + release_PlateData_list(dst_plates); + } + } +} +