diff --git a/RELEASE_NOTES_v0.92.md b/RELEASE_NOTES_v0.92.md new file mode 100644 index 0000000000..4cd762b2cc --- /dev/null +++ b/RELEASE_NOTES_v0.92.md @@ -0,0 +1,49 @@ +# Snapmaker Orca Full Spectrum v0.92 (Pre-release) + +Mixed-filament stability and workflow release. +Based on Snapmaker Orca v2.2.4. + +## Highlights + +### Gradient Behavior Is Now Gradual +- Removed the `Mixed filament layer cycle` setting. +- Reworked non-height-weighted gradient math to use gradual integer cadence by ratio (minority side anchors to 1 layer, majority side scales progressively). +- This prevents abrupt jumps in layer cadence and makes transitions more predictable. + +### Mixed Filament Indexing And Mapping Fixes +- Fixed mixed-filament row labeling so IDs stay compact after deletions in the Mixed Filaments panel. +- Fixed regeneration order so auto-generated/precomputed mixes are appended at the end of existing mixed rows instead of being inserted at the front. +- Fixed paint/index remapping when adding physical or virtual filaments so existing painted mixed regions keep their intended virtual filament mapping. +- Fixed edge case where the first mixed filament could be temporarily replaced by a newly added physical filament. + +### Automatic Mixed Filaments Are Editable +- Automatic mixed rows can now be expanded and edited through the gradient controls. +- Edited auto-origin rows are treated as user-managed entries. +- Deleting an edited auto-origin row now keeps that auto pair tombstoned (it will not be regenerated immediately and overwrite user intent). + +### Gradient Preview Color Fidelity +- Updated gradient preview blending to use the FilamentMixer library path for color reproduction. +- Preview swatches now better match expected mixed output. + +### Selective Expansion/Contraction Fix +- Updated selective mixed-zone contraction so positive values trim only the outside-facing (visible) boundary band. +- Internal/shared boundaries are no longer symmetrically contracted. +- Outward expansion behavior remains unchanged. + +## Also Included (v0.91 Carry-Forward Fixes) +- Windows startup/log path handling for non-ASCII usernames (UTF-8-safe path resolution). +- Mixed-filament swatch desync/gray-regression fixes in preview rendering. + +## Internal/Config Changes +- Removed `mixed_filament_cycle_layers` from config/UI wiring. +- Extended mixed filament serialized metadata with auto-origin tracking to preserve delete/regenerate intent across reloads. +- Added diagnostic logging around mixed-filament remap and selective-indentation application paths. + +## Important Notes +- Experimental build with limited testing. +- Some projects may show different gradient cadence behavior due to the removal of explicit layer-cycle control. +- Use at your own risk. + +## Credits +- FilamentMixer color blending integration is powered by FilamentMixer by [justinh-rahb](https://github.com/justinh-rahb). +- Library: [https://github.com/justinh-rahb/filament-mixer](https://github.com/justinh-rahb/filament-mixer) diff --git a/src/libslic3r/MixedFilament.cpp b/src/libslic3r/MixedFilament.cpp index 9edf39302f..df3e2398bc 100644 --- a/src/libslic3r/MixedFilament.cpp +++ b/src/libslic3r/MixedFilament.cpp @@ -10,6 +10,7 @@ #include #include #include +#include namespace Slic3r { @@ -231,7 +232,7 @@ static void normalize_ratio_pair(int &a, int &b) } } -static void compute_gradient_ratios(MixedFilament &mf, int gradient_mode, float lower_bound, float upper_bound, int cycle_layers) +static void compute_gradient_ratios(MixedFilament &mf, int gradient_mode, float lower_bound, float upper_bound) { if (gradient_mode == 1) { // Height-weighted mode: @@ -245,12 +246,25 @@ static void compute_gradient_ratios(MixedFilament &mf, int gradient_mode, float mf.ratio_b = std::max(1, safe_ratio_from_height(h_b, unit)); } else { // Layer-cycle mode: - // distribute an integer cycle directly by blend percentages. + // derive a gradual integer cadence directly from the blend ratio + // by fixing the minority side to one layer and scaling the majority. const int mix_b = clamp_int(mf.mix_b_percent, 0, 100); - const float pct_b = float(mix_b) / 100.f; - const int cycle = std::max(2, cycle_layers); - mf.ratio_b = clamp_int(int(std::lround(pct_b * cycle)), 0, cycle); - mf.ratio_a = cycle - mf.ratio_b; + if (mix_b <= 0) { + mf.ratio_a = 1; + mf.ratio_b = 0; + } else if (mix_b >= 100) { + mf.ratio_a = 0; + mf.ratio_b = 1; + } else { + const int pct_b = mix_b; + const int pct_a = 100 - pct_b; + const bool b_is_major = pct_b >= pct_a; + const int major_pct = b_is_major ? pct_b : pct_a; + const int minor_pct = b_is_major ? pct_a : pct_b; + const int major_layers = std::max(1, int(std::lround(double(major_pct) / double(std::max(1, minor_pct))))); + mf.ratio_a = b_is_major ? 1 : major_layers; + mf.ratio_b = b_is_major ? major_layers : 1; + } } normalize_ratio_pair(mf.ratio_a, mf.ratio_b); @@ -303,6 +317,7 @@ static bool parse_row_definition(const std::string &row, unsigned int &b, bool &enabled, bool &custom, + bool &origin_auto, int &mix_b_percent, bool &pointillism_all_filaments, std::string &gradient_component_ids, @@ -368,6 +383,7 @@ static bool parse_row_definition(const std::string &row, b = unsigned(values[1]); enabled = (values[2] != 0); custom = (tokens.size() == 4) ? true : (values[3] != 0); + origin_auto = !custom; mix_b_percent = clamp_int(values[4], 0, 100); pointillism_all_filaments = false; gradient_component_ids.clear(); @@ -418,6 +434,12 @@ static bool parse_row_definition(const std::string &row, deleted = parsed_deleted != 0; continue; } + if (tok[0] == 'o' || tok[0] == 'O') { + int parsed_origin_auto = origin_auto ? 1 : 0; + if (parse_int_token(tok.substr(1), parsed_origin_auto)) + origin_auto = parsed_origin_auto != 0; + continue; + } manual_pattern = tok; } @@ -696,6 +718,7 @@ void MixedFilamentManager::auto_generate(const std::vector &filamen mf.enabled = true; mf.deleted = false; mf.custom = false; + mf.origin_auto = true; // Try to preserve previous settings. for (const auto &prev : old) { @@ -769,6 +792,7 @@ void MixedFilamentManager::add_custom_filament(unsigned int component_a, mf.enabled = true; mf.deleted = false; mf.custom = true; + mf.origin_auto = false; m_mixed.push_back(std::move(mf)); refresh_display_colors(filament_colours); } @@ -799,13 +823,11 @@ std::string MixedFilamentManager::normalize_manual_pattern(const std::string &pa void MixedFilamentManager::apply_gradient_settings(int gradient_mode, float lower_bound, float upper_bound, - int cycle_layers, bool advanced_dithering) { m_gradient_mode = (gradient_mode != 0) ? 1 : 0; m_height_lower_bound = std::max(0.01f, lower_bound); m_height_upper_bound = std::max(m_height_lower_bound, upper_bound); - m_cycle_layers = std::max(2, cycle_layers); m_advanced_dithering = advanced_dithering; for (MixedFilament &mf : m_mixed) { @@ -814,7 +836,7 @@ void MixedFilamentManager::apply_gradient_settings(int gradient_mode, mf.ratio_b = 1; continue; } - compute_gradient_ratios(mf, m_gradient_mode, m_height_lower_bound, m_height_upper_bound, m_cycle_layers); + compute_gradient_ratios(mf, m_gradient_mode, m_height_lower_bound, m_height_upper_bound); } } @@ -837,7 +859,8 @@ std::string MixedFilamentManager::serialize_custom_entries() const << 'g' << normalized_ids << ',' << 'w' << normalized_weights << ',' << 'm' << clamp_int(mf.distribution_mode, int(MixedFilament::LayerCycle), int(MixedFilament::Simple)) << ',' - << 'd' << (mf.deleted ? 1 : 0); + << 'd' << (mf.deleted ? 1 : 0) << ',' + << 'o' << (mf.origin_auto ? 1 : 0); const std::string normalized_pattern = normalize_manual_pattern(mf.manual_pattern); if (!normalized_pattern.empty()) ss << ',' << normalized_pattern; @@ -858,8 +881,24 @@ void MixedFilamentManager::load_custom_entries(const std::string &serialized, co size_t parsed_rows = 0; size_t loaded_rows = 0; size_t updated_auto = 0; + size_t appended_auto = 0; size_t skipped_rows = 0; + auto canonical_pair = [](unsigned int a, unsigned int b) { + return std::make_pair(std::min(a, b), std::max(a, b)); + }; + + std::vector auto_rows; + auto_rows.reserve(m_mixed.size()); + for (const MixedFilament &mf : m_mixed) { + if (!mf.custom) + auto_rows.push_back(mf); + } + + std::vector rebuilt; + rebuilt.reserve(m_mixed.size() + 8); + std::set> consumed_auto_pairs; + std::stringstream all(serialized); std::string row; while (std::getline(all, row, ';')) { @@ -870,6 +909,7 @@ void MixedFilamentManager::load_custom_entries(const std::string &serialized, co unsigned int b = 0; bool enabled = true; bool custom = true; + bool origin_auto = false; int mix = 50; bool pointillism_all_filaments = false; std::string gradient_component_ids; @@ -877,7 +917,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, mix, pointillism_all_filaments, + if (!parse_row_definition(row, a, b, 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; @@ -894,24 +934,49 @@ void MixedFilamentManager::load_custom_entries(const std::string &serialized, co } if (!custom) { - auto it_auto = std::find_if(m_mixed.begin(), m_mixed.end(), [a, b](const MixedFilament &mf) { - return !mf.custom && mf.component_a == a && mf.component_b == b; - }); - if (it_auto != m_mixed.end()) { - it_auto->enabled = enabled; - it_auto->pointillism_all_filaments = pointillism_all_filaments; - it_auto->gradient_component_ids = normalize_gradient_component_ids(gradient_component_ids); - it_auto->gradient_component_weights = - normalize_gradient_component_weights(gradient_component_weights, it_auto->gradient_component_ids.size()); - it_auto->manual_pattern = normalize_manual_pattern(manual_pattern); - it_auto->distribution_mode = clamp_int(distribution_mode, int(MixedFilament::LayerCycle), int(MixedFilament::Simple)); - it_auto->mix_b_percent = it_auto->manual_pattern.empty() ? mix : mix_percent_from_normalized_pattern(it_auto->manual_pattern); - it_auto->deleted = deleted; - if (it_auto->deleted) - it_auto->enabled = false; - ++updated_auto; + const auto key = canonical_pair(a, b); + if (consumed_auto_pairs.count(key) != 0) { + ++skipped_rows; + BOOST_LOG_TRIVIAL(warning) << "MixedFilamentManager::load_custom_entries duplicate auto row" + << ", row=" << row + << ", a=" << key.first + << ", b=" << key.second; continue; } + + auto it_auto = std::find_if(auto_rows.begin(), auto_rows.end(), [key, canonical_pair](const MixedFilament &mf) { + return canonical_pair(mf.component_a, mf.component_b) == key; + }); + if (it_auto == auto_rows.end()) { + ++skipped_rows; + BOOST_LOG_TRIVIAL(warning) << "MixedFilamentManager::load_custom_entries auto row missing after regenerate" + << ", row=" << row + << ", a=" << key.first + << ", b=" << key.second; + continue; + } + + MixedFilament mf = *it_auto; + mf.component_a = key.first; + mf.component_b = key.second; + mf.enabled = enabled; + mf.pointillism_all_filaments = pointillism_all_filaments; + mf.gradient_component_ids = normalize_gradient_component_ids(gradient_component_ids); + mf.gradient_component_weights = + normalize_gradient_component_weights(gradient_component_weights, mf.gradient_component_ids.size()); + mf.manual_pattern = normalize_manual_pattern(manual_pattern); + mf.distribution_mode = clamp_int(distribution_mode, int(MixedFilament::LayerCycle), int(MixedFilament::Simple)); + mf.mix_b_percent = mf.manual_pattern.empty() ? mix : mix_percent_from_normalized_pattern(mf.manual_pattern); + mf.deleted = deleted; + if (mf.deleted) + mf.enabled = false; + mf.custom = false; + mf.origin_auto = true; + + rebuilt.push_back(std::move(mf)); + consumed_auto_pairs.insert(key); + ++updated_auto; + continue; } MixedFilament mf; @@ -933,15 +998,34 @@ void MixedFilamentManager::load_custom_entries(const std::string &serialized, co if (mf.deleted) mf.enabled = false; mf.custom = custom; - m_mixed.push_back(std::move(mf)); + mf.origin_auto = origin_auto; + rebuilt.push_back(std::move(mf)); ++loaded_rows; } + + // Keep any newly generated auto rows that were not present in serialized + // definitions and append them at the end to preserve existing virtual IDs. + for (const MixedFilament &auto_mf : auto_rows) { + const auto key = canonical_pair(auto_mf.component_a, auto_mf.component_b); + if (consumed_auto_pairs.count(key) != 0) + continue; + MixedFilament mf = auto_mf; + mf.component_a = key.first; + mf.component_b = key.second; + mf.custom = false; + mf.origin_auto = true; + rebuilt.push_back(std::move(mf)); + ++appended_auto; + } + + m_mixed = std::move(rebuilt); refresh_display_colors(filament_colours); BOOST_LOG_TRIVIAL(info) << "MixedFilamentManager::load_custom_entries" << ", physical_count=" << n << ", parsed_rows=" << parsed_rows << ", loaded_rows=" << loaded_rows << ", updated_auto_rows=" << updated_auto + << ", appended_auto_rows=" << appended_auto << ", skipped_rows=" << skipped_rows << ", mixed_total=" << m_mixed.size(); } diff --git a/src/libslic3r/MixedFilament.hpp b/src/libslic3r/MixedFilament.hpp index 4c6a75cc67..588843fabd 100644 --- a/src/libslic3r/MixedFilament.hpp +++ b/src/libslic3r/MixedFilament.hpp @@ -65,6 +65,11 @@ struct MixedFilament // True when this row was user-created (custom) instead of auto-generated. bool custom = false; + // True when this row originated from an auto-generated pair. This remains + // true even after editing so delete logic can keep the base auto pair + // tombstoned instead of letting regeneration resurrect it. + bool origin_auto = false; + // Computed display colour as "#RRGGBB". std::string display_color; @@ -82,7 +87,8 @@ struct MixedFilament distribution_mode == rhs.distribution_mode && enabled == rhs.enabled && deleted == rhs.deleted && - custom == rhs.custom; + custom == rhs.custom && + origin_auto == rhs.origin_auto; } bool operator!=(const MixedFilament &rhs) const { return !(*this == rhs); } }; @@ -125,7 +131,6 @@ public: void apply_gradient_settings(int gradient_mode, float lower_bound, float upper_bound, - int cycle_layers, bool advanced_dithering = false); // Persist only custom rows. @@ -196,7 +201,6 @@ private: int m_gradient_mode = 0; float m_height_lower_bound = 0.04f; float m_height_upper_bound = 0.16f; - int m_cycle_layers = 4; bool m_advanced_dithering = false; }; diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp index 62b8f5281f..d97a7a7d67 100644 --- a/src/libslic3r/PresetBundle.cpp +++ b/src/libslic3r/PresetBundle.cpp @@ -45,7 +45,6 @@ static std::vector s_project_options { "mixed_filament_gradient_mode", "mixed_filament_height_lower_bound", "mixed_filament_height_upper_bound", - "mixed_filament_cycle_layers", "mixed_filament_advanced_dithering", "mixed_filament_surface_indentation", "mixed_filament_definitions", @@ -1869,7 +1868,7 @@ void PresetBundle::update_num_filaments(unsigned int to_del_filament_id) ams_multi_color_filment.resize(to_del_filament_id); } - update_multi_material_filament_presets(to_del_filament_id); + update_multi_material_filament_presets(to_del_filament_id, old_filament_count); } void PresetBundle::set_num_filaments(unsigned int n, std::vector new_colors) { @@ -1890,7 +1889,7 @@ void PresetBundle::set_num_filaments(unsigned int n, std::vector ne } } } - update_multi_material_filament_presets(); + update_multi_material_filament_presets(size_t(-1), size_t(old_filament_count)); } void PresetBundle::set_num_filaments(unsigned int n, std::string new_color) { @@ -1914,7 +1913,7 @@ void PresetBundle::set_num_filaments(unsigned int n, std::string new_color) } } - update_multi_material_filament_presets(); + update_multi_material_filament_presets(size_t(-1), size_t(old_filament_count)); } unsigned int PresetBundle::sync_ams_list(unsigned int &unknowns) @@ -3226,7 +3225,7 @@ std::pair PresetBundle::load_vendor_configs_ return std::make_pair(std::move(substitutions), presets_loaded); } -void PresetBundle::update_multi_material_filament_presets(size_t to_delete_filament_id) +void PresetBundle::update_multi_material_filament_presets(size_t to_delete_filament_id, size_t old_num_filaments_arg) { if (printers.get_edited_preset().printer_technology() != ptFFF) return; @@ -3245,7 +3244,9 @@ void PresetBundle::update_multi_material_filament_presets(size_t to_delete_filam size_t num_filaments = this->filament_presets.size(); #endif const bool deleting_filament = (to_delete_filament_id != size_t(-1)); - const size_t old_num_filaments = deleting_filament ? (num_filaments + 1) : num_filaments; + const size_t old_num_filaments = (old_num_filaments_arg != size_t(-1)) + ? old_num_filaments_arg + : (deleting_filament ? (num_filaments + 1) : num_filaments); const std::vector old_mixed = this->mixed_filaments.mixed_filaments(); m_last_filament_id_remap.clear(); @@ -3288,13 +3289,6 @@ void PresetBundle::update_multi_material_filament_presets(size_t to_delete_filam ConfigOptionStrings *color_opt = this->project_config.option("filament_colour"); if (color_opt) { DynamicPrintConfig &print_cfg = this->prints.get_edited_preset().config; - auto get_mixed_int = [this, &print_cfg](const std::string &key, int fallback) { - if (this->project_config.has(key)) - return this->project_config.opt_int(key); - if (print_cfg.has(key)) - return print_cfg.opt_int(key); - return fallback; - }; auto get_mixed_bool = [this, &print_cfg](const std::string &key, bool fallback) { if (const ConfigOptionBool *opt = this->project_config.option(key)) return opt->value; @@ -3354,16 +3348,14 @@ void PresetBundle::update_multi_material_filament_presets(size_t to_delete_filam int gradient_mode = get_mixed_mode(false) ? 1 : 0; float lower_bound = get_mixed_float("mixed_filament_height_lower_bound", 0.04f); float upper_bound = get_mixed_float("mixed_filament_height_upper_bound", 0.16f); - int cycle_layers = get_mixed_int("mixed_filament_cycle_layers", 4); bool advanced_dithering = get_mixed_bool("mixed_filament_advanced_dithering", false); gradient_mode = std::clamp(gradient_mode, 0, 1); lower_bound = std::max(0.01f, lower_bound); upper_bound = std::max(lower_bound, upper_bound); - cycle_layers = std::max(2, cycle_layers); this->mixed_filaments.clear_custom_entries(); this->mixed_filaments.load_custom_entries(get_mixed_string("mixed_filament_definitions"), color_opt->values); - this->mixed_filaments.apply_gradient_settings(gradient_mode, lower_bound, upper_bound, cycle_layers, advanced_dithering); + this->mixed_filaments.apply_gradient_settings(gradient_mode, lower_bound, upper_bound, advanced_dithering); const std::string serialized = this->mixed_filaments.serialize_custom_entries(); set_mixed_string("mixed_filament_definitions", serialized); @@ -3371,9 +3363,10 @@ void PresetBundle::update_multi_material_filament_presets(size_t to_delete_filam } // Build old->new filament ID remap for painted facet data normalization. - if (deleting_filament) { - const unsigned int deleted_1based = unsigned(to_delete_filament_id + 1); - + // 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) @@ -3383,20 +3376,30 @@ void PresetBundle::update_multi_material_filament_presets(size_t to_delete_filam 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) { - if (old_id == deleted_1based) - m_last_filament_id_remap[old_id] = 0; - else - m_last_filament_id_remap[old_id] = old_id > deleted_1based ? old_id - 1 : 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; + } + m_last_filament_id_remap[old_id] = mapped; } - std::map, unsigned int> new_pair_to_id; + 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_id[{mf.component_a, mf.component_b}] = 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; unsigned int old_virtual_id = unsigned(old_num_filaments + 1); for (const auto &mf : old_mixed) { if (!mf.enabled) @@ -3407,15 +3410,51 @@ void PresetBundle::update_multi_material_filament_presets(size_t to_delete_filam if (a == deleted_1based || b == deleted_1based) { m_last_filament_id_remap[old_virtual_id] = 0; } else { - if (a > deleted_1based) - --a; - if (b > deleted_1based) - --b; - auto it = new_pair_to_id.find({a, b}); - m_last_filament_id_remap[old_virtual_id] = (it == new_pair_to_id.end()) ? 0 : it->second; + if (deleting_filament) { + if (a > deleted_1based) + --a; + if (b > deleted_1based) + --b; + } + const auto key = canonical_pair(a, b); + auto it = new_pair_to_ids.find(key); + if (it == new_pair_to_ids.end()) { + m_last_filament_id_remap[old_virtual_id] = 0; + } else { + size_t &used = used_per_pair[key]; + if (used >= it->second.size()) { + m_last_filament_id_remap[old_virtual_id] = 0; + } else { + m_last_filament_id_remap[old_virtual_id] = it->second[used++]; + } + } } ++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); } } diff --git a/src/libslic3r/PresetBundle.hpp b/src/libslic3r/PresetBundle.hpp index ea524106f3..d721cf3672 100644 --- a/src/libslic3r/PresetBundle.hpp +++ b/src/libslic3r/PresetBundle.hpp @@ -252,10 +252,17 @@ public: // Read out the number of extruders from an active printer preset, // update size and content of filament_presets. - void update_multi_material_filament_presets(size_t to_delete_filament_id = size_t(-1)); - // Mapping generated during the latest filament deletion. + void update_multi_material_filament_presets(size_t to_delete_filament_id = size_t(-1), + size_t old_num_filaments = size_t(-1)); + // 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; } + std::vector consume_last_filament_id_remap() + { + std::vector out = std::move(m_last_filament_id_remap); + m_last_filament_id_remap.clear(); + return out; + } // Update the is_compatible flag of all print and filament presets depending on whether they are marked // as compatible with the currently selected printer (and print in case of filament presets). diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 59b13e59fe..528fdef999 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -251,7 +251,6 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n || opt_key == "mixed_filament_gradient_mode" || opt_key == "mixed_filament_height_lower_bound" || opt_key == "mixed_filament_height_upper_bound" - || opt_key == "mixed_filament_cycle_layers" || opt_key == "mixed_filament_advanced_dithering" || opt_key == "mixed_filament_surface_indentation" || opt_key == "mixed_filament_definitions" diff --git a/src/libslic3r/PrintApply.cpp b/src/libslic3r/PrintApply.cpp index d2caa3fd95..efcf985141 100644 --- a/src/libslic3r/PrintApply.cpp +++ b/src/libslic3r/PrintApply.cpp @@ -1173,7 +1173,6 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ new_full_config.option("mixed_filament_gradient_mode", true); new_full_config.option("mixed_filament_height_lower_bound", true); new_full_config.option("mixed_filament_height_upper_bound", true); - new_full_config.option("mixed_filament_cycle_layers", true); new_full_config.option("mixed_filament_advanced_dithering", true); new_full_config.option("mixed_filament_pointillism_pixel_size", true); new_full_config.option("mixed_filament_pointillism_line_gap", true); @@ -1185,7 +1184,6 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ m_config.option("mixed_filament_gradient_mode", true); m_config.option("mixed_filament_height_lower_bound", true); m_config.option("mixed_filament_height_upper_bound", true); - m_config.option("mixed_filament_cycle_layers", true); m_config.option("mixed_filament_advanced_dithering", true); m_config.option("mixed_filament_pointillism_pixel_size", true); m_config.option("mixed_filament_pointillism_line_gap", true); @@ -1197,7 +1195,6 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ m_default_object_config.option("mixed_filament_gradient_mode", true); m_default_object_config.option("mixed_filament_height_lower_bound", true); m_default_object_config.option("mixed_filament_height_upper_bound", true); - m_default_object_config.option("mixed_filament_cycle_layers", true); m_default_object_config.option("mixed_filament_advanced_dithering", true); m_default_object_config.option("mixed_filament_pointillism_pixel_size", true); m_default_object_config.option("mixed_filament_pointillism_line_gap", true); @@ -1303,7 +1300,6 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ int mixed_gradient_mode = 0; float mixed_height_lower = 0.04f; float mixed_height_upper = 0.16f; - int mixed_cycle_layers = 4; bool mixed_advanced_dither = false; float mixed_pointillism_pixel_size = 0.f; float mixed_pointillism_line_gap = 0.f; @@ -1319,8 +1315,6 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ mixed_height_lower = float(new_full_config.opt_float("mixed_filament_height_lower_bound")); if (new_full_config.has("mixed_filament_height_upper_bound")) mixed_height_upper = float(new_full_config.opt_float("mixed_filament_height_upper_bound")); - if (new_full_config.has("mixed_filament_cycle_layers")) - mixed_cycle_layers = new_full_config.opt_int("mixed_filament_cycle_layers"); if (new_full_config.has("mixed_filament_advanced_dithering")) { if (const ConfigOptionBool *opt = new_full_config.option("mixed_filament_advanced_dithering")) mixed_advanced_dither = opt->value; @@ -1339,7 +1333,6 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ mixed_gradient_mode = std::clamp(mixed_gradient_mode, 0, 1); mixed_height_lower = std::max(0.01f, mixed_height_lower); mixed_height_upper = std::max(mixed_height_lower, mixed_height_upper); - mixed_cycle_layers = std::max(2, mixed_cycle_layers); mixed_pointillism_pixel_size = std::max(0.f, mixed_pointillism_pixel_size); mixed_pointillism_line_gap = std::max(0.f, mixed_pointillism_line_gap); mixed_surface_indentation = std::clamp(mixed_surface_indentation, -2.f, 2.f); @@ -1348,7 +1341,6 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ << ", gradient_mode=" << mixed_gradient_mode << ", lower=" << mixed_height_lower << ", upper=" << mixed_height_upper - << ", cycle_layers=" << mixed_cycle_layers << ", advanced_dither=" << (mixed_advanced_dither ? 1 : 0) << ", pointillism_pixel_size=" << mixed_pointillism_pixel_size << ", pointillism_line_gap=" << mixed_pointillism_line_gap @@ -1366,7 +1358,6 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ m_mixed_filament_mgr.apply_gradient_settings(mixed_gradient_mode, mixed_height_lower, mixed_height_upper, - mixed_cycle_layers, mixed_advanced_dither); size_t mixed_custom_count = 0; for (const auto &mf : m_mixed_filament_mgr.mixed_filaments()) diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index f46eb9f4a3..ee5f615bbc 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -4174,15 +4174,6 @@ void PrintConfigDef::init_fff_params() def->mode = comAdvanced; def->set_default_value(new ConfigOptionFloat(0.16)); - def = this->add("mixed_filament_cycle_layers", coInt); - def->label = L("Mixed filament layer cycle"); - def->category = L("Others"); - def->tooltip = L("Number of layers in one alternation cycle for layer-cycle mixed filament mode.\n\n" - "Detailed mixed filament setting explanations will be published once the project wiki is available."); - def->min = 2; - def->mode = comAdvanced; - def->set_default_value(new ConfigOptionInt(4)); - def = this->add("mixed_filament_advanced_dithering", coBool); def->label = L("Advanced dithering"); def->category = L("Others"); diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index ae7518923e..93ee468b16 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -1356,7 +1356,6 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE( ((ConfigOptionBool, mixed_filament_gradient_mode)) ((ConfigOptionFloat, mixed_filament_height_lower_bound)) ((ConfigOptionFloat, mixed_filament_height_upper_bound)) - ((ConfigOptionInt, mixed_filament_cycle_layers)) ((ConfigOptionBool, mixed_filament_advanced_dithering)) ((ConfigOptionFloat, mixed_filament_pointillism_pixel_size)) ((ConfigOptionFloat, mixed_filament_pointillism_line_gap)) diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index 1ba786eafb..c69193201b 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -971,7 +971,6 @@ bool PrintObject::invalidate_state_by_config_options( opt_key == "mixed_filament_gradient_mode" || opt_key == "mixed_filament_height_lower_bound" || opt_key == "mixed_filament_height_upper_bound" - || opt_key == "mixed_filament_cycle_layers" || opt_key == "mixed_filament_advanced_dithering" || opt_key == "mixed_filament_surface_indentation" || opt_key == "mixed_filament_definitions") { diff --git a/src/libslic3r/PrintObjectSlice.cpp b/src/libslic3r/PrintObjectSlice.cpp index 1991196a0d..3770a1ed14 100644 --- a/src/libslic3r/PrintObjectSlice.cpp +++ b/src/libslic3r/PrintObjectSlice.cpp @@ -900,12 +900,14 @@ static bool apply_mixed_surface_indentation(PrintObject &print_object, std::vect size_t changed_states = 0; size_t emptied_states = 0; size_t overlap_clipped_states = 0; + size_t outside_trimmed_states = 0; for (size_t layer_id = 0; layer_id < segmentation.size(); ++layer_id) { if (segmentation[layer_id].size() != num_channels) continue; bool layer_changed = false; + ExPolygons outside_trim_band; ExPolygons occupied; if (expand_outward) { for (size_t channel_idx = 0; channel_idx < num_channels; ++channel_idx) { @@ -919,6 +921,25 @@ static bool apply_mixed_surface_indentation(PrintObject &print_object, std::vect } if (occupied.size() > 1) occupied = union_ex(occupied); + } else { + ExPolygons layer_masks; + for (size_t channel_idx = 0; channel_idx < num_channels; ++channel_idx) { + const ExPolygons &state_masks = segmentation[layer_id][channel_idx]; + if (!state_masks.empty()) + append(layer_masks, state_masks); + } + if (!layer_masks.empty()) { + if (layer_masks.size() > 1) + layer_masks = union_ex(layer_masks); + + ExPolygons layer_inner = offset_ex(layer_masks, -delta_scaled); + if (!layer_inner.empty() && layer_inner.size() > 1) + layer_inner = union_ex(layer_inner); + + outside_trim_band = layer_inner.empty() ? layer_masks : diff_ex(layer_masks, layer_inner, ApplySafetyOffset::Yes); + if (!outside_trim_band.empty() && outside_trim_band.size() > 1) + outside_trim_band = union_ex(outside_trim_band); + } } for (size_t channel_idx = num_physical; channel_idx < num_channels; ++channel_idx) { @@ -930,15 +951,24 @@ static bool apply_mixed_surface_indentation(PrintObject &print_object, std::vect if (!mixed_mgr.is_mixed(state_id, num_physical)) continue; - ExPolygons adjusted = offset_ex(state_masks, expand_outward ? delta_scaled : -delta_scaled); - if (!adjusted.empty() && adjusted.size() > 1) - adjusted = union_ex(adjusted); + ExPolygons adjusted; + if (expand_outward) { + adjusted = offset_ex(state_masks, delta_scaled); + if (!adjusted.empty() && adjusted.size() > 1) + adjusted = union_ex(adjusted); - if (expand_outward && !adjusted.empty() && !occupied.empty()) { - ExPolygons clipped = diff_ex(adjusted, occupied, ApplySafetyOffset::Yes); - if (std::abs(area(clipped)) + EPSILON < std::abs(area(adjusted))) - ++overlap_clipped_states; - adjusted = std::move(clipped); + if (!adjusted.empty() && !occupied.empty()) { + ExPolygons clipped = diff_ex(adjusted, occupied, ApplySafetyOffset::Yes); + if (std::abs(area(clipped)) + EPSILON < std::abs(area(adjusted))) + ++overlap_clipped_states; + adjusted = std::move(clipped); + if (!adjusted.empty() && adjusted.size() > 1) + adjusted = union_ex(adjusted); + } + } else { + adjusted = outside_trim_band.empty() ? state_masks : diff_ex(state_masks, outside_trim_band, ApplySafetyOffset::Yes); + if (std::abs(area(adjusted)) + EPSILON < std::abs(area(state_masks))) + ++outside_trimmed_states; if (!adjusted.empty() && adjusted.size() > 1) adjusted = union_ex(adjusted); } @@ -970,7 +1000,8 @@ static bool apply_mixed_surface_indentation(PrintObject &print_object, std::vect << " changed_layers=" << changed_layers << " changed_states=" << changed_states << " emptied_states=" << emptied_states - << " overlap_clipped_states=" << overlap_clipped_states; + << " overlap_clipped_states=" << overlap_clipped_states + << " outside_trimmed_states=" << outside_trimmed_states; return true; } diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index 9153e57a63..490a526f93 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -2432,23 +2432,10 @@ void GLCanvas3D::reload_scene(bool refresh_immediately, bool force_full_scene_re std::sort(model_volume_state.begin(), model_volume_state.end(), model_volume_state_lower); std::sort(aux_volume_state.begin(), aux_volume_state.end(), model_volume_state_lower); - // BBS: normalize painting data with current available filament count - // (physical + enabled mixed/virtual filaments). - for (unsigned int obj_idx = 0; obj_idx < (unsigned int)m_model->objects.size(); ++obj_idx) { - const ModelObject& model_object = *m_model->objects[obj_idx]; - for (int volume_idx = 0; volume_idx < (int)model_object.volumes.size(); ++volume_idx) { - ModelVolume& model_volume = *model_object.volumes[volume_idx]; - if (!model_volume.is_model_part()) - continue; - - unsigned int filaments_count = (unsigned int)dynamic_cast(m_config->option("filament_colour"))->values.size(); - size_t available_filaments = filaments_count; - if (wxGetApp().preset_bundle != nullptr) - available_filaments = wxGetApp().preset_bundle->mixed_filaments.total_filaments(filaments_count); - - model_volume.update_extruder_count(available_filaments); - } - } + // Mixed/physical filament ID normalization is handled in + // Plater::on_filaments_change() with explicit old->new remap. + // Mutating MMU painted states here during scene refresh may run before + // remap and corrupt virtual mixed IDs. // Release all ModelVolume based GLVolumes not found in the current Model. Find the GLVolume of a hollowed mesh. for (size_t volume_id = 0; volume_id < m_volumes.volumes.size(); ++volume_id) { diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index a82ba4f734..c4af957107 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -1,6 +1,7 @@ #include "Plater.hpp" #include "libslic3r/Config.hpp" #include "libslic3r/MixedFilament.hpp" +#include "libslic3r/filament_mixer.h" #include "common_func/common_func.hpp" #include @@ -2320,6 +2321,69 @@ wxColour parse_mixed_color(const std::string &value) return color; } +wxColour blend_pair_filament_mixer(const wxColour &left, const wxColour &right, float t) +{ + const wxColour safe_left = left.IsOk() ? left : wxColour("#26A69A"); + const wxColour safe_right = right.IsOk() ? right : wxColour("#26A69A"); + + unsigned char out_r = static_cast(safe_left.Red()); + unsigned char out_g = static_cast(safe_left.Green()); + unsigned char out_b = static_cast(safe_left.Blue()); + ::Slic3r::filament_mixer_lerp(static_cast(safe_left.Red()), + static_cast(safe_left.Green()), + static_cast(safe_left.Blue()), + static_cast(safe_right.Red()), + static_cast(safe_right.Green()), + static_cast(safe_right.Blue()), + std::clamp(t, 0.f, 1.f), + &out_r, &out_g, &out_b); + return wxColour(out_r, out_g, out_b); +} + +wxColour blend_multi_filament_mixer(const std::vector &colors, const std::vector &weights) +{ + if (colors.empty() || weights.empty()) + return wxColour("#26A69A"); + + unsigned char out_r = 0; + unsigned char out_g = 0; + unsigned char out_b = 0; + double accumulated_weight = 0.0; + bool has_color = false; + + for (size_t i = 0; i < colors.size() && i < weights.size(); ++i) { + const double weight = std::max(0.0, weights[i]); + if (weight <= 0.0) + continue; + + const wxColour safe = colors[i].IsOk() ? colors[i] : wxColour("#26A69A"); + const unsigned char r = static_cast(safe.Red()); + const unsigned char g = static_cast(safe.Green()); + const unsigned char b = static_cast(safe.Blue()); + + if (!has_color) { + out_r = r; + out_g = g; + out_b = b; + accumulated_weight = weight; + has_color = true; + continue; + } + + const double new_total = accumulated_weight + weight; + if (new_total <= 0.0) + continue; + const float t = float(weight / new_total); + ::Slic3r::filament_mixer_lerp(out_r, out_g, out_b, r, g, b, t, &out_r, &out_g, &out_b); + accumulated_weight = new_total; + } + + if (!has_color) + return wxColour("#26A69A"); + + return wxColour(out_r, out_g, out_b); +} + class MixedGradientSelector : public wxPanel { public: @@ -2446,7 +2510,28 @@ private: } } } else { - dc.GradientFillLinear(rect, m_left, m_right, wxEAST); + const int w = rect.GetWidth(); + const int h = rect.GetHeight(); + wxImage img(w, h); + unsigned char *data = img.GetData(); + if (data != nullptr) { + for (int x = 0; x < w; ++x) { + const float t = (w > 1) ? float(x) / float(w - 1) : 0.5f; + const wxColour col = blend_pair_filament_mixer(m_left, m_right, t); + const unsigned char r = static_cast(col.Red()); + const unsigned char g = static_cast(col.Green()); + const unsigned char b = static_cast(col.Blue()); + for (int y = 0; y < h; ++y) { + const int idx = (y * w + x) * 3; + data[idx + 0] = r; + data[idx + 1] = g; + data[idx + 2] = b; + } + } + dc.DrawBitmap(wxBitmap(img), rect.GetLeft(), rect.GetTop(), false); + } else { + dc.GradientFillLinear(rect, m_left, m_right, wxEAST); + } } dc.SetPen(wxPen(is_dark ? wxColour(100, 100, 106) : wxColour(170, 170, 170), 1)); dc.SetBrush(*wxTRANSPARENT_BRUSH); @@ -2717,19 +2802,7 @@ private: wxColour blended_color(const std::vector &weights) const { - if (weights.empty() || m_colors.empty()) - return wxColour("#26A69A"); - double r = 0.0; - double g = 0.0; - double b = 0.0; - for (size_t i = 0; i < weights.size() && i < m_colors.size(); ++i) { - r += weights[i] * double(m_colors[i].Red()); - g += weights[i] * double(m_colors[i].Green()); - b += weights[i] * double(m_colors[i].Blue()); - } - return wxColour(std::clamp(int(std::lround(r)), 0, 255), - std::clamp(int(std::lround(g)), 0, 255), - std::clamp(int(std::lround(b)), 0, 255)); + return blend_multi_filament_mixer(m_colors, weights); } wxRect canvas_rect() const @@ -3819,13 +3892,6 @@ void Sidebar::update_mixed_filament_panel() std::vector physical_colors = color_opt ? color_opt->values : std::vector(); physical_colors.resize(num_physical, "#26A69A"); - auto get_mixed_int = [preset_bundle, print_cfg](const std::string &key, int fallback) { - if (preset_bundle->project_config.has(key)) - return preset_bundle->project_config.opt_int(key); - if (print_cfg && print_cfg->has(key)) - return print_cfg->opt_int(key); - return fallback; - }; auto get_mixed_bool = [preset_bundle, print_cfg](const std::string &key, bool fallback) { if (const ConfigOptionBool *opt = preset_bundle->project_config.option(key)) return opt->value; @@ -3872,18 +3938,6 @@ void Sidebar::update_mixed_filament_panel() } return project_value.empty() ? fallback : project_value; }; - auto set_mixed_int = [preset_bundle, print_cfg](const std::string &key, int value) { - if (print_cfg) { - if (ConfigOptionInt *opt = print_cfg->option(key)) - opt->value = value; - else - print_cfg->set_key_value(key, new ConfigOptionInt(value)); - } - if (ConfigOptionInt *opt = preset_bundle->project_config.option(key)) - opt->value = value; - else - preset_bundle->project_config.set_key_value(key, new ConfigOptionInt(value)); - }; auto set_mixed_float = [preset_bundle, print_cfg](const std::string &key, float value) { if (print_cfg) { if (ConfigOptionFloat *opt = print_cfg->option(key)) @@ -4215,7 +4269,6 @@ void Sidebar::update_mixed_filament_panel() int gradient_mode = height_weighted_mode ? 1 : 0; float lower_bound = std::max(0.01f, get_mixed_float("mixed_filament_height_lower_bound", 0.04f)); float upper_bound = std::max(lower_bound, get_mixed_float("mixed_filament_height_upper_bound", 0.16f)); - int cycle_layers = std::max(2, get_mixed_int("mixed_filament_cycle_layers", 4)); float pointillism_pixel_size = std::max(0.f, get_mixed_float("mixed_filament_pointillism_pixel_size", 0.f)); float pointillism_line_gap = std::max(0.f, get_mixed_float("mixed_filament_pointillism_line_gap", 0.f)); float mixed_surface_indentation = std::clamp(get_mixed_float("mixed_filament_surface_indentation", 0.f), -2.f, 2.f); @@ -4226,7 +4279,7 @@ void Sidebar::update_mixed_filament_panel() mixed_mgr.auto_generate(physical_colors); mixed_mgr.clear_custom_entries(); mixed_mgr.load_custom_entries(mixed_definitions, physical_colors); - mixed_mgr.apply_gradient_settings(gradient_mode, lower_bound, upper_bound, cycle_layers, advanced_dithering); + mixed_mgr.apply_gradient_settings(gradient_mode, lower_bound, upper_bound, advanced_dithering); // During project load, sidebar may refresh before physical filament combos // finish syncing. Avoid overwriting persisted mixed definitions while the @@ -4235,7 +4288,6 @@ void Sidebar::update_mixed_filament_panel() set_mixed_mode(height_weighted_mode); set_mixed_float("mixed_filament_height_lower_bound", lower_bound); set_mixed_float("mixed_filament_height_upper_bound", upper_bound); - set_mixed_int("mixed_filament_cycle_layers", cycle_layers); set_mixed_float("mixed_filament_pointillism_pixel_size", pointillism_pixel_size); set_mixed_float("mixed_filament_pointillism_line_gap", pointillism_line_gap); set_mixed_float("mixed_filament_surface_indentation", mixed_surface_indentation); @@ -4392,24 +4444,23 @@ void Sidebar::update_mixed_filament_panel() float(preset_bundle->project_config.opt_float("mixed_filament_height_lower_bound")) : 0.04f; float hi = preset_bundle->project_config.has("mixed_filament_height_upper_bound") ? float(preset_bundle->project_config.opt_float("mixed_filament_height_upper_bound")) : 0.16f; - int cycle = preset_bundle->project_config.has("mixed_filament_cycle_layers") ? - preset_bundle->project_config.opt_int("mixed_filament_cycle_layers") : 4; bool advanced = false; if (const ConfigOptionBool *opt = preset_bundle->project_config.option("mixed_filament_advanced_dithering")) advanced = opt->value; mode = std::clamp(mode, 0, 1); lo = std::max(0.01f, lo); hi = std::max(lo, hi); - cycle = std::max(2, cycle); - mgr.apply_gradient_settings(mode, lo, hi, cycle, advanced); + mgr.apply_gradient_settings(mode, lo, hi, advanced); update_dynamic_filament_list(); }; + size_t visible_mixed_idx = 0; for (size_t mixed_id = 0; mixed_id < mixed.size(); ++mixed_id) { MixedFilament &mf = mixed[mixed_id]; if (mf.deleted) continue; - const bool editable_row = mf.custom; + const size_t display_mixed_idx = visible_mixed_idx++; + const bool auto_row = !mf.custom; auto *row = new wxPanel(rows_scroller, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE); row->SetBackgroundColour(mixed_row_bg); @@ -4428,7 +4479,7 @@ void Sidebar::update_mixed_filament_panel() swatch->SetMinSize(wxSize(FromDIP(12), FromDIP(12))); header_sizer->Add(swatch, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, compact_gap_x); - const int virtual_filament_id = int(num_physical + mixed_id + 1); + const int virtual_filament_id = int(num_physical + display_mixed_idx + 1); auto *name_label = new wxStaticText(header_panel, wxID_ANY, wxString::Format("Mixed Filament %d", virtual_filament_id)); name_label->SetForegroundColour(mixed_text_fg); header_sizer->Add(name_label, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, compact_gap_x); @@ -4463,16 +4514,60 @@ void Sidebar::update_mixed_filament_panel() del_btn->SetToolTip(_L("Delete mixed filament")); header_sizer->Add(del_btn, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, compact_gap_x); - del_btn->Bind(wxEVT_BUTTON, [this, mixed_id, set_mixed_string, notify_mixed_change](wxCommandEvent&) { + del_btn->Bind(wxEVT_BUTTON, [this, mixed_id, num_physical, set_mixed_string, notify_mixed_change](wxCommandEvent&) { if (wxGetApp().preset_bundle) { auto &mgr = wxGetApp().preset_bundle->mixed_filaments; auto &mfs = mgr.mixed_filaments(); if (mixed_id < mfs.size()) { - if (mfs[mixed_id].custom) + auto canonical_pair = [](unsigned int a, unsigned int b) { + return std::make_pair(std::min(a, b), std::max(a, b)); + }; + MixedFilament &target = mfs[mixed_id]; + const auto target_pair = canonical_pair(target.component_a, target.component_b); + const bool valid_auto_pair = target_pair.first >= 1 && + target_pair.second >= 1 && + target_pair.first <= num_physical && + target_pair.second <= num_physical && + target_pair.first != target_pair.second; + if (target.custom && target.origin_auto && valid_auto_pair) { + bool tombstoned_existing_auto = false; + for (size_t idx = 0; idx < mfs.size(); ++idx) { + if (idx == mixed_id) + continue; + MixedFilament &candidate = mfs[idx]; + if (candidate.custom) + continue; + if (canonical_pair(candidate.component_a, candidate.component_b) != target_pair) + continue; + candidate.deleted = true; + candidate.enabled = false; + tombstoned_existing_auto = true; + break; + } + + if (tombstoned_existing_auto) { + mfs.erase(mfs.begin() + mixed_id); + } else { + target.component_a = target_pair.first; + target.component_b = target_pair.second; + target.mix_b_percent = 50; + target.ratio_a = 1; + target.ratio_b = 1; + target.manual_pattern.clear(); + target.gradient_component_ids.clear(); + target.gradient_component_weights.clear(); + target.pointillism_all_filaments = false; + target.distribution_mode = int(MixedFilament::Simple); + target.custom = false; + target.origin_auto = true; + target.deleted = true; + target.enabled = false; + } + } else if (target.custom) { mfs.erase(mfs.begin() + mixed_id); - else { - mfs[mixed_id].deleted = true; - mfs[mixed_id].enabled = false; + } else { + target.deleted = true; + target.enabled = false; } p->m_expanded_mixed_filament_rows.clear(); set_mixed_string("mixed_filament_definitions", mgr.serialize_custom_entries()); @@ -4591,18 +4686,14 @@ void Sidebar::update_mixed_filament_panel() }); }; - header_panel->SetToolTip(editable_row ? - _L("Click to expand/retract mixed filament settings") : - _L("Automatic mixed filament settings are read-only.")); - if (editable_row) { - bind_toggle_target(row); - bind_toggle_target(header_panel); - bind_toggle_target(name_label); - bind_toggle_target(summary_label); - bind_toggle_target(swatch); - } else { - p->m_expanded_mixed_filament_rows.erase(mixed_id); - } + header_panel->SetToolTip(auto_row ? + _L("Click to edit automatic mixed filament settings (saved as custom).") : + _L("Click to expand/retract mixed filament settings")); + bind_toggle_target(row); + bind_toggle_target(header_panel); + bind_toggle_target(name_label); + bind_toggle_target(summary_label); + bind_toggle_target(swatch); bind_hover_target(row); bind_hover_target(header_panel); bind_hover_target(name_label); @@ -4614,7 +4705,7 @@ void Sidebar::update_mixed_filament_panel() evt.Skip(); }); - if (editable_row && p->m_expanded_mixed_filament_rows.count(mixed_id) != 0) { + if (p->m_expanded_mixed_filament_rows.count(mixed_id) != 0) { ensure_editor(); editor_host->Show(); } @@ -16673,9 +16764,9 @@ void Plater::on_filaments_delete(size_t num_filaments, size_t filament_id, int r state_map[i] = EnforcerBlockerType(i); const bool can_use_remap = replace_filament_id < 0; - static const std::vector empty_remap; - const std::vector &id_remap = - preset_bundle != nullptr ? preset_bundle->last_filament_id_remap() : empty_remap; + std::vector id_remap; + if (preset_bundle != nullptr) + id_remap = preset_bundle->consume_last_filament_id_remap(); if (can_use_remap && !id_remap.empty()) { for (size_t i = 1; i < state_map.size(); ++i) { const unsigned int mapped = i < id_remap.size() ? id_remap[i] : 0; @@ -16739,6 +16830,152 @@ void Plater::on_filaments_change(size_t num_filaments) { // only update elements in plater update_filament_colors_in_full_config(); + + const size_t old_num_filaments = sidebar().combos_filament().size(); + 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; + }; + auto summarize_used_states = [](const std::vector &used, size_t max_items = 24) { + std::string out = "["; + size_t total = 0; + size_t emitted = 0; + for (size_t i = 1; i < used.size(); ++i) { + if (!used[i]) + continue; + ++total; + if (emitted < max_items) { + if (emitted > 0) + out += ","; + out += std::to_string(i); + ++emitted; + } + } + if (total > emitted) + out += ",..."; + out += "] total=" + std::to_string(total); + return out; + }; + PresetBundle *preset_bundle = wxGetApp().preset_bundle; + // Consume remap before sidebar refresh, which may trigger config sync + // paths that regenerate mixed filaments and clear this remap buffer. + std::vector id_remap; + if (preset_bundle != nullptr) + id_remap = preset_bundle->consume_last_filament_id_remap(); + + size_t total_filaments = num_filaments; + if (preset_bundle != nullptr) + total_filaments = preset_bundle->mixed_filaments.total_filaments(num_filaments); + + EnforcerBlockerStateMap state_map; + for (size_t i = 0; i < state_map.size(); ++i) + state_map[i] = EnforcerBlockerType(i); + + bool have_explicit_remap = false; + bool should_remap_states = false; + if (!id_remap.empty()) { + have_explicit_remap = true; + should_remap_states = true; + for (size_t i = 1; i < state_map.size(); ++i) { + const unsigned int mapped = i < id_remap.size() ? id_remap[i] : 0; + if (mapped == 0 || mapped >= state_map.size() || mapped > total_filaments) + state_map[i] = EnforcerBlockerType::NONE; + else + state_map[i] = EnforcerBlockerType(mapped); + } + } + + // 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) { + const unsigned int mapped = unsigned(state_map[i]); + if (mapped == i) + continue; + ++changed_entries; + if (changed_entries <= 24) { + if (changed_entries > 1) + changed_map_preview += ","; + changed_map_preview += std::to_string(i) + "->" + std::to_string(mapped); + } + } + if (changed_entries > 24) + changed_map_preview += ",..."; + changed_map_preview += "]"; + BOOST_LOG_TRIVIAL(warning) << "MF_REMAP on_filaments_change" + << " old_physical=" << old_num_filaments + << " new_physical=" << num_filaments + << " total_filaments=" << total_filaments + << " id_remap_size=" << id_remap.size() + << " id_remap=" << summarize_uint_vector(id_remap) + << " explicit_remap=" << (have_explicit_remap ? 1 : 0) + << " should_remap_states=" << (should_remap_states ? 1 : 0) + << " changed_entries=" << changed_entries + << " changed_map=" << changed_map_preview; + + size_t obj_idx = 0; + for (ModelObject* mo : wxGetApp().model().objects) { + size_t vol_idx = 0; + for (ModelVolume* mv : mo->volumes) { + std::string used_before; + const bool has_mmu_paint = (mv != nullptr && !mv->mmu_segmentation_facets.empty()); + if (has_mmu_paint) + used_before = summarize_used_states(mv->mmu_segmentation_facets.get_data().used_states); + + if (should_remap_states) + mv->remap_extruder_ids(total_filaments, state_map); + else + mv->update_extruder_count(total_filaments); + + if (has_mmu_paint) { + const std::string used_after = summarize_used_states(mv->mmu_segmentation_facets.get_data().used_states); + BOOST_LOG_TRIVIAL(warning) << "MF_REMAP volume" + << " obj_idx=" << obj_idx + << " vol_idx=" << vol_idx + << " obj_name=" << mo->name + << " vol_name=" << mv->name + << " before=" << used_before + << " after=" << used_after; + } + ++vol_idx; + } + ++obj_idx; + } + + // Keep UI refresh after model remap. Some UI update paths may trigger + // scene/model sync that assumes already-remapped MMU state. sidebar().on_filaments_change(num_filaments); sidebar().obj_list()->update_objects_list_filament_column(num_filaments); @@ -16747,16 +16984,6 @@ void Plater::on_filaments_change(size_t num_filaments) PartPlate* part_plate = plate_list.get_plate(i); part_plate->update_first_layer_print_sequence(num_filaments); } - - size_t total_filaments = num_filaments; - if (wxGetApp().preset_bundle != nullptr) - total_filaments = wxGetApp().preset_bundle->mixed_filaments.total_filaments(num_filaments); - - for (ModelObject* mo : wxGetApp().model().objects) { - for (ModelVolume* mv : mo->volumes) { - mv->update_extruder_count(total_filaments); - } - } } void Plater::on_bed_type_change(BedType bed_type) diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 3795fd79a0..1d0d793c96 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -1787,7 +1787,6 @@ void Tab::on_value_change(const std::string& opt_key, const boost::any& value) opt_key == "mixed_filament_height_upper_bound" || opt_key == "mixed_color_layer_height_a" || opt_key == "mixed_color_layer_height_b" || - opt_key == "mixed_filament_cycle_layers" || opt_key == "mixed_filament_advanced_dithering" || opt_key == "mixed_filament_pointillism_pixel_size" || opt_key == "mixed_filament_pointillism_line_gap" || @@ -2519,7 +2518,6 @@ optgroup->append_single_option_line("skirt_loops", "others_settings_skirt#loops" optgroup->append_single_option_line("mixed_filament_gradient_mode"); optgroup->append_single_option_line("mixed_filament_height_lower_bound"); optgroup->append_single_option_line("mixed_filament_height_upper_bound"); - optgroup->append_single_option_line("mixed_filament_cycle_layers"); optgroup->append_single_option_line("mixed_filament_advanced_dithering"); optgroup->append_single_option_line("mixed_filament_pointillism_pixel_size"); optgroup->append_single_option_line("mixed_filament_pointillism_line_gap");