From 0178ad32add05653f26753901ed240d6d8503801 Mon Sep 17 00:00:00 2001 From: Rad Date: Wed, 11 Feb 2026 16:55:59 +0100 Subject: [PATCH] Enhance mixed filament management: Introduce manual pattern support for mixed filaments, allowing users to define custom layer patterns using '1' and '2' for component selection. Update parsing logic to accommodate new pattern definitions and ensure backward compatibility. Improve GUI to facilitate pattern entry and display, enhancing user experience in mixed filament configurations. --- src/libslic3r/MixedFilament.cpp | 126 ++++++++++++++++++++++++++++---- src/libslic3r/MixedFilament.hpp | 10 +++ src/slic3r/GUI/Plater.cpp | 108 ++++++++++++++++++++------- 3 files changed, 206 insertions(+), 38 deletions(-) diff --git a/src/libslic3r/MixedFilament.cpp b/src/libslic3r/MixedFilament.cpp index 93af56c1ab..75489da3f4 100644 --- a/src/libslic3r/MixedFilament.cpp +++ b/src/libslic3r/MixedFilament.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -261,35 +262,100 @@ static bool parse_row_definition(const std::string &row, unsigned int &b, bool &enabled, bool &custom, - int &mix_b_percent) + int &mix_b_percent, + std::string &manual_pattern) { - std::vector values; - std::stringstream ss(row); - std::string token; - while (std::getline(ss, token, ',')) { - if (token.empty()) + auto trim_copy = [](const std::string &s) { + size_t lo = 0; + size_t hi = s.size(); + while (lo < hi && std::isspace(static_cast(s[lo]))) + ++lo; + while (hi > lo && std::isspace(static_cast(s[hi - 1]))) + --hi; + return s.substr(lo, hi - lo); + }; + + auto parse_int_token = [&trim_copy](const std::string &tok, int &out) { + const std::string t = trim_copy(tok); + if (t.empty()) return false; try { - values.push_back(std::stoi(token)); + size_t consumed = 0; + int v = std::stoi(t, &consumed); + if (consumed != t.size()) + return false; + out = v; + return true; } catch (...) { return false; } - } + }; - if (values.size() != 4 && values.size() != 5) + std::vector tokens; + std::stringstream ss(row); + std::string token; + while (std::getline(ss, token, ',')) + tokens.emplace_back(trim_copy(token)); + + if (tokens.size() < 4 || tokens.size() > 6) return false; + int values[5] = { 0, 0, 1, 1, 50 }; + if (tokens.size() == 4) { + // Legacy: a,b,enabled,mix + if (!parse_int_token(tokens[0], values[0]) || + !parse_int_token(tokens[1], values[1]) || + !parse_int_token(tokens[2], values[2]) || + !parse_int_token(tokens[3], values[4])) + return false; + } else { + // Current: a,b,enabled,custom,mix[,pattern] + for (size_t i = 0; i < 5; ++i) + if (!parse_int_token(tokens[i], values[i])) + return false; + } + if (values[0] <= 0 || values[1] <= 0) return false; a = unsigned(values[0]); b = unsigned(values[1]); enabled = (values[2] != 0); - custom = (values.size() == 5) ? (values[3] != 0) : true; - mix_b_percent = clamp_int(values.size() == 5 ? values[4] : values[3], 0, 100); + custom = (tokens.size() == 4) ? true : (values[3] != 0); + mix_b_percent = clamp_int(values[4], 0, 100); + manual_pattern = (tokens.size() == 6) ? tokens[5] : std::string(); return true; } +static bool is_pattern_separator(char c) +{ + return std::isspace(static_cast(c)) || c == '/' || c == '-' || c == '_' || c == '|' || c == ':' || c == ';'; +} + +static bool decode_pattern_step(char c, char &out) +{ + switch (std::tolower(static_cast(c))) { + case '1': + case 'a': + out = '1'; + return true; + case '2': + case 'b': + out = '2'; + return true; + default: + return false; + } +} + +static int mix_percent_from_normalized_pattern(const std::string &pattern) +{ + if (pattern.empty()) + return 50; + const int count_b = int(std::count(pattern.begin(), pattern.end(), '2')); + return clamp_int(int(std::lround(100.0 * double(count_b) / double(pattern.size()))), 0, 100); +} + // --------------------------------------------------------------------------- // MixedFilamentManager // --------------------------------------------------------------------------- @@ -388,6 +454,7 @@ void MixedFilamentManager::add_custom_filament(unsigned int component_a, mf.mix_b_percent = clamp_int(mix_b_percent, 0, 100); mf.ratio_a = 1; mf.ratio_b = 1; + mf.manual_pattern.clear(); mf.enabled = true; mf.custom = true; m_mixed.push_back(std::move(mf)); @@ -399,6 +466,24 @@ void MixedFilamentManager::clear_custom_entries() m_mixed.erase(std::remove_if(m_mixed.begin(), m_mixed.end(), [](const MixedFilament &mf) { return mf.custom; }), m_mixed.end()); } +std::string MixedFilamentManager::normalize_manual_pattern(const std::string &pattern) +{ + std::string normalized; + normalized.reserve(pattern.size()); + for (char c : pattern) { + char step = '\0'; + if (decode_pattern_step(c, step)) { + normalized.push_back(step); + continue; + } + if (is_pattern_separator(c)) + continue; + // Unknown token => invalid pattern. + return std::string(); + } + return normalized; +} + void MixedFilamentManager::apply_gradient_settings(int gradient_mode, float lower_bound, float upper_bound, @@ -434,6 +519,9 @@ std::string MixedFilamentManager::serialize_custom_entries() const << (mf.enabled ? 1 : 0) << ',' << (mf.custom ? 1 : 0) << ',' << clamp_int(mf.mix_b_percent, 0, 100); + const std::string normalized_pattern = normalize_manual_pattern(mf.manual_pattern); + if (!normalized_pattern.empty()) + ss << ',' << normalized_pattern; } return ss.str(); } @@ -464,7 +552,8 @@ void MixedFilamentManager::load_custom_entries(const std::string &serialized, co bool enabled = true; bool custom = true; int mix = 50; - if (!parse_row_definition(row, a, b, enabled, custom, mix)) { + std::string manual_pattern; + if (!parse_row_definition(row, a, b, enabled, custom, mix, manual_pattern)) { ++skipped_rows; BOOST_LOG_TRIVIAL(warning) << "MixedFilamentManager::load_custom_entries invalid row format: " << row; continue; @@ -485,7 +574,8 @@ void MixedFilamentManager::load_custom_entries(const std::string &serialized, co }); if (it_auto != m_mixed.end()) { it_auto->enabled = enabled; - it_auto->mix_b_percent = mix; + it_auto->manual_pattern = normalize_manual_pattern(manual_pattern); + it_auto->mix_b_percent = it_auto->manual_pattern.empty() ? mix : mix_percent_from_normalized_pattern(it_auto->manual_pattern); ++updated_auto; continue; } @@ -497,6 +587,9 @@ void MixedFilamentManager::load_custom_entries(const std::string &serialized, co mf.mix_b_percent = mix; mf.ratio_a = 1; mf.ratio_b = 1; + mf.manual_pattern = normalize_manual_pattern(manual_pattern); + if (!mf.manual_pattern.empty()) + mf.mix_b_percent = mix_percent_from_normalized_pattern(mf.manual_pattern); mf.enabled = enabled; mf.custom = custom; m_mixed.push_back(std::move(mf)); @@ -528,6 +621,13 @@ unsigned int MixedFilamentManager::resolve(unsigned int filament_id, const MixedFilament &mf = m_mixed[idx]; + // Manual pattern takes precedence when provided. Pattern uses repeating + // steps: '1' => component_a, '2' => component_b. + if (!mf.manual_pattern.empty()) { + const int pos = safe_mod(layer_index, int(mf.manual_pattern.size())); + return mf.manual_pattern[size_t(pos)] == '2' ? mf.component_b : mf.component_a; + } + // Height-weighted cadence can be forced by the local-Z planner. The // regular gradient height mode keeps historical behavior (custom rows). const bool use_height_weighted = force_height_weighted || (m_gradient_mode == 1 && mf.custom); diff --git a/src/libslic3r/MixedFilament.hpp b/src/libslic3r/MixedFilament.hpp index 8ac54a6f51..287ff9249c 100644 --- a/src/libslic3r/MixedFilament.hpp +++ b/src/libslic3r/MixedFilament.hpp @@ -26,6 +26,11 @@ struct MixedFilament // Blend percentage of component B in [0..100]. int mix_b_percent = 50; + // Optional manual layer pattern for this mixed filament, encoded as a + // string of '1' and '2'. '1' means component_a, '2' means component_b. + // Example: "11112222" => AAAABBBB repeating. + std::string manual_pattern; + // Whether this mixed filament is enabled (available for assignment). bool enabled = true; @@ -42,6 +47,7 @@ struct MixedFilament ratio_a == rhs.ratio_a && ratio_b == rhs.ratio_b && mix_b_percent == rhs.mix_b_percent && + manual_pattern == rhs.manual_pattern && enabled == rhs.enabled && custom == rhs.custom; } @@ -93,6 +99,10 @@ public: std::string serialize_custom_entries() const; void load_custom_entries(const std::string &serialized, const std::vector &filament_colours); + // Normalize a manual mixed-pattern string into compact '1'/'2' form. + // Accepts separators and A/B aliases. Returns empty string if invalid. + static std::string normalize_manual_pattern(const std::string &pattern); + // ---- Queries -------------------------------------------------------- // True when `filament_id` (1-based) refers to a mixed filament. diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index fd7b09822e..89fd785b8b 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -2489,21 +2489,28 @@ void Sidebar::update_mixed_filament_panel() p->m_sizer_mixed_filaments->Clear(true); - // Header with title and add button. + // Header with title and add buttons (gradient / pattern). auto *header_row = new wxPanel(p->m_panel_mixed_filaments, wxID_ANY); header_row->SetBackgroundColour(wxColour(255, 255, 255)); auto *header_sizer = new wxBoxSizer(wxHORIZONTAL); auto *mixed_title = new wxStaticText(header_row, wxID_ANY, _L("Mixed Filaments")); mixed_title->SetFont(Label::Head_14); header_sizer->Add(mixed_title, 1, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(8)); - auto *add_btn = new wxButton(header_row, wxID_ANY, "+", wxDefaultPosition, wxSize(FromDIP(24), FromDIP(22))); - add_btn->SetToolTip(_L("Add custom mixed filament")); - add_btn->Enable(num_physical >= 2); - add_btn->Bind(wxEVT_BUTTON, [this, preset_bundle, physical_colors, get_mixed_mode, get_mixed_int, get_mixed_float, get_mixed_bool, set_mixed_string, notify_mixed_change](wxCommandEvent &) { + + auto add_custom_row = [this, preset_bundle, physical_colors, get_mixed_mode, get_mixed_int, get_mixed_float, get_mixed_bool, set_mixed_string, notify_mixed_change](bool pattern_row) { if (physical_colors.size() < 2) return; auto &mgr = preset_bundle->mixed_filaments; mgr.add_custom_filament(1, 2, 50, physical_colors); + if (!mgr.mixed_filaments().empty()) { + MixedFilament &row = mgr.mixed_filaments().back(); + if (pattern_row) { + row.manual_pattern = "12"; + row.mix_b_percent = 50; + } else { + row.manual_pattern.clear(); + } + } int mode = get_mixed_mode(false) ? 1 : 0; float lo = get_mixed_float("mixed_filament_height_lower_bound", 0.04f); @@ -2523,8 +2530,24 @@ void Sidebar::update_mixed_filament_panel() update_dynamic_filament_list(); update_mixed_filament_panel(); }); - }); - header_sizer->Add(add_btn, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(8)); + }; + + auto *grad_label = new wxStaticText(header_row, wxID_ANY, _L("Gradient")); + header_sizer->Add(grad_label, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(4)); + auto *add_gradient_btn = new wxButton(header_row, wxID_ANY, "+", wxDefaultPosition, wxSize(FromDIP(24), FromDIP(22))); + add_gradient_btn->SetToolTip(_L("Add custom gradient mixed filament")); + add_gradient_btn->Enable(num_physical >= 2); + add_gradient_btn->Bind(wxEVT_BUTTON, [add_custom_row](wxCommandEvent &) { add_custom_row(false); }); + header_sizer->Add(add_gradient_btn, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(8)); + + auto *pattern_label = new wxStaticText(header_row, wxID_ANY, _L("Pattern")); + header_sizer->Add(pattern_label, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(4)); + auto *add_pattern_btn = new wxButton(header_row, wxID_ANY, "+", wxDefaultPosition, wxSize(FromDIP(24), FromDIP(22))); + add_pattern_btn->SetToolTip(_L("Add custom pattern mixed filament")); + add_pattern_btn->Enable(num_physical >= 2); + add_pattern_btn->Bind(wxEVT_BUTTON, [add_custom_row](wxCommandEvent &) { add_custom_row(true); }); + header_sizer->Add(add_pattern_btn, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(8)); + header_row->SetSizer(header_sizer); p->m_sizer_mixed_filaments->AddSpacer(FromDIP(2)); p->m_sizer_mixed_filaments->Add(header_row, 0, wxEXPAND); @@ -2648,22 +2671,38 @@ void Sidebar::update_mixed_filament_panel() picker_row->Add(choice_b, 1, wxALIGN_CENTER_VERTICAL); content_sizer->Add(picker_row, 0, wxEXPAND | wxLEFT | wxTOP, FromDIP(8)); - wxColour color_a = parse_mixed_color(physical_colors[size_t(component_a - 1)]); - wxColour color_b = parse_mixed_color(physical_colors[size_t(component_b - 1)]); - auto *blend_selector = new MixedGradientSelector(row, color_a, color_b, std::clamp(mf.mix_b_percent, 0, 100)); - auto *blend_row = new wxBoxSizer(wxHORIZONTAL); - blend_row->Add(blend_selector, 1, wxEXPAND); - content_sizer->Add(blend_row, 0, wxEXPAND | wxLEFT | wxTOP, FromDIP(8)); + const std::string normalized_pattern = MixedFilamentManager::normalize_manual_pattern(mf.manual_pattern); + const bool pattern_row_mode = !normalized_pattern.empty(); + MixedGradientSelector *blend_selector = nullptr; + wxStaticText *blend_label = nullptr; + wxTextCtrl *pattern_ctrl = nullptr; - auto *blend_label = new wxStaticText(row, wxID_ANY, wxString::Format("%d%%/%d%%", - 100 - std::clamp(mf.mix_b_percent, 0, 100), - std::clamp(mf.mix_b_percent, 0, 100))); - auto *ratio_row = new wxBoxSizer(wxHORIZONTAL); - ratio_row->AddStretchSpacer(1); - ratio_row->Add(blend_label, 0, wxALIGN_CENTER_VERTICAL); - content_sizer->Add(ratio_row, 0, wxEXPAND | wxLEFT | wxTOP, FromDIP(8)); + if (pattern_row_mode) { + auto *pattern_row = new wxBoxSizer(wxHORIZONTAL); + pattern_row->Add(new wxStaticText(row, wxID_ANY, _L("Pattern")), 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(6)); + pattern_ctrl = new wxTextCtrl(row, wxID_ANY, from_u8(normalized_pattern), wxDefaultPosition, + wxSize(FromDIP(170), -1), wxTE_PROCESS_ENTER); + pattern_ctrl->SetToolTip(_L("Manual repeating pattern. Use 1/2 or A/B, example: 1/1/1/1/2/2/2/2.")); + pattern_row->Add(pattern_ctrl, 1, wxALIGN_CENTER_VERTICAL); + content_sizer->Add(pattern_row, 0, wxEXPAND | wxLEFT | wxTOP, FromDIP(8)); + } else { + wxColour color_a = parse_mixed_color(physical_colors[size_t(component_a - 1)]); + wxColour color_b = parse_mixed_color(physical_colors[size_t(component_b - 1)]); + blend_selector = new MixedGradientSelector(row, color_a, color_b, std::clamp(mf.mix_b_percent, 0, 100)); + auto *blend_row = new wxBoxSizer(wxHORIZONTAL); + blend_row->Add(blend_selector, 1, wxEXPAND); + content_sizer->Add(blend_row, 0, wxEXPAND | wxLEFT | wxTOP, FromDIP(8)); - auto apply_custom_row = [this, preset_bundle, mixed_id, choice_a, choice_b, blend_selector, blend_label, swatch, num_physical, get_mixed_mode, get_mixed_int, get_mixed_float, get_mixed_bool, set_mixed_string, notify_mixed_change](bool refresh_panel) { + blend_label = new wxStaticText(row, wxID_ANY, wxString::Format("%d%%/%d%%", + 100 - std::clamp(mf.mix_b_percent, 0, 100), + std::clamp(mf.mix_b_percent, 0, 100))); + auto *ratio_row = new wxBoxSizer(wxHORIZONTAL); + ratio_row->AddStretchSpacer(1); + ratio_row->Add(blend_label, 0, wxALIGN_CENTER_VERTICAL); + content_sizer->Add(ratio_row, 0, wxEXPAND | wxLEFT | wxTOP, FromDIP(8)); + } + + auto apply_custom_row = [this, preset_bundle, mixed_id, choice_a, choice_b, blend_selector, blend_label, pattern_ctrl, swatch, num_physical, pattern_row_mode, get_mixed_mode, get_mixed_int, get_mixed_float, get_mixed_bool, set_mixed_string, notify_mixed_change](bool refresh_panel) { if (num_physical < 1) return; @@ -2682,7 +2721,19 @@ void Sidebar::update_mixed_filament_panel() MixedFilament &cur = mfs[mixed_id]; cur.component_a = unsigned(a); cur.component_b = unsigned(b); - cur.mix_b_percent = std::clamp(blend_selector->value(), 0, 100); + if (pattern_row_mode) { + std::string normalized = MixedFilamentManager::normalize_manual_pattern(into_u8(pattern_ctrl->GetValue())); + if (normalized.empty()) + normalized = "12"; + if (into_u8(pattern_ctrl->GetValue()) != normalized) + pattern_ctrl->ChangeValue(from_u8(normalized)); + cur.manual_pattern = normalized; + const int count_b = int(std::count(normalized.begin(), normalized.end(), '2')); + cur.mix_b_percent = std::clamp((100 * count_b + int(normalized.size()) / 2) / std::max(1, int(normalized.size())), 0, 100); + } else { + cur.mix_b_percent = std::clamp(blend_selector ? blend_selector->value() : 50, 0, 100); + cur.manual_pattern.clear(); + } cur.custom = true; ConfigOptionStrings *co = preset_bundle->project_config.option("filament_colour"); @@ -2691,10 +2742,12 @@ void Sidebar::update_mixed_filament_panel() wxColour color_a_wx = parse_mixed_color(colors[size_t(a - 1)]); wxColour color_b_wx = parse_mixed_color(colors[size_t(b - 1)]); - blend_selector->set_colors(color_a_wx, color_b_wx); + if (blend_selector) + blend_selector->set_colors(color_a_wx, color_b_wx); cur.display_color = MixedFilamentManager::blend_color(colors[size_t(a - 1)], colors[size_t(b - 1)], 100 - cur.mix_b_percent, cur.mix_b_percent); - blend_label->SetLabel(wxString::Format("%d%%/%d%%", 100 - cur.mix_b_percent, cur.mix_b_percent)); + if (blend_label) + blend_label->SetLabel(wxString::Format("%d%%/%d%%", 100 - cur.mix_b_percent, cur.mix_b_percent)); swatch->SetBackgroundColour(wxColour(cur.display_color)); swatch->Refresh(); @@ -2724,7 +2777,12 @@ void Sidebar::update_mixed_filament_panel() choice_a->Bind(wxEVT_CHOICE, [apply_custom_row](wxCommandEvent &) { apply_custom_row(true); }); choice_b->Bind(wxEVT_CHOICE, [apply_custom_row](wxCommandEvent &) { apply_custom_row(true); }); - blend_selector->Bind(wxEVT_SLIDER, [apply_custom_row](wxCommandEvent &) { apply_custom_row(false); }); + if (blend_selector) + blend_selector->Bind(wxEVT_SLIDER, [apply_custom_row](wxCommandEvent &) { apply_custom_row(false); }); + if (pattern_ctrl) { + pattern_ctrl->Bind(wxEVT_TEXT_ENTER, [apply_custom_row](wxCommandEvent &) { apply_custom_row(true); }); + pattern_ctrl->Bind(wxEVT_KILL_FOCUS, [apply_custom_row](wxFocusEvent &evt) { apply_custom_row(true); evt.Skip(); }); + } } row_sizer->Add(content_sizer, 1, wxEXPAND);