Only serialize selected published settings. Minor cleanup

This commit is contained in:
Lam Wei Lun
2026-08-14 12:53:44 +08:00
parent b488067f82
commit 96f5a23387
10 changed files with 190 additions and 18 deletions

View File

@@ -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);

View File

@@ -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,

View File

@@ -2,6 +2,7 @@
#include "PresetBundle.hpp"
#include "Preset.hpp"
#include "PrintConfig.hpp"
#include <algorithm>
@@ -102,4 +103,60 @@ std::vector<std::string> collect_dirty_settings_keys(const PresetBundle& bundle)
return keys;
}
DynamicPrintConfig filter_published_config(
const DynamicPrintConfig &full_config,
const std::vector<std::string> &published_keys,
const std::vector<PublishedMaterialEntry> &material_keys)
{
DynamicPrintConfig filtered;
std::set<std::string> base_keys_to_include;
// 1. Mandatory material identity & slot count keys for 3MF validation/normalization
static const std::vector<std::string> 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<std::string> 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

View File

@@ -53,4 +53,12 @@ struct PublishedMaterialEntry {
int slot{-1};
std::vector<std::string> 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<std::string> &published_keys,
const std::vector<PublishedMaterialEntry> &material_keys);
}

View File

@@ -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<size_t>(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<const ConfigOptionVectorBase *>(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;

View File

@@ -7324,7 +7324,7 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& 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<std::string> modified_gcodes;
@@ -16204,16 +16204,21 @@ int Plater::export_published_3mf(const std::vector<std::string>& 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;

View File

@@ -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();

View File

@@ -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<Slic3r::PublishedMaterialEntry> 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();

View File

@@ -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

View File

@@ -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<std::string> published_keys = { "layer_height", "retraction_length" };
const std::vector<PublishedMaterialEntry> 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<ModelInfo>();
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<Preset*> 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<Preset*> 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);
}
}
}