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