Add mixed filament support: Implement MixedFilament and MixedFilamentManager classes for layer-based color mixing. Update CLI to handle downward_check option. Enhance GUI to display mixed filaments and integrate with existing filament management.

This commit is contained in:
Rad
2026-02-09 22:35:18 +01:00
parent 706508c792
commit e529461234
21 changed files with 669 additions and 39 deletions
+7 -2
View File
@@ -2432,7 +2432,8 @@ void GLCanvas3D::reload_scene(bool refresh_immediately, bool force_full_scene_re
std::sort(model_volume_state.begin(), model_volume_state.end(), model_volume_state_lower);
std::sort(aux_volume_state.begin(), aux_volume_state.end(), model_volume_state_lower);
// BBS: normalize painting data with current filament count
// BBS: normalize painting data with current available filament count
// (physical + enabled mixed/virtual filaments).
for (unsigned int obj_idx = 0; obj_idx < (unsigned int)m_model->objects.size(); ++obj_idx) {
const ModelObject& model_object = *m_model->objects[obj_idx];
for (int volume_idx = 0; volume_idx < (int)model_object.volumes.size(); ++volume_idx) {
@@ -2441,7 +2442,11 @@ void GLCanvas3D::reload_scene(bool refresh_immediately, bool force_full_scene_re
continue;
unsigned int filaments_count = (unsigned int)dynamic_cast<const ConfigOptionStrings*>(m_config->option("filament_colour"))->values.size();
model_volume.update_extruder_count(filaments_count);
size_t available_filaments = filaments_count;
if (wxGetApp().preset_bundle != nullptr)
available_filaments = wxGetApp().preset_bundle->mixed_filaments.total_filaments(filaments_count);
model_volume.update_extruder_count(available_filaments);
}
}
+8 -4
View File
@@ -1488,14 +1488,18 @@ void MenuFactory::create_filament_action_menu(bool init, int active_filament_men
if (item_id != wxNOT_FOUND)
menu->Destroy(item_id);
wxMenu* sub_menu = new wxMenu();
std::vector<wxBitmap*> icons = get_extruder_color_icons(true);
int filaments_cnt = icons.size();
wxMenu * sub_menu = new wxMenu();
std::vector<wxBitmap *> icons = get_extruder_color_icons(true);
const std::vector<std::string> &filament_presets = wxGetApp().preset_bundle->filament_presets;
int filaments_cnt = std::max(wxGetApp().filaments_cnt(), 0);
filaments_cnt = std::min(filaments_cnt, static_cast<int>(icons.size()));
filaments_cnt = std::min(filaments_cnt, static_cast<int>(filament_presets.size()));
for (int i = 0; i < filaments_cnt; i++) {
if (i == active_filament_menu_id)
continue;
auto preset = wxGetApp().preset_bundle->filaments.find_preset(wxGetApp().preset_bundle->filament_presets[i]);
auto preset = wxGetApp().preset_bundle->filaments.find_preset(filament_presets[i]);
wxString item_name = preset ? from_u8(preset->label(false)) : wxString::Format(_L("Filament %d"), i + 1);
append_menu_item(
+7 -1
View File
@@ -70,7 +70,13 @@ static DynamicPrintConfig& printer_config()
static int filaments_count()
{
return wxGetApp().filaments_cnt();
if (wxGetApp().preset_bundle == nullptr)
return 0;
int physical = wxGetApp().filaments_cnt();
// Include enabled mixed (virtual) filaments in the count.
const auto &mixed_mgr = wxGetApp().preset_bundle->mixed_filaments;
return static_cast<int>(mixed_mgr.total_filaments(physical));
}
static void take_snapshot(const std::string& snapshot_name)
@@ -16,6 +16,8 @@
#include <GL/glew.h>
#include <algorithm>
#include <boost/log/trivial.hpp>
namespace Slic3r::GUI {
@@ -770,6 +772,29 @@ void GLGizmoMmuSegmentation::update_model_object()
}
if (updated) {
const size_t num_physical = static_cast<size_t>(std::max(wxGetApp().filaments_cnt(), 0));
size_t num_total = num_physical;
if (wxGetApp().preset_bundle != nullptr)
num_total = wxGetApp().preset_bundle->mixed_filaments.total_filaments(num_physical);
size_t max_used_state = 0;
for (const ModelVolume *mv : mo->volumes) {
if (!mv->is_model_part())
continue;
const auto &used_states = mv->mmu_segmentation_facets.get_data().used_states;
for (size_t state_idx = static_cast<size_t>(EnforcerBlockerType::Extruder1); state_idx < used_states.size(); ++state_idx) {
if (used_states[state_idx])
max_used_state = std::max(max_used_state, state_idx);
}
}
if (max_used_state > num_physical) {
BOOST_LOG_TRIVIAL(warning) << "GLGizmoMmuSegmentation::update_model_object painted virtual extruder state detected"
<< " max_used_state=" << max_used_state
<< " physical_filaments=" << num_physical
<< " total_filaments=" << num_total;
}
const ModelObjectPtrs &mos = wxGetApp().model().objects;
size_t obj_idx = std::find(mos.begin(), mos.end(), mo) - mos.begin();
wxGetApp().obj_list()->update_info_items(obj_idx);
+158 -4
View File
@@ -1,5 +1,6 @@
#include "Plater.hpp"
#include "libslic3r/Config.hpp"
#include "libslic3r/MixedFilament.hpp"
#include "common_func/common_func.hpp"
#include <cstddef>
@@ -42,6 +43,7 @@
#include <wx/popupwin.h>
#endif
#include <wx/clrpicker.h>
#include <wx/spinctrl.h>
#include <wx/tokenzr.h>
#include <wx/aui/aui.h>
@@ -634,6 +636,10 @@ struct Sidebar::priv
int m_menu_filament_id = -1;
wxPanel* m_panel_filament_content;
wxScrolledWindow* m_scrolledWindow_filament_content;
// Mixed (virtual) filaments panel
wxPanel* m_panel_mixed_filaments = nullptr;
wxBoxSizer* m_sizer_mixed_filaments = nullptr;
wxStaticLine* m_staticline2;
wxPanel* m_panel_project_title;
ScalableButton* m_filament_icon = nullptr;
@@ -1266,7 +1272,7 @@ Sidebar::Sidebar(Plater *parent)
ConfigOptionFloat* flush_multi_opt = project_config.option<ConfigOptionFloat>("flush_multiplier");
float flush_multiplier = flush_multi_opt ? flush_multi_opt->getFloat() : 1.f;
const std::vector<std::string> extruder_colours = wxGetApp().plater()->get_extruder_colors_from_plater_config();
const std::vector<std::string> extruder_colours = wxGetApp().plater()->get_extruder_colors_from_plater_config(nullptr, false);
const auto& full_config = wxGetApp().preset_bundle->full_config();
const auto& extra_flush_volumes = get_min_flush_volumes(full_config);
WipingDialog dlg(parent, cast<float>(init_matrix), cast<float>(init_extruders), extruder_colours, extra_flush_volumes, flush_multiplier);
@@ -1423,6 +1429,23 @@ Sidebar::Sidebar(Plater *parent)
scrolled_sizer->Add(p->m_panel_filament_content, 0, wxEXPAND, 0);
}
// --- Mixed Colours Panel ---
{
p->m_panel_mixed_filaments = new wxPanel(p->scrolled, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL);
p->m_panel_mixed_filaments->SetBackgroundColour(wxColour(255, 255, 255));
p->m_sizer_mixed_filaments = new wxBoxSizer(wxVERTICAL);
p->m_sizer_mixed_filaments->AddSpacer(FromDIP(4));
// Title
auto *mixed_title = new wxStaticText(p->m_panel_mixed_filaments, wxID_ANY, _L("Mixed Colors"));
mixed_title->SetFont(Label::Head_14);
p->m_sizer_mixed_filaments->Add(mixed_title, 0, wxLEFT | wxRIGHT, FromDIP(16));
p->m_sizer_mixed_filaments->AddSpacer(FromDIP(4));
p->m_panel_mixed_filaments->SetSizer(p->m_sizer_mixed_filaments);
p->m_panel_mixed_filaments->Layout();
p->m_panel_mixed_filaments->Hide(); // Hidden until 2+ filaments
scrolled_sizer->Add(p->m_panel_mixed_filaments, 0, wxEXPAND, 0);
}
{
//add project title
auto params_panel = ((MainFrame*)parent->GetParent())->m_param_panel;
@@ -2161,6 +2184,116 @@ void Sidebar::on_filaments_change(size_t num_filaments)
p->m_panel_filament_title->Refresh();
update_ui_from_settings();
update_dynamic_filament_list();
update_mixed_filament_panel();
}
void Sidebar::update_mixed_filament_panel()
{
if (!p->m_panel_mixed_filaments || !p->m_sizer_mixed_filaments)
return;
auto *preset_bundle = wxGetApp().preset_bundle;
if (!preset_bundle)
return;
const auto &mixed_mgr = preset_bundle->mixed_filaments;
const auto &mixed = mixed_mgr.mixed_filaments();
// Clear old entries safely. Using Clear(true) avoids manual child-window
// destruction loops with deferred wxWindow::Destroy().
p->m_sizer_mixed_filaments->Clear(true);
// Recreate header.
p->m_sizer_mixed_filaments->AddSpacer(FromDIP(4));
auto *mixed_title = new wxStaticText(p->m_panel_mixed_filaments, wxID_ANY, _L("Mixed Colors"));
mixed_title->SetFont(Label::Head_14);
p->m_sizer_mixed_filaments->Add(mixed_title, 0, wxLEFT | wxRIGHT, FromDIP(16));
p->m_sizer_mixed_filaments->AddSpacer(FromDIP(4));
if (mixed.empty()) {
p->m_panel_mixed_filaments->Hide();
Layout();
return;
}
int mixed_id = 0;
for (const auto &mf : mixed) {
// Row panel
auto *row = new wxPanel(p->m_panel_mixed_filaments, wxID_ANY);
row->SetBackgroundColour(wxColour(255, 255, 255));
auto *row_sizer = new wxBoxSizer(wxHORIZONTAL);
// Colour swatch
wxColour swatch_color(mf.display_color);
auto *swatch = new wxPanel(row, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(16), FromDIP(16)));
swatch->SetBackgroundColour(swatch_color);
swatch->SetMinSize(wxSize(FromDIP(16), FromDIP(16)));
row_sizer->Add(swatch, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(8));
// Label: "Filament 1 + Filament 2"
wxString label_str = wxString::Format("Filament %u + Filament %u",
(unsigned)mf.component_a, (unsigned)mf.component_b);
auto *label = new wxStaticText(row, wxID_ANY, label_str);
row_sizer->Add(label, 1, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(8));
// Ratio display
wxString ratio_str = wxString::Format("%d:%d", mf.ratio_a, mf.ratio_b);
auto *ratio_label = new wxStaticText(row, wxID_ANY, ratio_str);
row_sizer->Add(ratio_label, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(4));
// Ratio spin controls
auto *spin_a = new wxSpinCtrl(row, wxID_ANY, wxEmptyString, wxDefaultPosition,
wxSize(FromDIP(45), -1), wxSP_ARROW_KEYS, 1, 10, mf.ratio_a);
auto *spin_b = new wxSpinCtrl(row, wxID_ANY, wxEmptyString, wxDefaultPosition,
wxSize(FromDIP(45), -1), wxSP_ARROW_KEYS, 1, 10, mf.ratio_b);
row_sizer->Add(spin_a, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(2));
auto *colon = new wxStaticText(row, wxID_ANY, ":");
row_sizer->Add(colon, 0, wxALIGN_CENTER_VERTICAL);
row_sizer->Add(spin_b, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(2));
// Enable checkbox
auto *chk = new wxCheckBox(row, wxID_ANY, wxEmptyString);
chk->SetValue(mf.enabled);
row_sizer->Add(chk, 0, wxALIGN_CENTER_VERTICAL | wxLEFT | wxRIGHT, FromDIP(8));
// Bind events to update the MixedFilamentManager when ratio/enabled changes.
int capturedIdx = mixed_id;
auto *capturedSpinA = spin_a;
auto *capturedSpinB = spin_b;
auto *capturedChk = chk;
auto update_lambda = [capturedIdx, capturedSpinA, capturedSpinB, capturedChk](wxCommandEvent &) {
auto &mgr = wxGetApp().preset_bundle->mixed_filaments;
auto &mfs = mgr.mixed_filaments();
if (capturedIdx < (int)mfs.size()) {
mfs[capturedIdx].ratio_a = capturedSpinA->GetValue();
mfs[capturedIdx].ratio_b = capturedSpinB->GetValue();
mfs[capturedIdx].enabled = capturedChk->GetValue();
// Recompute display color.
ConfigOptionStrings *co = wxGetApp().preset_bundle->project_config.option<ConfigOptionStrings>("filament_colour");
if (co && mfs[capturedIdx].component_a <= co->values.size() && mfs[capturedIdx].component_b <= co->values.size()) {
mfs[capturedIdx].display_color = Slic3r::MixedFilamentManager::blend_color(
co->values[mfs[capturedIdx].component_a - 1],
co->values[mfs[capturedIdx].component_b - 1],
mfs[capturedIdx].ratio_a, mfs[capturedIdx].ratio_b);
}
}
};
spin_a->Bind(wxEVT_SPINCTRL, update_lambda);
spin_b->Bind(wxEVT_SPINCTRL, update_lambda);
chk->Bind(wxEVT_CHECKBOX, update_lambda);
row->SetSizer(row_sizer);
p->m_sizer_mixed_filaments->Add(row, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(8));
p->m_sizer_mixed_filaments->AddSpacer(FromDIP(2));
++mixed_id;
}
p->m_sizer_mixed_filaments->AddSpacer(FromDIP(8));
p->m_panel_mixed_filaments->Show();
p->m_panel_mixed_filaments->Layout();
Layout();
}
void Sidebar::add_filament() {
@@ -2845,7 +2978,7 @@ void Sidebar::auto_calc_flushing_volumes(const int modify_id)
int m_max_flush_volume = Slic3r::g_max_flush_volume;
unsigned int m_number_of_extruders = (int)(sqrt(init_matrix.size()) + 0.001);
const std::vector<std::string> extruder_colours = wxGetApp().plater()->get_extruder_colors_from_plater_config();
const std::vector<std::string> extruder_colours = wxGetApp().plater()->get_extruder_colors_from_plater_config(nullptr, false);
std::vector<std::vector<wxColour>> multi_colours;
// Support for multi-color filament
@@ -8377,6 +8510,10 @@ void Plater::priv::on_filament_color_changed(wxCommandEvent &event)
if (wxGetApp().app_config->get("auto_calculate") == "true") {
sidebar->auto_calc_flushing_volumes(modify_id);
}
// Regenerate mixed filaments and update the sidebar panel.
wxGetApp().preset_bundle->update_multi_material_filament_presets();
sidebar->update_mixed_filament_panel();
}
void Plater::priv::install_network_plugin(wxCommandEvent &event)
@@ -14238,9 +14375,13 @@ void Plater::on_filaments_change(size_t num_filaments)
part_plate->update_first_layer_print_sequence(num_filaments);
}
size_t total_filaments = num_filaments;
if (wxGetApp().preset_bundle != nullptr)
total_filaments = wxGetApp().preset_bundle->mixed_filaments.total_filaments(num_filaments);
for (ModelObject* mo : wxGetApp().model().objects) {
for (ModelVolume* mv : mo->volumes) {
mv->update_extruder_count(num_filaments);
mv->update_extruder_count(total_filaments);
}
}
}
@@ -14442,17 +14583,30 @@ void Plater::on_activate()
}
// Get vector of extruder colors considering filament color, if extruder color is undefined.
std::vector<std::string> Plater::get_extruder_colors_from_plater_config(const GCodeProcessorResult* const result) const
std::vector<std::string> Plater::get_extruder_colors_from_plater_config(const GCodeProcessorResult* const result, bool include_mixed) const
{
if (wxGetApp().is_gcode_viewer() && result != nullptr)
return result->extruder_colors;
else {
if (wxGetApp().preset_bundle == nullptr)
return {};
const Slic3r::DynamicPrintConfig* config = &wxGetApp().preset_bundle->project_config;
std::vector<std::string> filament_colors;
if (!config->has("filament_colour")) // in case of a SLA print
return filament_colors;
filament_colors = (config->option<ConfigOptionStrings>("filament_colour"))->values;
const size_t num_physical = static_cast<size_t>(std::max(wxGetApp().filaments_cnt(), 0));
filament_colors.resize(num_physical, "#26A69A");
if (include_mixed) {
// Append display colours for enabled mixed (virtual) filaments.
const auto &mixed_mgr = wxGetApp().preset_bundle->mixed_filaments;
for (const auto &dc : mixed_mgr.display_colors())
filament_colors.push_back(dc);
}
return filament_colors;
}
}
+2 -1
View File
@@ -156,6 +156,7 @@ public:
void edit_filament();
void on_filaments_delete(size_t filament_id);
void update_mixed_filament_panel();
// BBS
void on_bed_type_change(BedType bed_type);
void load_ams_list(std::string const & device, MachineObject* obj);
@@ -499,7 +500,7 @@ public:
void force_print_bed_update();
// On activating the parent window.
void on_activate();
std::vector<std::string> get_extruder_colors_from_plater_config(const GCodeProcessorResult* const result = nullptr) const;
std::vector<std::string> get_extruder_colors_from_plater_config(const GCodeProcessorResult* const result = nullptr, bool include_mixed = true) const;
std::vector<std::string> get_colors_for_color_print(const GCodeProcessorResult* const result = nullptr) const;
void update_menus();
+2 -3
View File
@@ -1702,7 +1702,7 @@ void Tab::on_value_change(const std::string& opt_key, const boost::any& value)
// -1 means caculate all
auto update_flush_volume = [](int idx = -1) {
if (idx < 0) {
size_t filament_size = wxGetApp().plater()->get_extruder_colors_from_plater_config().size();
size_t filament_size = wxGetApp().plater()->get_extruder_colors_from_plater_config(nullptr, false).size();
for (size_t i = 0; i < filament_size; ++i)
wxGetApp().plater()->sidebar().auto_calc_flushing_volumes(i);
}
@@ -5347,8 +5347,7 @@ bool Tab::select_preset(std::string preset_name, bool delete_current /*=false*/,
if (m_type == Preset::TYPE_PRINTER && wxGetApp().app_config->get_bool("remember_printer_config")) {
if (preset_name.find("Snapmaker U1") != std::string::npos) {
// 在 update_selections() 改变耗材数量之前先保存旧数量和颜色
size_t old_filament_count = m_preset_bundle->filament_presets.size();
std::vector<std::string> old_filament_colors = wxGetApp().plater()->get_extruder_colors_from_plater_config();
std::vector<std::string> old_filament_colors = wxGetApp().plater()->get_extruder_colors_from_plater_config(nullptr, false);
std::vector<std::string> old_filament_presets = m_preset_bundle->filament_presets;
m_preset_bundle->update_selections(*wxGetApp().app_config);