Extend Publish workflow with full-filament and type/color requirements

Per material slot, the Publish dialog can now embed the entire filament preset ("Full Publish") and require a curated filament type and/or colour:

- On export, full-publish vector options are masked to the author's slot so unrelated slot data never leaks into the published file.

- On load, slots are matched by the published type: a match keeps the receiver's material (full dumps ignored, partial keys applied); a mismatch replaces the slot with the first visible same-type library filament, falling back to a temporary embedded preset or skipped keys when none exists. Required colours apply regardless of the type match.

- The receiver's slot count grows only to the highest published slot.

- Published 3MFs load as a new project: the file's path is not adopted as the project filename, published metadata is stripped from the model, and the file is added to recent projects.

- Notifications list replaced slots, and the edited filament preset is refreshed so applied values surface in the GUI.

- Dialog: "Full Publish" toggle replaces the material opt-in and select-all headers; new Color/Type requirement rows with swatches.

- Add Ctrl+Shift+E shortcut for the Publish dialog (menu, key handling, and the keyboard shortcuts dialog).

- Tests for export slot masking, metadata round-trip, replacement semantics, slot growth, and skipped-key reporting.
This commit is contained in:
Lam Wei Lun
2026-08-18 13:49:10 +08:00
parent 2b18744cc2
commit 6c429059e0
13 changed files with 1037 additions and 135 deletions
+1
View File
@@ -174,6 +174,7 @@ void KBShortcutsDialog::fill_shortcuts()
{ ctrl + "O", L("Open Project") },
{ ctrl + "S", L("Save Project") },
{ ctrl + shift + "S", L("Save Project as")},
{ ctrl + shift + "E", L("Publish") },
// File>Import
{ ctrl + "I", L("Import geometry data from STL/STEP/3MF/OBJ/AMF files") },
// File>Export
+17 -8
View File
@@ -741,6 +741,10 @@ DPIFrame(NULL, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, BORDERLESS_FRAME_
if (m_plater) { m_plater->add_file(); }
return;
}
if (evt.CmdDown() && evt.ShiftDown() && evt.GetKeyCode() == 'E') {
if (can_export_model()) publish_project();
return;
}
evt.Skip();
});
@@ -1728,6 +1732,16 @@ bool MainFrame::save_project_as(const wxString& filename)
return ret;
}
void MainFrame::publish_project()
{
if (m_plater == nullptr)
return;
PublishSettingsDialog dlg(this);
if (dlg.ShowModal() != wxID_OK)
return;
m_plater->export_published_3mf(dlg.GetPublishedKeys(), dlg.GetPublishedMaterialKeys());
}
bool MainFrame::can_upload() const
{
return true;
@@ -2820,19 +2834,14 @@ void MainFrame::init_menubar_as_editor()
// BBS: publish
fileMenu->AppendSeparator();
auto publish_handler = [this](wxCommandEvent&) {
if (!m_plater) return;
PublishSettingsDialog dlg(this);
if (dlg.ShowModal() != wxID_OK) return;
m_plater->export_published_3mf(dlg.GetPublishedKeys(), dlg.GetPublishedMaterialKeys());
};
auto publish_handler = [this](wxCommandEvent&) { publish_project(); };
#ifndef __APPLE__
append_menu_item(fileMenu, wxID_ANY, _L("Publish") + dots, _L("Export a 3MF file with the selected settings embedded"),
append_menu_item(fileMenu, wxID_ANY, _L("Publish") + dots + "\t" + ctrl + shift + "E", _L("Export a 3MF file with the selected settings embedded"),
publish_handler, "menu_publish", nullptr,
[this](){return can_export_model(); }, this);
#else
append_menu_item(fileMenu, wxID_ANY, _L("Publish") + dots, _L("Export a 3MF file with the selected settings embedded"),
append_menu_item(fileMenu, wxID_ANY, _L("Publish") + dots + "\t" + ctrl + shift + "E", _L("Export a 3MF file with the selected settings embedded"),
publish_handler, "", nullptr,
[this](){return can_export_model(); }, this);
#endif
+2
View File
@@ -340,6 +340,8 @@ public:
bool can_upload() const;
void save_project();
bool save_project_as(const wxString& filename = wxString());
// Open the Publish dialog and export the selected settings as a published 3MF.
void publish_project();
void add_to_recent_projects(const wxString& filename);
void get_recent_projects(boost::property_tree::wptree &tree, int images);
+65 -8
View File
@@ -5449,7 +5449,7 @@ struct Plater::priv
BoundingBox scaled_bed_shape_bb() const;
// BBS: backup & restore
std::vector<size_t> load_files(const std::vector<fs::path>& input_files, LoadStrategy strategy, bool ask_multi = false);
std::vector<size_t> load_files(const std::vector<fs::path>& input_files, LoadStrategy strategy, bool ask_multi = false, bool* published_out = nullptr);
std::vector<size_t> load_model_objects(const ModelObjectPtrs& model_objects, bool allow_negative_z = false, bool split_object = false, bool auto_drop = true);
fs::path get_export_file_path(GUI::FileType file_type);
@@ -6759,7 +6759,7 @@ void read_binary_stl(const std::string& filename, std::string& model_id, std::st
}
// BBS: backup & restore
std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_files, LoadStrategy strategy, bool ask_multi)
std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_files, LoadStrategy strategy, bool ask_multi, bool* published_out)
{
std::vector<size_t> empty_result;
bool dlg_cont = true;
@@ -7257,6 +7257,22 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
for (const auto &k : *entry_keys_it)
if (k.is_string())
entry.keys.emplace_back(k.get<std::string>());
// Filament-publishing-v2 fields; absent in legacy files.
if (m.contains("full") && m["full"].is_boolean())
entry.full = m["full"].get<bool>();
const auto entry_full_keys_it = m.find("full_keys");
if (entry_full_keys_it != m.end() && entry_full_keys_it->is_array())
for (const auto &k : *entry_full_keys_it)
if (k.is_string())
entry.full_keys.emplace_back(k.get<std::string>());
if (m.contains("publish_type") && m["publish_type"].is_boolean())
entry.publish_type = m["publish_type"].get<bool>();
if (m.contains("type") && m["type"].is_string())
entry.publish_type_value = m["type"].get<std::string>();
if (m.contains("publish_color") && m["publish_color"].is_boolean())
entry.publish_color = m["publish_color"].get<bool>();
if (m.contains("color") && m["color"].is_string())
entry.color = m["color"].get<std::string>();
published_config.material_keys.emplace_back(std::move(entry));
}
} catch (...) {
@@ -7266,6 +7282,18 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
}
}
// BBS: a "published" 3MF behaves like a new project once loaded: the file's path
// must not become the project filename (Save/Ctrl-S would otherwise overwrite the
// shared file), and the published metadata is consumed by the overlay above and
// stripped so a later save produces a normal, unpublished 3MF.
if (published_out != nullptr && published_config.published)
*published_out = true;
if (published_config.published && load_config && this->model.model_info != nullptr) {
this->model.model_info->metadata_items.erase("published");
this->model.model_info->metadata_items.erase("published_keys");
this->model.model_info->metadata_items.erase("published_material_keys");
}
if (load_config) {
if (!config.empty()) {
Preset::normalize(config);
@@ -7380,6 +7408,16 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
notify_manager->bbl_show_3mf_warn_notification(message);
}
// BBS: notify the user about slot materials that were replaced while
// loading a published project (type mismatch / no same-type match).
if (!published_config.material_replacements.empty()) {
NotificationManager *notify_manager = q->get_notification_manager();
std::string message = _u8L("Some filament slots were changed to match the published materials:");
for (const std::string &replacement : published_config.material_replacements)
message += "\n-" + replacement;
notify_manager->bbl_show_3mf_warn_notification(message);
}
ConfigOption* bed_type_opt = preset_bundle->project_config.option("curr_bed_type");
if (bed_type_opt != nullptr) {
BedType bed_type = (BedType)bed_type_opt->getInt();
@@ -7519,7 +7557,13 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
dynamic_map->value = false;
}
// Update filament combobox after loading config
wxGetApp().plater()->sidebar().update_presets(Preset::TYPE_FILAMENT);
if (published_config.published) {
q->update_filament_colors_in_full_config();
wxGetApp().plater()->sidebar().update_all_preset_comboboxes();
wxGetApp().plater()->sidebar().update_dynamic_filament_list();
} else {
wxGetApp().plater()->sidebar().update_presets(Preset::TYPE_FILAMENT);
}
// The loaded project supplies nozzle_volume_type; refresh the sidebar
// nozzle-count badges against it.
if (auto *nozzle_volumes = wxGetApp().preset_bundle->project_config.option<ConfigOptionEnumsGeneric>("nozzle_volume_type")) {
@@ -13244,14 +13288,15 @@ void Plater::load_project(wxString const& filename2,
if (strategy & LoadStrategy::Restore)
input_paths.push_back(into_u8(originfile));
std::vector<size_t> res = load_files(input_paths, strategy);
bool loaded_published = false;
std::vector<size_t> res = load_files(input_paths, strategy, false, &loaded_published);
reset_project_dirty_initial_presets();
update_project_dirty_from_presets();
wxGetApp().preset_bundle->export_selections(*wxGetApp().app_config);
// if res is empty no data has been loaded
if (!res.empty() && (load_restore || !(strategy & LoadStrategy::Silence))) {
if (!res.empty() && !loaded_published && (load_restore || !(strategy & LoadStrategy::Silence))) {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << " call set_project_filename: " << (load_restore ? originfile : filename);
p->set_project_filename(load_restore ? originfile : filename);
if (load_restore && originfile.IsEmpty()) {
@@ -13263,6 +13308,15 @@ void Plater::load_project(wxString const& filename2,
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << " using ecported set project filename: " << filename;
p->set_project_filename(filename);
}
else if (loaded_published) {
// A "published" 3MF loads as a new project: the shared file's path must not become
// the project filename, so Save/Ctrl-S prompts for a destination instead of
// overwriting the published file. reset() above already cleared the project name
// and folder; restore the default new-project title and keep the file in recents.
p->set_project_name(_L("Untitled"));
if (!filename.IsEmpty())
wxGetApp().mainframe->add_to_recent_projects(filename);
}
}
@@ -14918,12 +14972,12 @@ void Plater::force_update_all_plate_thumbnails()
}
// BBS: backup
std::vector<size_t> Plater::load_files(const std::vector<fs::path>& input_files, LoadStrategy strategy, bool ask_multi) {
std::vector<size_t> Plater::load_files(const std::vector<fs::path>& input_files, LoadStrategy strategy, bool ask_multi, bool* published_out) {
//BBS: wish to reset state when load a new file
p->m_slice_all_only_has_gcode = false;
//BBS: wish to reset all plates stats item selected state when load a new file
p->preview->get_canvas3d()->reset_select_plate_toolbar_selection();
return p->load_files(input_files, strategy, ask_multi);
return p->load_files(input_files, strategy, ask_multi, published_out);
}
// To be called when providing a list of files to the GUI slic3r on command line.
@@ -16186,7 +16240,10 @@ int Plater::export_published_3mf(const std::vector<std::string>& published_keys,
j.push_back(key);
nlohmann::json jm = nlohmann::json::array();
for (const Slic3r::PublishedMaterialEntry& e : material_keys)
jm.push_back({ {"material", {{"filament_type", e.filament_type}, {"filament_vendor", e.filament_vendor}, {"filament_id", e.filament_id}}}, {"slot", e.slot}, {"keys", e.keys} });
jm.push_back({ {"material", {{"filament_type", e.filament_type}, {"filament_vendor", e.filament_vendor}, {"filament_id", e.filament_id}}}, {"slot", e.slot}, {"keys", e.keys},
{"full", e.full}, {"full_keys", e.full_keys},
{"publish_type", e.publish_type}, {"type", e.publish_type_value},
{"publish_color", e.publish_color}, {"color", e.color} });
Model& model = this->model();
// Remember the previous metadata state so it can be restored after the export, keeping the
+1 -1
View File
@@ -383,7 +383,7 @@ public:
bool preview_zip_archive(const boost::filesystem::path& archive_path);
// BBS: restore
std::vector<size_t> load_files(const std::vector<boost::filesystem::path>& input_files, LoadStrategy strategy = LoadStrategy::LoadModel | LoadStrategy::LoadConfig, bool ask_multi = false);
std::vector<size_t> load_files(const std::vector<boost::filesystem::path>& input_files, LoadStrategy strategy = LoadStrategy::LoadModel | LoadStrategy::LoadConfig, bool ask_multi = false, bool* published_out = nullptr);
// To be called when providing a list of files to the GUI slic3r on command line.
std::vector<size_t> load_files(const std::vector<std::string>& input_files, LoadStrategy strategy = LoadStrategy::LoadModel | LoadStrategy::LoadConfig, bool ask_multi = false);
// to be called on drag and drop
+115 -98
View File
@@ -290,6 +290,25 @@ void PublishSettingsDialog::build_option_model()
const PublishMaterialIdentity identity = material_identity(slot, full);
const wxString title = material_title(slot, bundle, full);
const size_t category_index = category_index_for(title, Section::Material, "custom-gcode_filament", g, slot, identity);
// Filament-publishing-v2 rows: the author may require a filament colour and/or
// a vendor-agnostic material type for this slot. They live in their own
// optgroup so they stay visually separated from the setting rows.
{
const size_t req_sub = subcategory_index_for(category_index, _L("Material"), "custom-gcode_filament");
std::string hex;
if (const auto* colours = full.opt<ConfigOptionStrings>("filament_colour"))
if (slot < colours->size())
hex = colours->get_at(slot);
add_row_ui("filament_colour", _L("Color"), from_u8(hex), wxString(), category_index, req_sub, RowKind::Color);
std::string type;
if (const auto* types = full.opt<ConfigOptionStrings>("filament_type"))
if (slot < types->size())
type = types->get_at(slot);
add_row_ui("filament_type", _L("Type"), from_u8(normalize_filament_type(type)), wxString(), category_index, req_sub,
RowKind::Type);
}
// A material section must not repeat a key; the same key may
// appear in other material sections - that is intended.
std::set<std::string> material_added;
@@ -371,6 +390,10 @@ void PublishSettingsDialog::build_option_model()
dirty_base.insert(n == std::string::npos ? key : key.substr(0, n));
}
for (Row& row : m_rows) {
// The Color/Type requirement rows are not "dirty overrides": they are never
// auto-checked by the dirty pre-check.
if (row.kind != RowKind::Setting)
continue;
std::string base = row.key.substr(0, row.key.find('#'));
row.dirty = dirty_base.count(base) > 0;
if (row.dirty) {
@@ -379,24 +402,11 @@ void PublishSettingsDialog::build_option_model()
}
}
// Wire the inner-page tri-state headers: clicking a header toggles all its children;
// toggling any child re-syncs its header. Bind by index so the lambdas stay
// valid even if the vectors are reallocated later.
for (size_t c = 0; c < m_categories.size(); ++c) {
if (m_categories[c].master_check != nullptr)
m_categories[c].master_check->Bind(wxEVT_CHECKBOX, [this, c](wxCommandEvent&) { on_master_toggle(c); });
if (m_categories[c].header != nullptr)
m_categories[c].header->Bind(wxEVT_CHECKBOX, [this, c](wxCommandEvent&) { on_category_toggle(c); });
for (size_t r : m_categories[c].rows)
m_rows[r].check->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent&) { update_all_headers(); });
update_category_header(m_categories[c]);
}
// Material pages start gated (master OFF): their rows and tri-state are
// disabled until the author opts the material in.
// Wire the "Full Publish" checkboxes: toggling one disables/enables the material's
// rows. Bind by index so the lambda stays valid even if the vector is reallocated later.
for (size_t c = 0; c < m_categories.size(); ++c)
if (m_categories[c].section == Section::Material)
on_master_toggle(c);
if (m_categories[c].full_check != nullptr)
m_categories[c].full_check->Bind(wxEVT_CHECKBOX, [this, c](wxCommandEvent&) { on_full_toggle(c); });
// No filter is active at startup: every row matches until the user types.
for (Row& row : m_rows)
@@ -502,13 +512,13 @@ size_t PublishSettingsDialog::category_index_for(const wxString& title,
category.filament_color_chip = new wxStaticBitmap(category.page, wxID_ANY, *chip);
header_sizer->Add(category.filament_color_chip, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(4));
}
category.master_check = new wxCheckBox(category.page, wxID_ANY, title);
category.master_check->SetFont(Label::Head_14);
category.master_check->SetToolTip(_L("Export this material"));
header_sizer->Add(category.master_check, 0, wxALIGN_CENTER_VERTICAL);
category.header = new wxCheckBox(category.page, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxCHK_3STATE);
category.header->SetToolTip(_L("Select/deselect all settings in this material"));
header_sizer->Add(category.header, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(6));
category.title_label = new wxStaticText(category.page, wxID_ANY, title);
category.title_label->SetFont(Label::Head_14);
header_sizer->Add(category.title_label, 0, wxALIGN_CENTER_VERTICAL);
category.full_check = new wxCheckBox(category.page, wxID_ANY, _L("Full Publish"));
category.full_check->SetFont(Label::Body_13);
category.full_check->SetToolTip(_L("Embed the entire filament of this slot in the 3MF file"));
header_sizer->Add(category.full_check, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(10));
page_sizer->Add(header_sizer, 0, wxEXPAND | wxTOP | wxLEFT | wxRIGHT, FromDIP(6));
}
@@ -575,7 +585,8 @@ void PublishSettingsDialog::add_row_ui(const std::string& key,
const wxString& value,
const wxString& unit,
size_t category_index,
size_t subcategory_index)
size_t subcategory_index,
RowKind kind)
{
Category& category = m_categories[category_index];
Row row;
@@ -583,6 +594,7 @@ void PublishSettingsDialog::add_row_ui(const std::string& key,
row.label = label;
row.value = value;
row.unit = unit;
row.kind = kind;
row.category = category.title;
row.subcategory = category.subs[subcategory_index].title;
row.section = category.section;
@@ -595,82 +607,38 @@ void PublishSettingsDialog::add_row_ui(const std::string& key,
Row& current = m_rows[row_index];
current.check = new wxCheckBox(category.scroll, wxID_ANY, label);
current.check->SetFont(Label::Body_13);
auto* row_sizer = new wxBoxSizer(wxHORIZONTAL);
row_sizer->Add(current.check, 0, wxALIGN_CENTER_VERTICAL);
// The value is read-only text (incl. the Type row: the published type is the slot's
// normalized type, the author cannot pick a different one here).
current.value_label = new wxStaticText(category.scroll, wxID_ANY, value, wxDefaultPosition, wxDefaultSize, wxST_ELLIPSIZE_END);
current.value_label->SetFont(Label::Body_13);
current.value_label->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#262E30")));
current.value_label->SetToolTip(unit.IsEmpty() ? value : value + " " + unit);
if (kind == RowKind::Color && !value.IsEmpty()) {
if (wxBitmap* chip = get_extruder_color_icon(value.ToStdString(), "", FromDIP(12), FromDIP(12))) {
current.color_chip = new wxStaticBitmap(category.scroll, wxID_ANY, *chip);
row_sizer->Add(current.color_chip, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(8));
}
}
row_sizer->Add(current.value_label, 1, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(8));
if (!unit.IsEmpty()) {
current.unit_label = new wxStaticText(category.scroll, wxID_ANY, unit);
current.unit_label->SetFont(Label::Body_13);
current.unit_label->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#6B6B6B")));
}
auto* row_sizer = new wxBoxSizer(wxHORIZONTAL);
row_sizer->Add(current.check, 0, wxALIGN_CENTER_VERTICAL);
row_sizer->Add(current.value_label, 1, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(8));
if (current.unit_label != nullptr)
row_sizer->Add(current.unit_label, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(4));
}
current.item = category.list_sizer->Add(row_sizer, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(38));
category.rows.push_back(row_index);
category.subs[subcategory_index].rows.push_back(row_index);
}
void PublishSettingsDialog::on_category_toggle(size_t category_index)
void PublishSettingsDialog::on_full_toggle(size_t category_index)
{
Category& cat = m_categories[category_index];
// Defensive: a gated material header is disabled and cannot fire.
if (cat.section == Section::Material && !cat.master)
return;
// A click on the header toggles between "all" and "none": if every child is
// checked, uncheck all; otherwise check all.
bool all_checked = true;
cat.full = cat.full_check->GetValue();
for (size_t r : cat.rows)
if (!m_rows[r].check->GetValue()) {
all_checked = false;
break;
}
bool value = !all_checked;
for (size_t r : cat.rows)
m_rows[r].check->SetValue(value);
update_all_headers();
}
void PublishSettingsDialog::on_master_toggle(size_t category_index)
{
Category& cat = m_categories[category_index];
cat.master = cat.master_check->GetValue();
for (size_t r : cat.rows)
m_rows[r].check->Enable(cat.master);
cat.header->Enable(cat.master);
update_all_headers();
}
void PublishSettingsDialog::update_all_headers()
{
for (Category& category : m_categories)
update_category_header(category);
}
void PublishSettingsDialog::update_category_header(Category& category)
{
if (category.header == nullptr)
return;
// A gated material section's tri-state must not reflect the preserved
// (greyed-out) row values.
if (category.section == Section::Material && !category.master) {
category.header->Set3StateValue(wxCHK_UNCHECKED);
return;
}
int checked = 0;
for (size_t r : category.rows)
if (m_rows[r].check->GetValue())
++checked;
if (checked == 0)
category.header->Set3StateValue(wxCHK_UNCHECKED);
else if (checked == static_cast<int>(category.rows.size()))
category.header->Set3StateValue(wxCHK_CHECKED);
else
category.header->Set3StateValue(wxCHK_UNDETERMINED);
m_rows[r].check->Enable(!cat.full);
}
void PublishSettingsDialog::set_row_bold(Row& row, bool bold)
@@ -845,7 +813,6 @@ void PublishSettingsDialog::select_all(bool value)
for (Row& row : m_rows)
if (row.check->IsEnabled())
row.check->SetValue(value);
update_all_headers();
}
bool PublishSettingsDialog::row_is_visible(const Row& row) const
@@ -876,9 +843,8 @@ void PublishSettingsDialog::select_visible(bool value)
// re-enters apply_filter() - that is fine, the rows above were already
// toggled and the trailing call below is idempotent.
m_filter_ctrl->ChangeValue("");
apply_filter(""); // resync visibility, headers and the All/None bar
apply_filter(""); // resync visibility and the All/None bar
}
update_all_headers();
}
void PublishSettingsDialog::show_menu(wxMouseEvent& evt)
@@ -942,24 +908,67 @@ std::vector<Slic3r::PublishedMaterialEntry> PublishSettingsDialog::GetPublishedM
{
std::vector<Slic3r::PublishedMaterialEntry> out;
for (const Category& cat : m_categories) {
// Only opted-in materials export their keys.
if (cat.section != Section::Material || !cat.master)
if (cat.section != Section::Material)
continue;
Slic3r::PublishedMaterialEntry entry;
entry.filament_type = cat.filament_type;
entry.filament_vendor = cat.filament_vendor;
entry.filament_id = cat.filament_id;
entry.slot = static_cast<int>(cat.filament_slot);
for (size_t r : cat.rows)
if (m_rows[r].check->GetValue())
entry.keys.push_back(m_rows[r].key);
// A section without any checked key carries no information for the writer.
if (!entry.keys.empty())
// "Full Publish": the entire filament preset of the slot is embedded; type and color
// are implicitly published, and the per-key rows are disabled and their state is ignored.
if (cat.full_check != nullptr && cat.full_check->GetValue()) {
entry.full = true;
entry.full_keys = full_keys_for_slot();
entry.publish_type = true;
entry.publish_type_value = normalize_filament_type(cat.filament_type);
for (size_t r : cat.rows) {
const Row& row = m_rows[r];
if (row.kind == RowKind::Color && !row.value.IsEmpty()) {
entry.publish_color = true;
entry.color = row.value.ToStdString();
}
}
out.push_back(std::move(entry));
continue;
}
for (size_t r : cat.rows) {
const Row& row = m_rows[r];
if (!row.check->GetValue())
continue;
if (row.kind == RowKind::Color) {
entry.publish_color = true;
entry.color = row.value.ToStdString();
} else if (row.kind == RowKind::Type) {
entry.publish_type = true;
entry.publish_type_value = row.value.ToStdString();
} else {
entry.keys.push_back(row.key);
}
}
// A material with only setting keys but none checked, or with nothing selected at all,
// carries no information for the writer.
if (!entry.keys.empty() || entry.publish_type || entry.publish_color)
out.push_back(std::move(entry));
}
return out;
}
std::vector<std::string> PublishSettingsDialog::full_keys_for_slot() const
{
// The canonical filament preset keys, minus the structural keys the published overlay must
// never touch (inherits, compatibility, *_settings_id, ...), plus filament_colour (not a
// member of Preset::filament_options). The values travel in the exported config, masked to
// this slot, and are applied on load onto the receiver's slot.
const std::set<std::string>& denylist = publish_structural_keys();
std::vector<std::string> keys;
for (const std::string& key : Preset::filament_options())
if (denylist.count(key) == 0)
keys.emplace_back(key);
keys.emplace_back("filament_colour");
return keys;
}
void PublishSettingsDialog::on_dpi_changed(const wxRect& suggested_rect)
{
// Rescale toolbar bitmaps and icons; collapse chevrons are vector-drawn and repaint themselves.
@@ -974,10 +983,10 @@ void PublishSettingsDialog::on_dpi_changed(const wxRect& suggested_rect)
cat.icon_bmp.msw_rescale();
cat.icon->SetBitmap(cat.icon_bmp.bmp());
}
if (cat.header != nullptr)
cat.header->Refresh();
if (cat.master_check != nullptr)
cat.master_check->Refresh();
if (cat.full_check != nullptr)
cat.full_check->Refresh();
if (cat.title_label != nullptr)
cat.title_label->Refresh();
if (cat.filament_color_chip != nullptr) {
std::string hex;
const DynamicPrintConfig full = wxGetApp().preset_bundle->full_config();
@@ -994,6 +1003,14 @@ void PublishSettingsDialog::on_dpi_changed(const wxRect& suggested_rect)
for (SectionGroup& section : m_sections)
section.tabs->Rescale();
// Refresh the per-row Color chips at the new DPI.
for (Row& row : m_rows) {
if (row.color_chip != nullptr && !row.value.IsEmpty()) {
if (wxBitmap* chip = get_extruder_color_icon(row.value.ToStdString(), "", FromDIP(12), FromDIP(12)))
row.color_chip->SetBitmap(*chip);
}
}
const DynamicPrintConfig full = wxGetApp().preset_bundle->full_config();
for (size_t category_index = 0; category_index < m_categories.size(); ++category_index) {
const Category& category = m_categories[category_index];
+19 -14
View File
@@ -59,6 +59,11 @@ private:
// One selectable setting row: a checkbox (setting name) plus a value label
// and a (optional) grey unit label. key is the full config key and may carry
// a "#N" variant suffix (print/printer rows); material rows carry the base key.
enum class RowKind {
Setting, // a regular setting key
Color, // material colour requirement (filament_colour)
Type, // material type requirement (read-only text)
};
struct Row
{
std::string key;
@@ -69,6 +74,7 @@ private:
wxString unit;
wxString section_title; // outer tab title, for filter matching
Section section{Section::Print};
RowKind kind{RowKind::Setting};
size_t outer_index{0};
size_t inner_index{0};
size_t subcategory_index{0};
@@ -77,6 +83,7 @@ private:
wxCheckBox* check{nullptr};
wxStaticText* value_label{nullptr};
wxStaticText* unit_label{nullptr};
wxStaticBitmap* color_chip{nullptr}; // Color rows only; swatch next to the value
wxSizerItem* item{nullptr}; // sizer item of this row's h-sizer in its tab list sizer
};
@@ -89,7 +96,7 @@ private:
std::vector<size_t> rows;
};
// An inner TabCtrl page with its select-all/material controls and content.
// An inner TabCtrl page with its material controls and content.
struct Category
{
wxString title;
@@ -106,11 +113,11 @@ private:
ScalableBitmap icon_bmp; // scalable bitmap for DPI changes
wxStaticBitmap* icon{nullptr};
wxStaticBitmap* filament_color_chip{nullptr};
wxCheckBox* header{nullptr}; // material select-all tri-state; null for Printer/Process
// Material opt-in: the master checkbox carries the material title and
// gates whether this material's keys may be exported.
bool master{false};
wxCheckBox* master_check{nullptr};
wxStaticText* title_label{nullptr}; // material title (static text, Full Publish carries the label elsewhere)
// "Full Publish": serializing the entire filament preset of this slot. While checked,
// the slot's rows (incl. Color/Type) are disabled.
bool full{false};
wxCheckBox* full_check{nullptr};
// Material identity, only for Section::Material categories.
std::string filament_type;
std::string filament_vendor;
@@ -118,7 +125,7 @@ private:
// The author's 0-based filament slot this material section represents.
size_t filament_slot{0};
std::vector<Subcategory> subs;
std::vector<size_t> rows; // flattened rows, for the tri-state math
std::vector<size_t> rows; // flattened rows of this category
};
// One outer TabCtrl page. Category entries are its inner tabs.
@@ -140,25 +147,23 @@ private:
void select_all(bool value);
void select_visible(bool value);
void show_menu(wxMouseEvent& evt);
void update_category_header(Category& category);
void set_row_bold(Row& row, bool bold);
void on_category_toggle(size_t category_index);
// Material opt-in toggled: enables/disables the material's rows + tri-state
// and resyncs the header.
void on_master_toggle(size_t category_index);
// "Full Publish" toggled: disables/enables the material's rows.
void on_full_toggle(size_t category_index);
// Return/create the fixed outer page for a Section kind.
size_t section_group_for(Section kind);
size_t category_index_for(const wxString& title, Section section, const std::string& icon_name, size_t group,
size_t source_index, const PublishMaterialIdentity& identity = PublishMaterialIdentity());
size_t subcategory_index_for(size_t category_index, const wxString& title, const wxString& icon);
void add_row_ui(const std::string& key, const wxString& label, const wxString& value, const wxString& unit,
size_t category_index, size_t subcategory_index);
size_t category_index, size_t subcategory_index, RowKind kind = RowKind::Setting);
// The non-structural filament keys of a slot's preset, for a "Full Publish" entry.
std::vector<std::string> full_keys_for_slot() const;
void save_scroll_position(Category& category);
void show_outer_page(size_t section_index);
void show_inner_page(size_t section_index, int inner_index);
void on_outer_tab_changed(wxCommandEvent& event);
void on_inner_tab_changed(size_t section_index, wxCommandEvent& event);
void update_all_headers();
bool row_is_visible(const Row& row) const;
void apply_visibility();
void bind_tab_events();