diff --git a/src/libslic3r/CustomGCode.cpp b/src/libslic3r/CustomGCode.cpp index acd7089c05..5e847406e1 100644 --- a/src/libslic3r/CustomGCode.cpp +++ b/src/libslic3r/CustomGCode.cpp @@ -57,16 +57,17 @@ extern void check_mode_for_custom_gcode_per_print_z(Info& info) info.mode = is_single_extruder ? SingleExtruder : MultiExtruder; } -// Return pairs of sorted by increasing print_z from custom_gcode_per_print_z. -// print_z corresponds to the first layer printed with the new extruder. -std::vector> custom_tool_changes(const Info& custom_gcode_per_print_z, size_t num_extruders) +// Return pairs of sorted by increasing print_z +// from custom_gcode_per_print_z. +std::vector> custom_tool_changes(const Info& custom_gcode_per_print_z, size_t num_filaments) { std::vector> custom_tool_changes; for (const Item& custom_gcode : custom_gcode_per_print_z.gcodes) if (custom_gcode.type == ToolChange) { - // If extruder count in PrinterSettings was changed, use default (0) extruder for extruders, more than num_extruders + // If available filament slots changed, fall back to filament 1 for + // stale IDs beyond the current physical+mixed range. assert(custom_gcode.extruder >= 0); - custom_tool_changes.emplace_back(custom_gcode.print_z, static_cast(size_t(custom_gcode.extruder) > num_extruders ? 1 : custom_gcode.extruder)); + custom_tool_changes.emplace_back(custom_gcode.print_z, static_cast(size_t(custom_gcode.extruder) > num_filaments ? 1 : custom_gcode.extruder)); } return custom_tool_changes; } diff --git a/src/libslic3r/CustomGCode.hpp b/src/libslic3r/CustomGCode.hpp index 7d8c7d9982..e1eafc0528 100644 --- a/src/libslic3r/CustomGCode.hpp +++ b/src/libslic3r/CustomGCode.hpp @@ -120,9 +120,10 @@ struct Info // So, we should set CustomGCode::Info.mode should be updated considering code values from items. extern void check_mode_for_custom_gcode_per_print_z(Info& info); -// Return pairs of sorted by increasing print_z from custom_gcode_per_print_z. -// print_z corresponds to the first layer printed with the new extruder. -std::vector> custom_tool_changes(const Info& custom_gcode_per_print_z, size_t num_extruders); +// Return pairs of sorted by increasing print_z +// from custom_gcode_per_print_z. The filament count may include mixed virtual +// filaments in addition to physical ones. +std::vector> custom_tool_changes(const Info& custom_gcode_per_print_z, size_t num_filaments); } // namespace CustomGCode diff --git a/src/libslic3r/GCode/ToolOrdering.cpp b/src/libslic3r/GCode/ToolOrdering.cpp index c4b52f9dd1..8bbcc51447 100644 --- a/src/libslic3r/GCode/ToolOrdering.cpp +++ b/src/libslic3r/GCode/ToolOrdering.cpp @@ -366,12 +366,13 @@ ToolOrdering::ToolOrdering(const Print &print, unsigned int first_extruder, bool std::vector> per_layer_extruder_switches; // BBS - if (auto num_filaments = unsigned(print.config().filament_diameter.size()); - num_filaments > 1 && print.object_extruders().size() == 1 && // the current Print's configuration is CustomGCode::MultiAsSingle + if (auto num_physical = unsigned(print.config().filament_diameter.size()); + num_physical > 1 && print.object_extruders().size() == 1 && // the current Print's configuration is CustomGCode::MultiAsSingle //BBS: replace model custom gcode with current plate custom gcode print.model().get_curr_plate_custom_gcodes().mode == CustomGCode::MultiAsSingle) { // Printing a single extruder platter on a printer with more than 1 extruder (or single-extruder multi-material). // There may be custom per-layer tool changes available at the model. + const size_t num_filaments = (m_mixed_mgr == nullptr) ? num_physical : m_mixed_mgr->total_filaments(num_physical); per_layer_extruder_switches = custom_tool_changes(print.model().get_curr_plate_custom_gcodes(), num_filaments); } diff --git a/src/libslic3r/MixedFilament.cpp b/src/libslic3r/MixedFilament.cpp index df3e2398bc..4931199ab4 100644 --- a/src/libslic3r/MixedFilament.cpp +++ b/src/libslic3r/MixedFilament.cpp @@ -315,6 +315,7 @@ static bool use_component_b_advanced_dither(int layer_index, int ratio_a, int ra static bool parse_row_definition(const std::string &row, unsigned int &a, unsigned int &b, + uint64_t &stable_id, bool &enabled, bool &custom, bool &origin_auto, @@ -352,13 +353,29 @@ static bool parse_row_definition(const std::string &row, } }; + auto parse_uint64_token = [&trim_copy](const std::string &tok, uint64_t &out) { + const std::string t = trim_copy(tok); + if (t.empty()) + return false; + try { + size_t consumed = 0; + const unsigned long long v = std::stoull(t, &consumed); + if (consumed != t.size()) + return false; + out = uint64_t(v); + return true; + } catch (...) { + return false; + } + }; + 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() > 12) + if (tokens.size() < 4 || tokens.size() > 13) return false; int values[5] = { 0, 0, 1, 1, 50 }; @@ -381,6 +398,7 @@ static bool parse_row_definition(const std::string &row, a = unsigned(values[0]); b = unsigned(values[1]); + stable_id = 0; enabled = (values[2] != 0); custom = (tokens.size() == 4) ? true : (values[3] != 0); origin_auto = !custom; @@ -440,6 +458,12 @@ static bool parse_row_definition(const std::string &row, origin_auto = parsed_origin_auto != 0; continue; } + if (tok[0] == 'u' || tok[0] == 'U') { + uint64_t parsed_stable_id = stable_id; + if (parse_uint64_token(tok.substr(1), parsed_stable_id)) + stable_id = parsed_stable_id; + continue; + } manual_pattern = tok; } @@ -685,6 +709,22 @@ static std::vector build_weighted_gradient_sequence(const std::vec // MixedFilamentManager // --------------------------------------------------------------------------- +uint64_t MixedFilamentManager::allocate_stable_id() +{ + const uint64_t stable_id = std::max(1, m_next_stable_id); + m_next_stable_id = stable_id + 1; + return stable_id; +} + +uint64_t MixedFilamentManager::normalize_stable_id(uint64_t stable_id) +{ + if (stable_id == 0) + return allocate_stable_id(); + if (stable_id >= m_next_stable_id) + m_next_stable_id = stable_id + 1; + return stable_id; +} + void MixedFilamentManager::auto_generate(const std::vector &filament_colours) { // Keep a copy of the old list so we can preserve user-modified ratios and @@ -703,7 +743,9 @@ void MixedFilamentManager::auto_generate(const std::vector &filamen continue; if (prev.component_a == 0 || prev.component_b == 0 || prev.component_a > n || prev.component_b > n || prev.component_a == prev.component_b) continue; - custom_rows.push_back(prev); + MixedFilament custom = prev; + custom.stable_id = normalize_stable_id(custom.stable_id); + custom_rows.push_back(std::move(custom)); } // Generate all C(N,2) pairwise combinations. @@ -727,11 +769,13 @@ void MixedFilamentManager::auto_generate(const std::vector &filamen prev.component_b == mf.component_b) { mf.enabled = prev.enabled; mf.deleted = prev.deleted; + mf.stable_id = prev.stable_id; if (mf.deleted) mf.enabled = false; break; } } + mf.stable_id = normalize_stable_id(mf.stable_id); m_mixed.push_back(mf); } } @@ -781,6 +825,7 @@ void MixedFilamentManager::add_custom_filament(unsigned int component_a, MixedFilament mf; mf.component_a = component_a; mf.component_b = component_b; + mf.stable_id = allocate_stable_id(); mf.mix_b_percent = clamp_int(mix_b_percent, 0, 100); mf.ratio_a = 1; mf.ratio_b = 1; @@ -840,14 +885,15 @@ void MixedFilamentManager::apply_gradient_settings(int gradient_mode, } } -std::string MixedFilamentManager::serialize_custom_entries() const +std::string MixedFilamentManager::serialize_custom_entries() { std::ostringstream ss; bool first = true; - for (const MixedFilament &mf : m_mixed) { + for (MixedFilament &mf : m_mixed) { if (!first) ss << ';'; first = false; + mf.stable_id = normalize_stable_id(mf.stable_id); const std::string normalized_ids = normalize_gradient_component_ids(mf.gradient_component_ids); const std::string normalized_weights = normalize_gradient_component_weights(mf.gradient_component_weights, normalized_ids.size()); ss << mf.component_a << ',' @@ -860,7 +906,8 @@ std::string MixedFilamentManager::serialize_custom_entries() const << 'w' << normalized_weights << ',' << 'm' << clamp_int(mf.distribution_mode, int(MixedFilament::LayerCycle), int(MixedFilament::Simple)) << ',' << 'd' << (mf.deleted ? 1 : 0) << ',' - << 'o' << (mf.origin_auto ? 1 : 0); + << 'o' << (mf.origin_auto ? 1 : 0) << ',' + << 'u' << mf.stable_id; const std::string normalized_pattern = normalize_manual_pattern(mf.manual_pattern); if (!normalized_pattern.empty()) ss << ',' << normalized_pattern; @@ -898,6 +945,15 @@ void MixedFilamentManager::load_custom_entries(const std::string &serialized, co std::vector rebuilt; rebuilt.reserve(m_mixed.size() + 8); std::set> consumed_auto_pairs; + std::set used_stable_ids; + auto dedupe_stable_id = [this, &used_stable_ids](uint64_t stable_id) { + stable_id = normalize_stable_id(stable_id); + if (used_stable_ids.insert(stable_id).second) + return stable_id; + uint64_t replacement = allocate_stable_id(); + used_stable_ids.insert(replacement); + return replacement; + }; std::stringstream all(serialized); std::string row; @@ -907,6 +963,7 @@ void MixedFilamentManager::load_custom_entries(const std::string &serialized, co ++parsed_rows; unsigned int a = 0; unsigned int b = 0; + uint64_t stable_id = 0; bool enabled = true; bool custom = true; bool origin_auto = false; @@ -917,7 +974,7 @@ void MixedFilamentManager::load_custom_entries(const std::string &serialized, co std::string manual_pattern; int distribution_mode = int(MixedFilament::Simple); bool deleted = false; - if (!parse_row_definition(row, a, b, enabled, custom, origin_auto, mix, pointillism_all_filaments, + if (!parse_row_definition(row, a, b, stable_id, enabled, custom, origin_auto, mix, pointillism_all_filaments, gradient_component_ids, gradient_component_weights, manual_pattern, distribution_mode, deleted)) { ++skipped_rows; BOOST_LOG_TRIVIAL(warning) << "MixedFilamentManager::load_custom_entries invalid row format: " << row; @@ -959,6 +1016,7 @@ void MixedFilamentManager::load_custom_entries(const std::string &serialized, co MixedFilament mf = *it_auto; mf.component_a = key.first; mf.component_b = key.second; + mf.stable_id = dedupe_stable_id(stable_id != 0 ? stable_id : mf.stable_id); mf.enabled = enabled; mf.pointillism_all_filaments = pointillism_all_filaments; mf.gradient_component_ids = normalize_gradient_component_ids(gradient_component_ids); @@ -982,6 +1040,7 @@ void MixedFilamentManager::load_custom_entries(const std::string &serialized, co MixedFilament mf; mf.component_a = a; mf.component_b = b; + mf.stable_id = dedupe_stable_id(stable_id); mf.mix_b_percent = mix; mf.ratio_a = 1; mf.ratio_b = 1; @@ -1012,6 +1071,7 @@ void MixedFilamentManager::load_custom_entries(const std::string &serialized, co MixedFilament mf = auto_mf; mf.component_a = key.first; mf.component_b = key.second; + mf.stable_id = dedupe_stable_id(mf.stable_id); mf.custom = false; mf.origin_auto = true; rebuilt.push_back(std::move(mf)); diff --git a/src/libslic3r/MixedFilament.hpp b/src/libslic3r/MixedFilament.hpp index 588843fabd..53a852b1ac 100644 --- a/src/libslic3r/MixedFilament.hpp +++ b/src/libslic3r/MixedFilament.hpp @@ -27,6 +27,10 @@ struct MixedFilament unsigned int component_a = 1; unsigned int component_b = 2; + // Persistent row identity used to keep painted virtual-tool assignments + // stable even when the visible mixed-filament list is rebuilt. + uint64_t stable_id = 0; + // Layer-alternation ratio. With ratio_a = 2, ratio_b = 1 the cycle is // A, A, B, A, A, B, ... int ratio_a = 1; @@ -77,6 +81,7 @@ struct MixedFilament { return component_a == rhs.component_a && component_b == rhs.component_b && + stable_id == rhs.stable_id && ratio_a == rhs.ratio_a && ratio_b == rhs.ratio_b && mix_b_percent == rhs.mix_b_percent && @@ -133,8 +138,9 @@ public: float upper_bound, bool advanced_dithering = false); - // Persist only custom rows. - std::string serialize_custom_entries() const; + // Persist mixed rows, including auto/deleted state, into the compact + // project-settings string. + std::string serialize_custom_entries(); void load_custom_entries(const std::string &serialized, const std::vector &filament_colours); // Normalize a manual mixed-pattern string into compact token form. @@ -196,12 +202,15 @@ private: } void refresh_display_colors(const std::vector &filament_colours); + uint64_t allocate_stable_id(); + uint64_t normalize_stable_id(uint64_t stable_id); std::vector m_mixed; int m_gradient_mode = 0; float m_height_lower_bound = 0.04f; float m_height_upper_bound = 0.16f; bool m_advanced_dithering = false; + uint64_t m_next_stable_id = 1; }; } // namespace Slic3r diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp index d97a7a7d67..78ce62a913 100644 --- a/src/libslic3r/PresetBundle.cpp +++ b/src/libslic3r/PresetBundle.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -3365,51 +3366,84 @@ void PresetBundle::update_multi_material_filament_presets(size_t to_delete_filam // Build old->new filament ID remap for painted facet data normalization. // This is needed for both deletion and addition of physical filaments so // painted mixed states keep pointing at the same virtual mixed entries. - if (old_num_filaments != num_filaments || deleting_filament) { - const unsigned int deleted_1based = deleting_filament ? unsigned(to_delete_filament_id + 1) : 0u; - size_t old_enabled_mixed = 0; - for (const auto &mf : old_mixed) - if (mf.enabled) - ++old_enabled_mixed; + if (old_num_filaments != num_filaments || deleting_filament) + build_filament_id_remap(old_mixed, old_num_filaments, num_filaments, deleting_filament, + deleting_filament ? unsigned(to_delete_filament_id + 1) : 0u); +} - const size_t old_total_filaments = old_num_filaments + old_enabled_mixed; - m_last_filament_id_remap.assign(old_total_filaments + 1, 0); +void PresetBundle::update_mixed_filament_id_remap(const std::vector &old_mixed, + size_t old_num_filaments, + size_t new_num_filaments) +{ + build_filament_id_remap(old_mixed, old_num_filaments, new_num_filaments, false, 0u); +} - for (unsigned int old_id = 1; old_id <= unsigned(old_num_filaments); ++old_id) { - unsigned int mapped = 0; - if (deleting_filament && old_id == deleted_1based) { - mapped = 0; - } else if (old_id <= unsigned(num_filaments)) { - mapped = old_id; - if (deleting_filament && old_id > deleted_1based) - --mapped; +void PresetBundle::build_filament_id_remap(const std::vector &old_mixed, + size_t old_num_filaments, + size_t new_num_filaments, + bool deleting_filament, + unsigned int deleted_1based) +{ + size_t old_enabled_mixed = 0; + for (const auto &mf : old_mixed) + if (mf.enabled) + ++old_enabled_mixed; + + const size_t old_total_filaments = old_num_filaments + old_enabled_mixed; + m_last_filament_id_remap.assign(old_total_filaments + 1, 0); + + for (unsigned int old_id = 1; old_id <= unsigned(old_num_filaments); ++old_id) { + unsigned int mapped = 0; + if (deleting_filament && old_id == deleted_1based) { + mapped = 0; + } else if (old_id <= unsigned(new_num_filaments)) { + mapped = old_id; + if (deleting_filament && old_id > deleted_1based) + --mapped; + } + m_last_filament_id_remap[old_id] = mapped; + } + + auto canonical_pair = [](unsigned int a, unsigned int b) { + return std::make_pair(std::min(a, b), std::max(a, b)); + }; + + std::unordered_map new_stable_id_to_virtual_id; + std::map, std::vector> new_pair_to_ids; + unsigned int next_virtual_id = unsigned(new_num_filaments + 1); + for (const auto &mf : this->mixed_filaments.mixed_filaments()) { + if (!mf.enabled) + continue; + if (mf.stable_id != 0) + new_stable_id_to_virtual_id.emplace(mf.stable_id, next_virtual_id); + new_pair_to_ids[canonical_pair(mf.component_a, mf.component_b)].push_back(next_virtual_id++); + } + + std::map, size_t> used_per_pair; + size_t stable_id_hits = 0; + size_t fallback_pair_hits = 0; + size_t missing_hits = 0; + unsigned int old_virtual_id = unsigned(old_num_filaments + 1); + for (const auto &mf : old_mixed) { + if (!mf.enabled) + continue; + + unsigned int a = mf.component_a; + unsigned int b = mf.component_b; + if (a == deleted_1based || b == deleted_1based) { + m_last_filament_id_remap[old_virtual_id] = 0; + ++missing_hits; + } else { + bool mapped_by_stable_id = false; + if (mf.stable_id != 0) { + auto it_stable = new_stable_id_to_virtual_id.find(mf.stable_id); + if (it_stable != new_stable_id_to_virtual_id.end()) { + m_last_filament_id_remap[old_virtual_id] = it_stable->second; + mapped_by_stable_id = true; + ++stable_id_hits; + } } - m_last_filament_id_remap[old_id] = mapped; - } - - auto canonical_pair = [](unsigned int a, unsigned int b) { - return std::make_pair(std::min(a, b), std::max(a, b)); - }; - - std::map, std::vector> new_pair_to_ids; - unsigned int next_virtual_id = unsigned(num_filaments + 1); - for (const auto &mf : this->mixed_filaments.mixed_filaments()) { - if (!mf.enabled) - continue; - new_pair_to_ids[canonical_pair(mf.component_a, mf.component_b)].push_back(next_virtual_id++); - } - - std::map, size_t> used_per_pair; - unsigned int old_virtual_id = unsigned(old_num_filaments + 1); - for (const auto &mf : old_mixed) { - if (!mf.enabled) - continue; - - unsigned int a = mf.component_a; - unsigned int b = mf.component_b; - if (a == deleted_1based || b == deleted_1based) { - m_last_filament_id_remap[old_virtual_id] = 0; - } else { + if (!mapped_by_stable_id) { if (deleting_filament) { if (a > deleted_1based) --a; @@ -3420,42 +3454,48 @@ void PresetBundle::update_multi_material_filament_presets(size_t to_delete_filam auto it = new_pair_to_ids.find(key); if (it == new_pair_to_ids.end()) { m_last_filament_id_remap[old_virtual_id] = 0; + ++missing_hits; } else { size_t &used = used_per_pair[key]; if (used >= it->second.size()) { m_last_filament_id_remap[old_virtual_id] = 0; + ++missing_hits; } else { m_last_filament_id_remap[old_virtual_id] = it->second[used++]; + ++fallback_pair_hits; } } } - ++old_virtual_id; } - - auto summarize_uint_vector = [](const std::vector &values, size_t max_items = 24) { - std::string out = "["; - const size_t n = std::min(values.size(), max_items); - for (size_t i = 0; i < n; ++i) { - if (i > 0) - out += ","; - out += std::to_string(values[i]); - } - if (values.size() > n) - out += ",..."; - out += "]"; - return out; - }; - - BOOST_LOG_TRIVIAL(warning) << "MF_REMAP preset_bundle" - << " old_physical=" << old_num_filaments - << " new_physical=" << num_filaments - << " deleting=" << (deleting_filament ? 1 : 0) - << " deleted_id=" << deleted_1based - << " old_mixed_enabled=" << old_enabled_mixed - << " new_mixed_enabled=" << this->mixed_filaments.enabled_count() - << " remap_size=" << m_last_filament_id_remap.size() - << " remap=" << summarize_uint_vector(m_last_filament_id_remap); + ++old_virtual_id; } + + auto summarize_uint_vector = [](const std::vector &values, size_t max_items = 24) { + std::string out = "["; + const size_t n = std::min(values.size(), max_items); + for (size_t i = 0; i < n; ++i) { + if (i > 0) + out += ","; + out += std::to_string(values[i]); + } + if (values.size() > n) + out += ",..."; + out += "]"; + return out; + }; + + BOOST_LOG_TRIVIAL(warning) << "MF_REMAP preset_bundle" + << " old_physical=" << old_num_filaments + << " new_physical=" << new_num_filaments + << " deleting=" << (deleting_filament ? 1 : 0) + << " deleted_id=" << deleted_1based + << " old_mixed_enabled=" << old_enabled_mixed + << " new_mixed_enabled=" << this->mixed_filaments.enabled_count() + << " stable_id_hits=" << stable_id_hits + << " fallback_pair_hits=" << fallback_pair_hits + << " missing_hits=" << missing_hits + << " remap_size=" << m_last_filament_id_remap.size() + << " remap=" << summarize_uint_vector(m_last_filament_id_remap); } void PresetBundle::update_compatible(PresetSelectCompatibleType select_other_print_if_incompatible, PresetSelectCompatibleType select_other_filament_if_incompatible) diff --git a/src/libslic3r/PresetBundle.hpp b/src/libslic3r/PresetBundle.hpp index d721cf3672..c11552870c 100644 --- a/src/libslic3r/PresetBundle.hpp +++ b/src/libslic3r/PresetBundle.hpp @@ -254,6 +254,11 @@ public: // update size and content of filament_presets. void update_multi_material_filament_presets(size_t to_delete_filament_id = size_t(-1), size_t old_num_filaments = size_t(-1)); + // Rebuild old->new virtual filament mapping after mixed-row enable/delete + // changes when the physical filament count itself did not change. + void update_mixed_filament_id_remap(const std::vector &old_mixed, + size_t old_num_filaments, + size_t new_num_filaments); // Mapping generated during the latest filament count change. // Index is old 1-based filament ID, value is new 1-based filament ID (0 = removed). const std::vector& last_filament_id_remap() const { return m_last_filament_id_remap; } @@ -312,6 +317,11 @@ private: std::pair load_system_presets_from_json(ForwardCompatibilitySubstitutionRule compatibility_rule); // Merge one vendor's presets with the other vendor's presets, report duplicates. std::vector merge_presets(PresetBundle &&other); + void build_filament_id_remap(const std::vector &old_mixed, + size_t old_num_filaments, + size_t new_num_filaments, + bool deleting_filament, + unsigned int deleted_1based); // Update renamed_from and alias maps of system profiles. void update_system_maps(); diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 528fdef999..de75c7756a 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -484,10 +484,11 @@ std::vector Print::extruders(bool conside_custom_gcode) const if (conside_custom_gcode) { //BBS - int num_extruders = m_config.filament_colour.size(); + const size_t num_physical = m_config.filament_colour.size(); + const size_t num_filaments = m_mixed_filament_mgr.total_filaments(num_physical); if (m_model.plates_custom_gcodes.find(m_model.curr_plate_index) != m_model.plates_custom_gcodes.end()) { for (auto item : m_model.plates_custom_gcodes.at(m_model.curr_plate_index).gcodes) { - if (item.type == CustomGCode::Type::ToolChange && item.extruder <= num_extruders) + if (item.type == CustomGCode::Type::ToolChange && item.extruder <= int(num_filaments)) extruders.push_back((unsigned int)(item.extruder - 1)); } } diff --git a/src/slic3r/GUI/PartPlate.cpp b/src/slic3r/GUI/PartPlate.cpp index cc772c8b1c..d4e3d5aad4 100644 --- a/src/slic3r/GUI/PartPlate.cpp +++ b/src/slic3r/GUI/PartPlate.cpp @@ -1439,9 +1439,10 @@ std::vector PartPlate::get_extruders(bool conside_custom_gcode) const int nums_extruders = 0; if (const ConfigOptionStrings *color_option = dynamic_cast(wxGetApp().preset_bundle->project_config.option("filament_colour"))) { nums_extruders = color_option->values.size(); + const size_t total_filaments = wxGetApp().preset_bundle->mixed_filaments.total_filaments(size_t(nums_extruders)); if (m_model->plates_custom_gcodes.find(m_plate_index) != m_model->plates_custom_gcodes.end()) { for (auto item : m_model->plates_custom_gcodes.at(m_plate_index).gcodes) { - if (item.type == CustomGCode::Type::ToolChange && item.extruder <= nums_extruders) + if (item.type == CustomGCode::Type::ToolChange && item.extruder <= int(total_filaments)) plate_extruders.push_back(item.extruder); } } @@ -1561,9 +1562,12 @@ std::vector PartPlate::get_extruders_under_cli(bool conside_custom_gcode, D int nums_extruders = 0; if (const ConfigOptionStrings *color_option = dynamic_cast(full_config.option("filament_colour"))) { nums_extruders = color_option->values.size(); + size_t total_filaments = size_t(nums_extruders); + if (wxGetApp().preset_bundle != nullptr) + total_filaments = wxGetApp().preset_bundle->mixed_filaments.total_filaments(total_filaments); if (m_model->plates_custom_gcodes.find(m_plate_index) != m_model->plates_custom_gcodes.end()) { for (auto item : m_model->plates_custom_gcodes.at(m_plate_index).gcodes) { - if (item.type == CustomGCode::Type::ToolChange && item.extruder <= nums_extruders) + if (item.type == CustomGCode::Type::ToolChange && item.extruder <= int(total_filaments)) plate_extruders.push_back(item.extruder); } } @@ -1606,9 +1610,10 @@ std::vector PartPlate::get_extruders_without_support(bool conside_custom_gc int nums_extruders = 0; if (const ConfigOptionStrings* color_option = dynamic_cast(wxGetApp().preset_bundle->project_config.option("filament_colour"))) { nums_extruders = color_option->values.size(); + const size_t total_filaments = wxGetApp().preset_bundle->mixed_filaments.total_filaments(size_t(nums_extruders)); if (m_model->plates_custom_gcodes.find(m_plate_index) != m_model->plates_custom_gcodes.end()) { for (auto item : m_model->plates_custom_gcodes.at(m_plate_index).gcodes) { - if (item.type == CustomGCode::Type::ToolChange && item.extruder <= nums_extruders) + if (item.type == CustomGCode::Type::ToolChange && item.extruder <= int(total_filaments)) plate_extruders.push_back(item.extruder); } } diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index c88341878f..fa3985ed09 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -4400,7 +4400,10 @@ void Sidebar::update_mixed_filament_panel() return wxString::Format("(F%u + F%u)", unsigned(entry.component_a), unsigned(entry.component_b)); }; - auto apply_mixed_entry_changes = [this, preset_bundle, print_cfg](size_t mixed_id, const MixedFilament &updated_mf, bool preserve_enabled = false) { + auto apply_mixed_entry_changes = [this, preset_bundle, print_cfg, num_physical](size_t mixed_id, + const MixedFilament &updated_mf, + bool preserve_enabled = false, + bool rebuild_virtual_id_remap = false) { if (!preset_bundle) return; @@ -4409,6 +4412,7 @@ void Sidebar::update_mixed_filament_panel() if (mixed_id >= mfs.size()) return; + const std::vector old_mixed = rebuild_virtual_id_remap ? mfs : std::vector(); MixedFilament merged = updated_mf; if (preserve_enabled) merged.enabled = mfs[mixed_id].enabled; @@ -4435,6 +4439,9 @@ void Sidebar::update_mixed_filament_panel() if (wxGetApp().plater()) wxGetApp().plater()->update_project_dirty_from_presets(); + if (rebuild_virtual_id_remap) + preset_bundle->update_mixed_filament_id_remap(old_mixed, num_physical, num_physical); + int mode = 0; if (const ConfigOptionBool *opt = preset_bundle->project_config.option("mixed_filament_gradient_mode")) mode = opt->value ? 1 : 0; @@ -4452,6 +4459,9 @@ void Sidebar::update_mixed_filament_panel() hi = std::max(lo, hi); mgr.apply_gradient_settings(mode, lo, hi, advanced); update_dynamic_filament_list(); + + if (rebuild_virtual_id_remap && wxGetApp().plater()) + wxGetApp().plater()->on_filaments_change(num_physical); }; size_t visible_mixed_idx = 0; @@ -4507,7 +4517,7 @@ void Sidebar::update_mixed_filament_panel() return; MixedFilament updated = mfs[mixed_id]; updated.enabled = enabled_chk->GetValue(); - apply_mixed_entry_changes(mixed_id, updated, false); + apply_mixed_entry_changes(mixed_id, updated, false, true); }); auto *del_btn = new ScalableButton(header_panel, wxID_ANY, "cross"); @@ -4519,6 +4529,7 @@ void Sidebar::update_mixed_filament_panel() auto &mgr = wxGetApp().preset_bundle->mixed_filaments; auto &mfs = mgr.mixed_filaments(); if (mixed_id < mfs.size()) { + const std::vector old_mixed = mfs; auto canonical_pair = [](unsigned int a, unsigned int b) { return std::make_pair(std::min(a, b), std::max(a, b)); }; @@ -4571,10 +4582,12 @@ void Sidebar::update_mixed_filament_panel() } p->m_expanded_mixed_filament_rows.clear(); set_mixed_string("mixed_filament_definitions", mgr.serialize_custom_entries()); + wxGetApp().preset_bundle->update_mixed_filament_id_remap(old_mixed, num_physical, num_physical); notify_mixed_change(); if (wxGetApp().plater()) wxGetApp().plater()->update_project_dirty_from_presets(); - update_mixed_filament_panel(); + if (wxGetApp().plater()) + wxGetApp().plater()->on_filaments_change(num_physical); } } }); @@ -16894,30 +16907,6 @@ void Plater::on_filaments_change(size_t num_filaments) } } - // Add-flow is deterministic: old virtual filament IDs must shift by - // +delta_physical because physical IDs were prepended. - // Keep this rule authoritative to avoid stale/partial remaps. - if (num_filaments > old_num_filaments) { - const size_t delta = num_filaments - old_num_filaments; - const size_t old_first_virtual = old_num_filaments + 1; - size_t old_total = 0; - if (!id_remap.empty() && id_remap.size() > 1) - old_total = std::min(id_remap.size() - 1, state_map.size() - 1); - else - old_total = std::min((total_filaments >= delta ? total_filaments - delta : 0), state_map.size() - 1); - - if (old_total >= old_first_virtual) { - should_remap_states = true; - for (size_t i = old_first_virtual; i <= old_total; ++i) { - const size_t mapped = i + delta; - if (mapped >= state_map.size() || mapped > total_filaments) - state_map[i] = EnforcerBlockerType::NONE; - else - state_map[i] = EnforcerBlockerType(mapped); - } - } - } - size_t changed_entries = 0; std::string changed_map_preview = "["; for (size_t i = 1; i < state_map.size(); ++i) { diff --git a/tests/libslic3r/CMakeLists.txt b/tests/libslic3r/CMakeLists.txt index 62bdba4dcb..07320fab41 100644 --- a/tests/libslic3r/CMakeLists.txt +++ b/tests/libslic3r/CMakeLists.txt @@ -7,8 +7,10 @@ add_executable(${_TEST_NAME}_tests test_clipper_offset.cpp test_clipper_utils.cpp test_config.cpp + test_custom_gcode.cpp test_elephant_foot_compensation.cpp test_geometry.cpp + test_mixed_filament.cpp test_placeholder_parser.cpp test_polygon.cpp test_mutable_polygon.cpp diff --git a/tests/libslic3r/test_custom_gcode.cpp b/tests/libslic3r/test_custom_gcode.cpp new file mode 100644 index 0000000000..932ce26520 --- /dev/null +++ b/tests/libslic3r/test_custom_gcode.cpp @@ -0,0 +1,28 @@ +#include + +#include "libslic3r/CustomGCode.hpp" + +using namespace Slic3r; + +TEST_CASE("Custom layer tool changes keep mixed virtual filament ids", "[CustomGCode]") +{ + CustomGCode::Info info; + info.gcodes.emplace_back(CustomGCode::Item{1.25, CustomGCode::ToolChange, 5, "", ""}); + + const auto tool_changes = CustomGCode::custom_tool_changes(info, 6); + + REQUIRE(tool_changes.size() == 1); + CHECK(tool_changes.front().first == Approx(1.25)); + CHECK(tool_changes.front().second == 5u); +} + +TEST_CASE("Custom layer tool changes still clamp stale filament ids", "[CustomGCode]") +{ + CustomGCode::Info info; + info.gcodes.emplace_back(CustomGCode::Item{2.0, CustomGCode::ToolChange, 7, "", ""}); + + const auto tool_changes = CustomGCode::custom_tool_changes(info, 6); + + REQUIRE(tool_changes.size() == 1); + CHECK(tool_changes.front().second == 1u); +} diff --git a/tests/libslic3r/test_mixed_filament.cpp b/tests/libslic3r/test_mixed_filament.cpp new file mode 100644 index 0000000000..d37b69b2fe --- /dev/null +++ b/tests/libslic3r/test_mixed_filament.cpp @@ -0,0 +1,125 @@ +#include + +#include "libslic3r/PresetBundle.hpp" + +#include +#include + +using namespace Slic3r; + +namespace { + +static std::vector split_rows(const std::string &serialized) +{ + std::vector rows; + std::stringstream ss(serialized); + std::string row; + while (std::getline(ss, row, ';')) { + if (!row.empty()) + rows.push_back(row); + } + return rows; +} + +static std::string join_rows(const std::vector &rows) +{ + std::ostringstream ss; + for (size_t i = 0; i < rows.size(); ++i) { + if (i != 0) + ss << ';'; + ss << rows[i]; + } + return ss.str(); +} + +static unsigned int virtual_id_for_stable_id(const std::vector &mixed, size_t num_physical, uint64_t stable_id) +{ + unsigned int next_virtual_id = unsigned(num_physical + 1); + for (const MixedFilament &mf : mixed) { + if (!mf.enabled || mf.deleted) + continue; + if (mf.stable_id == stable_id) + return next_virtual_id; + ++next_virtual_id; + } + return 0; +} + +} // namespace + +TEST_CASE("Mixed filament remap follows stable row ids when same-pair rows reorder", "[MixedFilament]") +{ + PresetBundle bundle; + bundle.filament_presets = {"Default Filament", "Default Filament"}; + bundle.project_config.option("filament_colour")->values = {"#FF0000", "#0000FF"}; + bundle.update_multi_material_filament_presets(); + + auto &mgr = bundle.mixed_filaments; + auto &mixed = mgr.mixed_filaments(); + REQUIRE(mixed.size() == 1); + + mixed[0].deleted = true; + mixed[0].enabled = false; + + const auto colors = bundle.project_config.option("filament_colour")->values; + mgr.add_custom_filament(1, 2, 25, colors); + mgr.add_custom_filament(1, 2, 75, colors); + + auto &old_mixed = mgr.mixed_filaments(); + REQUIRE(old_mixed.size() == 3); + REQUIRE(old_mixed[1].enabled); + REQUIRE(old_mixed[2].enabled); + const uint64_t first_custom_id = old_mixed[1].stable_id; + const uint64_t second_custom_id = old_mixed[2].stable_id; + + std::vector rows = split_rows(mgr.serialize_custom_entries()); + REQUIRE(rows.size() == 3); + std::swap(rows[1], rows[2]); + + auto *definitions = bundle.project_config.option("mixed_filament_definitions"); + REQUIRE(definitions != nullptr); + definitions->value = join_rows(rows); + + bundle.filament_presets.push_back(bundle.filament_presets.back()); + bundle.project_config.option("filament_colour")->values.push_back("#00FF00"); + bundle.update_multi_material_filament_presets(size_t(-1), 2); + + const std::vector remap = bundle.consume_last_filament_id_remap(); + REQUIRE(remap.size() >= 5); + + const auto &rebuilt = bundle.mixed_filaments.mixed_filaments(); + const unsigned int new_first_custom_virtual_id = virtual_id_for_stable_id(rebuilt, 3, first_custom_id); + const unsigned int new_second_custom_virtual_id = virtual_id_for_stable_id(rebuilt, 3, second_custom_id); + + REQUIRE(new_first_custom_virtual_id != 0); + REQUIRE(new_second_custom_virtual_id != 0); + CHECK(remap[3] == new_first_custom_virtual_id); + CHECK(remap[4] == new_second_custom_virtual_id); +} + +TEST_CASE("Mixed filament remap keeps later painted colors stable when an earlier mixed row is deleted", "[MixedFilament]") +{ + PresetBundle bundle; + bundle.filament_presets = {"Default Filament", "Default Filament", "Default Filament", "Default Filament"}; + bundle.project_config.option("filament_colour")->values = {"#FF0000", "#00FF00", "#0000FF", "#FFFF00"}; + bundle.update_multi_material_filament_presets(); + + auto &mixed = bundle.mixed_filaments.mixed_filaments(); + REQUIRE(mixed.size() >= 10); + + const uint64_t stable_id_6 = mixed[1].stable_id; + const uint64_t stable_id_7 = mixed[2].stable_id; + const uint64_t stable_id_8 = mixed[3].stable_id; + + const std::vector old_mixed = mixed; + mixed[0].enabled = false; + mixed[0].deleted = true; + + bundle.update_mixed_filament_id_remap(old_mixed, 4, 4); + const std::vector remap = bundle.consume_last_filament_id_remap(); + + REQUIRE(remap.size() >= 15); + CHECK(remap[6] == virtual_id_for_stable_id(mixed, 4, stable_id_6)); + CHECK(remap[7] == virtual_id_for_stable_id(mixed, 4, stable_id_7)); + CHECK(remap[8] == virtual_id_for_stable_id(mixed, 4, stable_id_8)); +}