#include "ColorDecomposeDialog.hpp" #include #include #include #include #include #include #include #include #include "wx/graphics.h" #include "I18N.hpp" #include "GUI.hpp" #include "GUI_App.hpp" #include "format.hpp" #include "Widgets/ComboBox.hpp" #include "Widgets/DropDown.hpp" #include "Widgets/Button.hpp" #include "Widgets/CheckBox.hpp" #include "Widgets/Label.hpp" #include "wxExtensions.hpp" #include "ColorDecomposeSupport.hpp" #include "libslic3r/ColorDecomposeRecipe.hpp" namespace Slic3r { namespace GUI { static const wxColour COLOR_BRAND("#00AE42"); static const wxColour COLOR_BORDER_NORMAL("#EEEEEE"); static const wxColour COLOR_BG_CARD("#F8F8F8"); static const wxColour COLOR_LABEL_GREY("#ACACAC"); static const wxColour COLOR_TEXT_DARK("#262E30"); static const wxColour COLOR_DIVIDER("#EEEEEE"); // Standard CMYW base colors static const wxColour CMYW_CYAN(0, 255, 255); static const wxColour CMYW_MAGENTA(255, 0, 255); static const wxColour CMYW_YELLOW(255, 255, 0); static const wxColour CMYW_WHITE(255, 255, 255); // Standard RYBW base colors static const wxColour RYBW_RED(255, 0, 0); static const wxColour RYBW_YELLOW(255, 255, 0); static const wxColour RYBW_BLUE(0, 0, 255); static const wxColour RYBW_WHITE(255, 255, 255); static size_t mode_index(DecomposeMode mode) { return static_cast(mode); } static ColorDecomposeRgb wx_colour_to_recipe_rgb(const wxColour& color) { return { static_cast(color.Red()), static_cast(color.Green()), static_cast(color.Blue()) }; } static wxColour hex_to_wx_colour(const std::string& hex, const wxColour& fallback) { wxColour color(hex); return color.IsOk() ? color : fallback; } static bool same_rgb(const wxColour& lhs, const wxColour& rhs) { return lhs.Red() == rhs.Red() && lhs.Green() == rhs.Green() && lhs.Blue() == rhs.Blue(); } static DecomposeBaseColor standard_base_color_from_key(const std::string& key) { if (key == "Cyan") return DecomposeBaseColor::Cyan; if (key == "Magenta") return DecomposeBaseColor::Magenta; if (key == "Yellow") return DecomposeBaseColor::Yellow; if (key == "White") return DecomposeBaseColor::White; if (key == "Red") return DecomposeBaseColor::Red; if (key == "Green") return DecomposeBaseColor::Green; if (key == "Blue") return DecomposeBaseColor::Blue; return DecomposeBaseColor::None; } static wxColour pure_color_for_base(DecomposeBaseColor base) { switch (base) { case DecomposeBaseColor::Cyan: return CMYW_CYAN; case DecomposeBaseColor::Magenta: return CMYW_MAGENTA; case DecomposeBaseColor::Yellow: return CMYW_YELLOW; case DecomposeBaseColor::White: return CMYW_WHITE; case DecomposeBaseColor::Red: return RYBW_RED; case DecomposeBaseColor::Blue: return RYBW_BLUE; default: return *wxBLACK; } } static DecomposeBaseColor standard_base_color_for(DecomposeMode mode, const wxColour& color) { if (mode == DecomposeMode::CMYW) { if (same_rgb(color, CMYW_CYAN)) return DecomposeBaseColor::Cyan; if (same_rgb(color, CMYW_MAGENTA)) return DecomposeBaseColor::Magenta; if (same_rgb(color, CMYW_YELLOW)) return DecomposeBaseColor::Yellow; if (same_rgb(color, CMYW_WHITE)) return DecomposeBaseColor::White; } else if (mode == DecomposeMode::RYBW) { if (same_rgb(color, RYBW_RED)) return DecomposeBaseColor::Red; if (same_rgb(color, RYBW_YELLOW)) return DecomposeBaseColor::Yellow; if (same_rgb(color, RYBW_BLUE)) return DecomposeBaseColor::Blue; if (same_rgb(color, RYBW_WHITE)) return DecomposeBaseColor::White; } return DecomposeBaseColor::None; } static ColorDecomposeResult to_dialog_result(const ColorDecomposeRecipeResult& recipe, const wxColour& fallback) { ColorDecomposeResult result; result.mode = recipe.mode; result.matched_color = hex_to_wx_colour(recipe.matched_color_hex, fallback); for (const auto& comp_recipe : recipe.components) { DecomposeComponent comp; comp.colour = hex_to_wx_colour(comp_recipe.color_hex, fallback); comp.ratio = comp_recipe.ratio; comp.filament_index = static_cast(comp_recipe.filament_index); comp.base_color = standard_base_color_from_key(comp_recipe.base_color); if (comp.base_color == DecomposeBaseColor::None) comp.base_color = standard_base_color_for(recipe.mode, comp.colour); result.components.push_back(comp); } return result; } static wxPanel* create_h_divider(wxWindow* parent, int fixed_width = -1) { const int h = parent->FromDIP(1); int w = fixed_width > 0 ? fixed_width : -1; auto* panel = new wxPanel(parent, wxID_ANY, wxDefaultPosition, wxSize(w, h)); panel->SetMinSize(wxSize(w, h)); if (fixed_width > 0) panel->SetMaxSize(wxSize(fixed_width, h)); panel->SetBackgroundColour(StateColor::darkModeColorFor(COLOR_DIVIDER)); return panel; } static wxStaticText* create_mode_group_label(wxWindow* parent, const wxString& text) { auto* label = new wxStaticText(parent, wxID_ANY, text); label->SetFont(Label::Body_11); label->SetForegroundColour(StateColor::darkModeColorFor(COLOR_LABEL_GREY)); return label; } static void match_parent_bg(wxWindow* w, const wxColour& bg) { w->SetBackgroundColour(bg); } static bool material_type_matches(const std::string& a, const std::string& b) { if (a.empty() || b.empty()) return false; return a == b || a == b + " Basic" || b == a + " Basic"; } ColorDecomposeDialog::ColorDecomposeDialog(wxWindow* parent, int filament_idx, const wxColour& target_color, const std::vector& physical_colors, const std::vector& filament_names, const std::vector& filament_types, size_t current_filament_count, size_t max_filament_count, std::vector physical_config_indices) : DPIDialog(parent, wxID_ANY, _L("Decompose Color"), wxDefaultPosition, wxDefaultSize, wxCAPTION | wxCLOSE_BOX) , m_filament_idx(filament_idx) , m_target_color(target_color) , m_physical_colors(physical_colors) , m_filament_names(filament_names) , m_filament_types(filament_types) , m_current_filament_count(current_filament_count) , m_max_filament_count(max_filament_count) , m_physical_config_indices(std::move(physical_config_indices)) { for (const auto& t : m_filament_types) { if (std::find(m_project_types.begin(), m_project_types.end(), t) == m_project_types.end()) m_project_types.push_back(t); } if (m_filament_idx >= 0 && static_cast(m_filament_idx) < m_filament_types.size()) m_preferred_type = m_filament_types[m_filament_idx]; else if (!m_project_types.empty()) m_preferred_type = m_project_types.front(); build_ui(); wxGetApp().UpdateDlgDarkUI(this); // Restore target swatch after dark mode color remapping if (m_target_swatch) m_target_swatch->SetBackgroundColour(m_target_color); update_card_visibility(); Fit(); compute_decomposition(); update_matched_color_display(); update_ok_button_state(); } void ColorDecomposeDialog::on_dpi_changed(const wxRect& suggested_rect) { (void)suggested_rect; Fit(); Refresh(); } void ColorDecomposeDialog::build_ui() { SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE)); auto* main_sizer = new wxBoxSizer(wxVERTICAL); const int selector_side_margin = FromDIP(26); const int selector_top_gap = FromDIP(22); const int content_side_margin = FromDIP(30); const int target_section_top_gap = FromDIP(18); main_sizer->AddSpacer(selector_top_gap); main_sizer->Add(create_filament_selector(), 0, wxEXPAND | wxLEFT | wxRIGHT, selector_side_margin); main_sizer->AddSpacer(target_section_top_gap); main_sizer->Add(create_target_color_section(), 0, wxEXPAND | wxLEFT | wxRIGHT, content_side_margin); main_sizer->AddSpacer(FromDIP(16)); main_sizer->Add(create_h_divider(this), 0, wxEXPAND | wxLEFT | wxRIGHT, content_side_margin); main_sizer->AddSpacer(FromDIP(16)); main_sizer->Add(create_mode_selection_section(), 0, wxEXPAND | wxLEFT | wxRIGHT, content_side_margin); main_sizer->AddSpacer(FromDIP(16)); main_sizer->Add(create_button_panel(), 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, content_side_margin); SetSizer(main_sizer); SetMinSize(wxSize(FromDIP(477), FromDIP(380))); Fit(); CenterOnParent(); } wxBoxSizer* ColorDecomposeDialog::create_filament_selector() { auto* sizer = new wxBoxSizer(wxHORIZONTAL); m_type_combo = new ComboBox(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(-1, FromDIP(36)), 0, nullptr, wxCB_READONLY); m_type_combo->SetFont(Label::Body_13); m_combo_item_types.clear(); int default_sel = -1; // --- Group 1: Project filament list (deduplicated by type) --- m_type_combo->Append(_L("Project Filament List"), wxNullBitmap, DD_ITEM_STYLE_SPLIT_ITEM | DD_ITEM_STYLE_DISABLED); m_combo_item_types.push_back(std::string()); std::set seen_types; for (size_t i = 0; i < m_filament_names.size(); ++i) { const std::string& type = (i < m_filament_types.size()) ? m_filament_types[i] : "PLA"; if (!seen_types.insert(type).second) continue; int idx = m_type_combo->Append(wxString::FromUTF8(m_filament_names[i])); m_combo_item_types.push_back(type); if (type == m_preferred_type && default_sel < 0) default_sel = idx; } // --- Group 2: Standard mode material recommendations --- static const char* kStandardTypes[] = { kDecomposePlaBasicType }; m_type_combo->Append(_L("Standard Mode Recommendations"), wxNullBitmap, DD_ITEM_STYLE_SPLIT_ITEM | DD_ITEM_STYLE_DISABLED); m_combo_item_types.push_back(std::string()); for (size_t s = 0; s < sizeof(kStandardTypes) / sizeof(kStandardTypes[0]); ++s) { // Always show standard recommendations, even if the same type already // appears in the project filament list above. const std::string label = std::string(kDecomposeBambuPresetPrefix) + kStandardTypes[s]; int idx = m_type_combo->Append(wxString::FromUTF8(label)); m_combo_item_types.push_back(kStandardTypes[s]); if (kStandardTypes[s] == m_preferred_type && default_sel < 0) default_sel = idx; } if (default_sel < 0) { for (int i = 0; i < static_cast(m_combo_item_types.size()); ++i) { if (!m_combo_item_types[i].empty()) { default_sel = i; break; } } } if (default_sel >= 0) { m_type_combo->SetSelection(default_sel); if (!m_combo_item_types[default_sel].empty()) m_preferred_type = m_combo_item_types[default_sel]; } m_type_combo->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent& evt) { evt.StopPropagation(); int sel = m_type_combo->GetSelection(); if (sel >= 0 && static_cast(sel) < m_combo_item_types.size() && !m_combo_item_types[sel].empty()) { m_preferred_type = m_combo_item_types[sel]; } update_card_visibility(); compute_decomposition(); update_matched_color_display(); update_ok_button_state(); }); sizer->Add(m_type_combo, 1, wxEXPAND); return sizer; } static wxPanel* create_color_swatch(wxWindow* parent, const wxColour& color, int size) { auto* panel = new wxPanel(parent, wxID_ANY, wxDefaultPosition, wxSize(size, size)); panel->SetBackgroundColour(color); panel->SetMinSize(wxSize(size, size)); return panel; } wxBoxSizer* ColorDecomposeDialog::create_target_color_section() { auto* sizer = new wxBoxSizer(wxHORIZONTAL); auto* label = new wxStaticText(this, wxID_ANY, _L("Target Color")); label->SetFont(Label::Head_14); label->SetForegroundColour(StateColor::darkModeColorFor(COLOR_TEXT_DARK)); sizer->Add(label, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(19)); m_target_swatch = create_color_swatch(this, m_target_color, FromDIP(28)); sizer->Add(m_target_swatch, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(12)); m_target_rgb_text = new wxStaticText(this, wxID_ANY, wxString::Format("RGB: %d, %d, %d", m_target_color.Red(), m_target_color.Green(), m_target_color.Blue())); m_target_rgb_text->SetFont(Label::Body_13); m_target_rgb_text->SetForegroundColour(StateColor::darkModeColorFor(COLOR_TEXT_DARK)); sizer->Add(m_target_rgb_text, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(12)); auto* arrow_text = new wxStaticText(this, wxID_ANY, wxString::FromUTF8("\xe2\x86\x92")); arrow_text->SetForegroundColour(StateColor::darkModeColorFor(COLOR_TEXT_DARK)); sizer->Add(arrow_text, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(12)); m_matched_swatch = create_color_swatch(this, m_target_color, FromDIP(28)); sizer->Add(m_matched_swatch, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(12)); m_matched_rgb_text = new wxStaticText(this, wxID_ANY, wxString::Format("RGB: %d, %d, %d", m_target_color.Red(), m_target_color.Green(), m_target_color.Blue())); m_matched_rgb_text->SetFont(Label::Head_13); m_matched_rgb_text->SetForegroundColour(StateColor::darkModeColorFor(COLOR_TEXT_DARK)); sizer->Add(m_matched_rgb_text, 0, wxALIGN_CENTER_VERTICAL); return sizer; } wxPanel* ColorDecomposeDialog::create_mode_card(wxWindow* parent, DecomposeMode mode, const wxString& title) { const int pad = FromDIP(12); auto* card = new wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE); card->SetBackgroundStyle(wxBG_STYLE_PAINT); auto* card_sizer = new wxBoxSizer(wxVERTICAL); auto* title_sizer = new wxBoxSizer(wxHORIZONTAL); auto* title_label = new wxStaticText(card, wxID_ANY, title); title_label->SetFont(Label::Body_14); title_label->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#909090"))); match_parent_bg(title_label, StateColor::darkModeColorFor(COLOR_BG_CARD)); title_sizer->Add(title_label, 1, wxALIGN_CENTER_VERTICAL); auto* chk = new ::CheckBox(card); chk->SetValue(mode == m_selected_mode); match_parent_bg(chk, StateColor::darkModeColorFor(COLOR_BG_CARD)); switch (mode) { case DecomposeMode::MaterialList: m_chk_material_list = chk; break; case DecomposeMode::CMYW: m_chk_cmyw = chk; break; case DecomposeMode::RYBW: m_chk_rybw = chk; break; } chk->Bind(wxEVT_TOGGLEBUTTON, [this, mode](wxCommandEvent& e) { select_mode(mode); e.Skip(); // let CheckBox::update() re-sync its bitmap to GetValue() }); title_sizer->Add(chk, 0, wxALIGN_CENTER_VERTICAL); card_sizer->Add(title_sizer, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, pad); card_sizer->Add(create_h_divider(card), 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, FromDIP(8)); auto* colors_sizer = new wxBoxSizer(wxHORIZONTAL); card_sizer->Add(colors_sizer, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP | wxBOTTOM, pad); auto& controls = m_mode_cards[mode_index(mode)]; controls.card = card; controls.components_sizer = colors_sizer; card->SetSizer(card_sizer); card->SetMinSize(wxSize(FromDIP(128), FromDIP(111))); card->SetMaxSize(wxSize(FromDIP(128), FromDIP(111))); card->Bind(wxEVT_PAINT, [this, card, mode](wxPaintEvent&) { wxBufferedPaintDC dc(card); wxSize sz = card->GetClientSize(); dc.SetBackground(wxBrush(StateColor::darkModeColorFor(*wxWHITE))); dc.Clear(); bool selected = (m_selected_mode == mode); wxColour border_col = selected ? StateColor::darkModeColorFor(COLOR_BRAND) : StateColor::darkModeColorFor(COLOR_BORDER_NORMAL); const int border_width = FromDIP(selected ? 2 : 1); const double inset = border_width / 2.0; std::unique_ptr gc(wxGraphicsContext::Create(dc)); if (gc) { gc->SetPen(wxPen(border_col, border_width)); gc->SetBrush(wxBrush(StateColor::darkModeColorFor(COLOR_BG_CARD))); gc->DrawRoundedRectangle(inset, inset, sz.x - 2 * inset, sz.y - 2 * inset, FromDIP(8)); } else { const int fallback_inset = (border_width + 1) / 2; dc.SetPen(wxPen(border_col, border_width)); dc.SetBrush(wxBrush(StateColor::darkModeColorFor(COLOR_BG_CARD))); dc.DrawRoundedRectangle(fallback_inset, fallback_inset, sz.x - 2 * fallback_inset, sz.y - 2 * fallback_inset, FromDIP(8)); } }); std::function bind_click; bind_click = [this, mode, chk, &bind_click](wxWindow* w) { if (w == chk || dynamic_cast<::CheckBox*>(w)) return; w->Bind(wxEVT_LEFT_UP, [this, mode](wxMouseEvent&) { select_mode(mode); }); w->SetCursor(wxCursor(wxCURSOR_HAND)); for (auto* child : w->GetChildren()) bind_click(child); }; bind_click(card); return card; } wxBoxSizer* ColorDecomposeDialog::create_mode_selection_section() { auto* sizer = new wxBoxSizer(wxVERTICAL); auto* section_label = new wxStaticText(this, wxID_ANY, _L("Select Color Decomposition")); section_label->SetFont(Label::Head_14); section_label->SetForegroundColour(StateColor::darkModeColorFor(COLOR_TEXT_DARK)); sizer->Add(section_label, 0, wxBOTTOM, FromDIP(4)); auto* modes_sizer = new wxBoxSizer(wxHORIZONTAL); // --- Arbitrary mode column (wrapped in a panel so the whole column hides together) --- m_arb_column_panel = new wxPanel(this, wxID_ANY); m_arb_column_panel->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE)); auto* arb_col = new wxBoxSizer(wxVERTICAL); { auto* arb_header_sizer = new wxBoxSizer(wxHORIZONTAL); arb_header_sizer->Add(create_mode_group_label(m_arb_column_panel, _L("Arbitrary Mode")), 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(5)); arb_header_sizer->Add(create_h_divider(m_arb_column_panel, FromDIP(88)), 0, wxALIGN_CENTER_VERTICAL); arb_col->Add(arb_header_sizer, 0, wxEXPAND | wxBOTTOM, FromDIP(8)); m_card_material_list = create_mode_card(m_arb_column_panel, DecomposeMode::MaterialList, _L("Material List")); arb_col->Add(m_card_material_list, 0, wxEXPAND); } m_arb_column_panel->SetSizer(arb_col); modes_sizer->Add(m_arb_column_panel, 0, wxEXPAND | wxRIGHT, FromDIP(16)); // --- Standard mode column --- auto* std_col = new wxBoxSizer(wxVERTICAL); { auto* std_header_sizer = new wxBoxSizer(wxHORIZONTAL); std_header_sizer->Add(create_mode_group_label(this, _L("Standard Mode")), 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(5)); std_header_sizer->Add(create_h_divider(this), 1, wxALIGN_CENTER_VERTICAL); std_col->Add(std_header_sizer, 0, wxEXPAND | wxBOTTOM, FromDIP(8)); auto* cards_sizer = new wxBoxSizer(wxHORIZONTAL); m_card_cmyw = create_mode_card(this, DecomposeMode::CMYW, "CMYW"); cards_sizer->Add(m_card_cmyw, 0, wxRIGHT, FromDIP(12)); m_card_rybw = create_mode_card(this, DecomposeMode::RYBW, "RYBW"); cards_sizer->Add(m_card_rybw, 0); std_col->Add(cards_sizer, 0, wxEXPAND); } modes_sizer->Add(std_col, 0, wxEXPAND); sizer->Add(modes_sizer, 0, wxEXPAND); m_no_card_hint = new wxStaticText(this, wxID_ANY, _L("At least two filaments of the same material type are required for decomposition")); m_no_card_hint->SetFont(Label::Body_13); m_no_card_hint->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#909090"))); m_no_card_hint->Wrap(FromDIP(400)); m_no_card_hint->Hide(); sizer->Add(m_no_card_hint, 0, wxTOP, FromDIP(8)); m_limit_warning_panel = new wxPanel(this, wxID_ANY); m_limit_warning_panel->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE)); auto* warning_sizer = new wxBoxSizer(wxHORIZONTAL); auto* warn_bmp = new wxStaticBitmap(m_limit_warning_panel, wxID_ANY, create_scaled_bitmap("obj_warning", m_limit_warning_panel, 16), wxDefaultPosition, wxSize(FromDIP(16), FromDIP(16))); m_limit_warning_text = new wxStaticText(m_limit_warning_panel, wxID_ANY, wxEmptyString); m_limit_warning_text->SetFont(Label::Body_13); m_limit_warning_text->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#D32F2F"))); m_limit_warning_text->Wrap(FromDIP(400)); warning_sizer->Add(warn_bmp, 0, wxALIGN_TOP | wxRIGHT, FromDIP(6)); warning_sizer->Add(m_limit_warning_text, 1, wxEXPAND); m_limit_warning_panel->SetSizer(warning_sizer); m_limit_warning_panel->Hide(); sizer->Add(m_limit_warning_panel, 0, wxEXPAND | wxTOP, FromDIP(8)); return sizer; } wxBoxSizer* ColorDecomposeDialog::create_button_panel() { auto* sizer = new wxBoxSizer(wxHORIZONTAL); sizer->AddStretchSpacer(); m_btn_cancel = new Button(this, _L("Cancel")); m_btn_cancel->SetBackgroundColor(StateColor::darkModeColorFor(*wxWHITE)); m_btn_cancel->SetBorderColor(StateColor::darkModeColorFor(wxColour("#CECECE"))); m_btn_cancel->SetTextColor(StateColor::darkModeColorFor(wxColour("#262E30"))); m_btn_cancel->SetMinSize(wxSize(FromDIP(55), FromDIP(24))); m_btn_cancel->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { EndModal(wxID_CANCEL); }); m_btn_ok = new Button(this, _L("OK")); m_btn_ok->SetBackgroundColor(StateColor( std::make_pair(wxColour("#C2C2C2"), (int) StateColor::Disabled), std::make_pair(wxColour("#00AE42"), (int) StateColor::Normal))); m_btn_ok->SetBorderColor(StateColor( std::make_pair(wxColour("#C2C2C2"), (int) StateColor::Disabled), std::make_pair(wxColour("#00AE42"), (int) StateColor::Normal))); m_btn_ok->SetTextColor(StateColor( std::make_pair(*wxWHITE, (int) StateColor::Disabled), std::make_pair(*wxWHITE, (int) StateColor::Normal))); m_btn_ok->SetMinSize(wxSize(FromDIP(55), FromDIP(24))); m_btn_ok->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { EndModal(wxID_OK); }); sizer->Add(m_btn_cancel, 0, wxRIGHT, FromDIP(12)); sizer->Add(m_btn_ok, 0); return sizer; } void ColorDecomposeDialog::select_mode(DecomposeMode mode) { m_selected_mode = mode; m_result = m_mode_results[mode_index(mode)]; update_card_styles(); update_matched_color_display(); update_ok_button_state(); } void ColorDecomposeDialog::update_card_styles() { if (m_card_material_list) m_card_material_list->Refresh(); if (m_card_cmyw) m_card_cmyw->Refresh(); if (m_card_rybw) m_card_rybw->Refresh(); if (m_chk_material_list) m_chk_material_list->SetValue(m_selected_mode == DecomposeMode::MaterialList); if (m_chk_cmyw) m_chk_cmyw->SetValue(m_selected_mode == DecomposeMode::CMYW); if (m_chk_rybw) m_chk_rybw->SetValue(m_selected_mode == DecomposeMode::RYBW); } void ColorDecomposeDialog::update_card_visibility() { // Count physical filaments of the same type (excluding the source filament) int same_type_count = 0; for (size_t i = 0; i < m_filament_types.size(); ++i) { if (static_cast(i) == m_filament_idx) continue; if (material_type_matches(m_filament_types[i], m_preferred_type)) ++same_type_count; } bool show_arb = (same_type_count >= 2); bool show_cmyw = (m_preferred_type == kDecomposePlaBasicType); bool show_rybw = (m_preferred_type == kDecomposePlaBasicType); if (m_arb_column_panel) m_arb_column_panel->Show(show_arb); if (m_card_material_list) m_card_material_list->Show(show_arb); if (m_card_cmyw) m_card_cmyw->Show(show_cmyw); if (m_card_rybw) m_card_rybw->Show(show_rybw); bool any_visible = show_arb || show_cmyw || show_rybw; if (m_no_card_hint) m_no_card_hint->Show(!any_visible); // Auto-select a visible mode when current selection becomes hidden if (any_visible) { bool cur_visible = false; if (m_selected_mode == DecomposeMode::MaterialList && show_arb) cur_visible = true; if (m_selected_mode == DecomposeMode::CMYW && show_cmyw) cur_visible = true; if (m_selected_mode == DecomposeMode::RYBW && show_rybw) cur_visible = true; if (!cur_visible) { if (show_arb) select_mode(DecomposeMode::MaterialList); else if (show_cmyw) select_mode(DecomposeMode::CMYW); else select_mode(DecomposeMode::RYBW); } } Layout(); update_ok_button_state(); } void ColorDecomposeDialog::update_filament_limit_warning() { if (!m_limit_warning_panel || !m_limit_warning_text) return; size_t missing_new = 0; if (m_missing_calculator) { missing_new = m_missing_calculator(m_result); } else { const size_t source_physical_idx = m_filament_idx >= 0 ? static_cast(m_filament_idx) : size_t(-1); const std::vector* indices = m_physical_config_indices.empty() ? nullptr : &m_physical_config_indices; missing_new = count_decompose_new_physical_filaments( m_result, m_physical_colors, m_filament_types, source_physical_idx, indices); } // A result with fewer than 2 components (e.g. target color is already a // standard base color shown as "100%") creates no mixed filament and no new // physical filament, so it can never exceed the limit. const bool creates_mixed = m_result.components.size() >= 2; // +1 for the mixed filament slot that will be created after decomposition. const size_t needed = m_current_filament_count + missing_new + 1; const bool blocked = creates_mixed && needed > m_max_filament_count; const bool was_shown = m_limit_warning_panel->IsShown(); if (!blocked) { if (was_shown) { m_limit_warning_panel->Hide(); Layout(); Fit(); CenterOnParent(); } return; } wxString mode_name; switch (m_selected_mode) { case DecomposeMode::CMYW: mode_name = "CMYW"; break; case DecomposeMode::RYBW: mode_name = "RYBW"; break; case DecomposeMode::MaterialList: mode_name = _L("Material List"); break; } const wxString warning_text = format_wxstr( _L("The material list supports at most %1% colors. After %2% decomposition, the material count would exceed %1%. Please delete unused filaments on the main screen before decomposing."), m_max_filament_count, mode_name); // Show first so the panel is laid out and the text control gets its real // width, then wrap to that width so the paragraph fills the content area. m_limit_warning_panel->Show(); Layout(); const int avail = m_limit_warning_text->GetClientSize().x; m_limit_warning_text->SetLabel(warning_text); if (avail > FromDIP(50)) m_limit_warning_text->Wrap(avail); Layout(); // Only resize/recenter when the warning panel actually toggled from hidden // to shown. While already visible, switching modes must not re-Fit/recenter // the dialog, which would make it jump on every card switch. if (!was_shown) { Fit(); CenterOnParent(); } } void ColorDecomposeDialog::set_missing_physical_calculator(std::function fn) { m_missing_calculator = std::move(fn); update_ok_button_state(); } void ColorDecomposeDialog::update_ok_button_state() { if (!m_btn_ok) return; update_filament_limit_warning(); bool any_card_visible = (m_card_material_list && m_card_material_list->IsShown()) || (m_card_cmyw && m_card_cmyw->IsShown()) || (m_card_rybw && m_card_rybw->IsShown()); const bool blocked = m_limit_warning_panel && m_limit_warning_panel->IsShown(); m_btn_ok->Enable(any_card_visible && !blocked); Layout(); } void ColorDecomposeDialog::update_mode_card_content(DecomposeMode mode) { auto& controls = m_mode_cards[mode_index(mode)]; auto* sizer = controls.components_sizer; auto* card = controls.card; if (!sizer || !card) return; sizer->Clear(true); const auto& components = m_mode_results[mode_index(mode)].components; const size_t count = components.size(); if (count == 0) { card->Layout(); card->Refresh(); return; } const int swatch_sz = FromDIP(24); const int plus_gap = FromDIP(24); const wxFont& ratio_font = Label::Body_13; auto bind_select = [this, mode](wxWindow* w) { w->Bind(wxEVT_LEFT_UP, [this, mode](wxMouseEvent&) { select_mode(mode); }); w->SetCursor(wxCursor(wxCURSOR_HAND)); }; for (size_t i = 0; i < count; ++i) { auto* col = new wxBoxSizer(wxVERTICAL); auto* swatch = create_color_swatch(card, components[i].colour, swatch_sz); bind_select(swatch); col->Add(swatch, 0, wxALIGN_CENTER_HORIZONTAL); auto* ratio_text = new wxStaticText(card, wxID_ANY, wxString::Format("%d%%", components[i].ratio)); ratio_text->SetFont(ratio_font); ratio_text->SetForegroundColour(StateColor::darkModeColorFor(COLOR_TEXT_DARK)); match_parent_bg(ratio_text, StateColor::darkModeColorFor(COLOR_BG_CARD)); bind_select(ratio_text); col->Add(ratio_text, 0, wxALIGN_CENTER_HORIZONTAL | wxTOP, FromDIP(4)); sizer->Add(col, 0, wxALIGN_TOP); if (i + 1 < count) { sizer->AddStretchSpacer(); auto* plus_panel = new wxPanel(card, wxID_ANY, wxDefaultPosition, wxSize(plus_gap, swatch_sz)); plus_panel->SetMinSize(wxSize(plus_gap, swatch_sz)); plus_panel->SetMaxSize(wxSize(plus_gap, swatch_sz)); plus_panel->SetBackgroundColour(StateColor::darkModeColorFor(COLOR_BG_CARD)); auto* plus_sizer = new wxBoxSizer(wxVERTICAL); auto* plus_label = new wxStaticText(plus_panel, wxID_ANY, "+"); plus_label->SetFont(Label::Body_13); plus_label->SetForegroundColour(StateColor::darkModeColorFor(COLOR_TEXT_DARK)); match_parent_bg(plus_label, StateColor::darkModeColorFor(COLOR_BG_CARD)); bind_select(plus_panel); bind_select(plus_label); plus_sizer->AddStretchSpacer(); plus_sizer->Add(plus_label, 0, wxALIGN_CENTER_HORIZONTAL); plus_sizer->AddStretchSpacer(); plus_panel->SetSizer(plus_sizer); sizer->Add(plus_panel, 0, wxALIGN_TOP); sizer->AddStretchSpacer(); } } const int card_width = FromDIP(128 + (count > 2 ? static_cast(count - 2) * 31 : 0)); card->SetMinSize(wxSize(card_width, FromDIP(111))); card->SetMaxSize(wxSize(card_width, FromDIP(111))); card->Layout(); card->Refresh(); } void ColorDecomposeDialog::update_mode_card_contents() { update_mode_card_content(DecomposeMode::MaterialList); update_mode_card_content(DecomposeMode::CMYW); update_mode_card_content(DecomposeMode::RYBW); Layout(); Fit(); } void ColorDecomposeDialog::update_matched_color_display() { if (!m_result.matched_color.IsOk()) m_result.matched_color = m_target_color; if (m_matched_swatch) { m_matched_swatch->SetBackgroundColour(m_result.matched_color); m_matched_swatch->Refresh(); } if (m_matched_rgb_text) { m_matched_rgb_text->SetLabel(wxString::Format("RGB: %d, %d, %d", m_result.matched_color.Red(), m_result.matched_color.Green(), m_result.matched_color.Blue())); } } bool ColorDecomposeDialog::try_build_single_base_result(DecomposeMode mode, ColorDecomposeResult& out) const { // Gate by preferred type, matching card visibility: CMYW and RYBW only for PLA Basic. if (mode == DecomposeMode::CMYW || mode == DecomposeMode::RYBW) { if (m_preferred_type != kDecomposePlaBasicType) return false; } else { return false; } static const DecomposeBaseColor cmyw_bases[] = { DecomposeBaseColor::Cyan, DecomposeBaseColor::Magenta, DecomposeBaseColor::Yellow, DecomposeBaseColor::White }; static const DecomposeBaseColor rybw_bases[] = { DecomposeBaseColor::Red, DecomposeBaseColor::Yellow, DecomposeBaseColor::Blue, DecomposeBaseColor::White }; const DecomposeBaseColor* bases = (mode == DecomposeMode::CMYW) ? cmyw_bases : rybw_bases; const size_t base_count = (mode == DecomposeMode::CMYW) ? sizeof(cmyw_bases) / sizeof(cmyw_bases[0]) : sizeof(rybw_bases) / sizeof(rybw_bases[0]); const std::string target_hex = decompose_normalize_color_hex( m_target_color.GetAsString(wxC2S_HTML_SYNTAX).ToStdString()); for (size_t i = 0; i < base_count; ++i) { const DecomposeBaseColor base = bases[i]; DecomposeOfficialComponent official = lookup_decompose_official_component(m_preferred_type, base, pure_color_for_base(base)); if (decompose_normalize_color_hex(official.color_hex) != target_hex) continue; out = ColorDecomposeResult{}; out.mode = mode; out.matched_color = hex_to_wx_colour(official.color_hex, m_target_color); DecomposeComponent comp; comp.colour = out.matched_color; comp.ratio = 100; comp.filament_index = -1; comp.base_color = base; out.components.push_back(comp); return true; } return false; } void ColorDecomposeDialog::compute_decomposition() { auto fallback_result = [this](DecomposeMode mode, const std::vector& components) { ColorDecomposeResult result; result.mode = mode; result.components = components; int total = 0; double r = 0.0, g = 0.0, b = 0.0; for (const auto& comp : result.components) total += comp.ratio; if (total <= 0) total = 100; for (const auto& comp : result.components) { const double w = static_cast(comp.ratio) / total; r += comp.colour.Red() * w; g += comp.colour.Green() * w; b += comp.colour.Blue() * w; } result.matched_color = result.components.empty() ? m_target_color : wxColour(static_cast(std::clamp(r, 0.0, 255.0)), static_cast(std::clamp(g, 0.0, 255.0)), static_cast(std::clamp(b, 0.0, 255.0))); return result; }; std::vector physical_filaments; physical_filaments.reserve(m_physical_colors.size()); for (size_t i = 0; i < m_physical_colors.size(); ++i) { if (m_filament_idx >= 0 && i == static_cast(m_filament_idx)) continue; ColorDecomposePhysicalFilament filament; filament.color_hex = m_physical_colors[i]; filament.name = i < m_filament_names.size() ? m_filament_names[i] : ""; filament.type = i < m_filament_types.size() ? m_filament_types[i] : ""; filament.filament_index = static_cast(i + 1); physical_filaments.push_back(std::move(filament)); } const ColorDecomposeRgb target_rgb = wx_colour_to_recipe_rgb(m_target_color); auto material_recipe = recommend_from_physical_filaments(target_rgb, physical_filaments, m_preferred_type); if (material_recipe.valid) { m_mode_results[mode_index(DecomposeMode::MaterialList)] = to_dialog_result(material_recipe, m_target_color); } else { std::vector components; for (size_t i = 0; i < std::min(2, physical_filaments.size()); ++i) { DecomposeComponent comp; comp.colour = wxColour(physical_filaments[i].color_hex); comp.ratio = 50; comp.filament_index = static_cast(physical_filaments[i].filament_index); components.push_back(comp); } if (components.empty()) { components.push_back({m_target_color, 100, -1}); } else if (components.size() == 1) { components.front().ratio = 100; } m_mode_results[mode_index(DecomposeMode::MaterialList)] = fallback_result(DecomposeMode::MaterialList, components); } ColorDecomposeResult single_base; if (try_build_single_base_result(DecomposeMode::CMYW, single_base)) { m_mode_results[mode_index(DecomposeMode::CMYW)] = single_base; } else { auto cmyw_recipe = lookup_standard_recipe(target_rgb, ColorDecomposeRecipeMode::CMYW, m_preferred_type); m_mode_results[mode_index(DecomposeMode::CMYW)] = cmyw_recipe.valid ? to_dialog_result(cmyw_recipe, m_target_color) : fallback_result(DecomposeMode::CMYW, { {CMYW_YELLOW, 50, -1, DecomposeBaseColor::Yellow}, {CMYW_CYAN, 50, -1, DecomposeBaseColor::Cyan} }); } if (try_build_single_base_result(DecomposeMode::RYBW, single_base)) { m_mode_results[mode_index(DecomposeMode::RYBW)] = single_base; } else { auto rybw_recipe = lookup_standard_recipe(target_rgb, ColorDecomposeRecipeMode::RYBW, m_preferred_type); m_mode_results[mode_index(DecomposeMode::RYBW)] = rybw_recipe.valid ? to_dialog_result(rybw_recipe, m_target_color) : fallback_result(DecomposeMode::RYBW, { {RYBW_YELLOW, 50, -1, DecomposeBaseColor::Yellow}, {RYBW_BLUE, 50, -1, DecomposeBaseColor::Blue} }); } m_result = m_mode_results[mode_index(m_selected_mode)]; update_mode_card_contents(); update_ok_button_state(); } } // namespace GUI } // namespace Slic3r