From 73a4ff9b160ada4e9a324d0ce23cd7e7e14f0de0 Mon Sep 17 00:00:00 2001 From: TheLegendTubaGuy <95944177+thelegendtubaguy@users.noreply.github.com> Date: Sun, 4 Oct 2026 11:03:42 -0500 Subject: [PATCH] Fix adding filaments with incomplete mixed metadata (#15728) --- src/libslic3r/PresetBundle.cpp | 55 ++++--- src/slic3r/GUI/Plater.cpp | 2 +- .../libslic3r/test_preset_bundle_loading.cpp | 149 +++++++++++++++++- 3 files changed, 180 insertions(+), 26 deletions(-) diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp index 54cc2b1f60..eb8321e4bd 100644 --- a/src/libslic3r/PresetBundle.cpp +++ b/src/libslic3r/PresetBundle.cpp @@ -3598,6 +3598,24 @@ void PresetBundle::export_selections(AppConfig &config) BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": printer %1%, print %2%, filaments[0] %3% ")%printers.get_selected_preset_name() % prints.get_selected_preset_name() %filament_presets[0]; } +// Preserve metadata only for existing colour slots; new slots get false/empty defaults. +static void resize_mixed_filament_metadata(DynamicPrintConfig &config, size_t old_slot_count, size_t new_slot_count) +{ + auto resize = [old_slot_count, new_slot_count](auto *opt) { + if (opt) { + opt->values.resize(std::min(old_slot_count, opt->values.size())); + opt->values.resize(new_slot_count); + } + }; + resize(config.option("filament_is_mixed")); + resize(config.option("filament_mixed_components")); + resize(config.option("filament_mixed_sublayer_ratios")); + resize(config.option("filament_mixed_gradient")); + resize(config.option("filament_mixed_gradient_range")); + resize(config.option("filament_mixed_gradient_curve")); + resize(config.option("filament_mixed_gradient_per_part")); +} + void PresetBundle::set_num_filaments(unsigned int n, std::string new_color) { unsigned old_filament_count = this->filament_presets.size(); @@ -3630,22 +3648,7 @@ void PresetBundle::set_num_filaments(unsigned int n, std::string new_color) filament_volume_map->values.resize(n, static_cast(NozzleVolumeType::nvtStandard)); ams_multi_color_filment.resize(n); - // Mixed-color metadata is a parallel per-filament array set, so it has to grow and shrink - // with the filament count exactly like filament_colour above. - if (auto* opt = project_config.option("filament_is_mixed")) - opt->values.resize(n, false); - if (auto* opt = project_config.option("filament_mixed_components")) - opt->values.resize(n, std::string{}); - if (auto* opt = project_config.option("filament_mixed_sublayer_ratios")) - opt->values.resize(n, std::string{}); - if (auto* opt = project_config.option("filament_mixed_gradient")) - opt->values.resize(n, false); - if (auto* opt = project_config.option("filament_mixed_gradient_range")) - opt->values.resize(n, std::string{}); - if (auto* opt = project_config.option("filament_mixed_gradient_curve")) - opt->values.resize(n, std::string{}); - if (auto* opt = project_config.option("filament_mixed_gradient_per_part")) - opt->values.resize(n, false); + resize_mixed_filament_metadata(project_config, old_slot_count, n); //BBS set new filament color to new_color if (!new_color.empty()) { @@ -3757,18 +3760,19 @@ bool PresetBundle::is_mixed_filament(size_t idx) const size_t PresetBundle::num_mixed_filaments() const { - auto *opt = project_config.option("filament_is_mixed"); - return opt == nullptr ? 0 : size_t(std::count(opt->values.begin(), opt->values.end(), true)); + const auto *opt = project_config.option("filament_is_mixed"); + if (opt == nullptr) + return 0; + const size_t slot_count = project_config.option("filament_colour")->values.size(); + return size_t(std::count(opt->values.begin(), opt->values.begin() + std::min(slot_count, opt->values.size()), true)); } -// Counted off the mixed flags, not filament_presets: that list is topped up to the nozzle count on -// its own, so it can sit a slot ahead of the arrays that describe slots. Unlike the sibling -// physical_filament_config_indices(), which bounds by filament_presets, this ignores that top-up. +// Colours describe actual slots; filament_presets can be topped up to the nozzle count alone. +// Imported projects may have fewer mixed flags than slots. Missing flags mean physical filaments. size_t PresetBundle::num_physical_filaments() const { - const auto *opt = project_config.option("filament_is_mixed"); - return opt == nullptr ? filament_presets.size() - : size_t(std::count(opt->values.begin(), opt->values.end(), false)); + const auto *colors = project_config.option("filament_colour"); + return colors->values.size() - num_mixed_filaments(); } std::vector PresetBundle::physical_filament_config_indices() const @@ -5604,6 +5608,9 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool // Load the project config values. In published mode only the plate/bed geometry keys // cross over (the receiver must not inherit the author's filament/purge data). this->project_config.apply_only(config, is_published ? s_project_options_published : s_project_options); + // Older projects inherit one-element mixed defaults, regardless of their filament count. + if (!is_published) + resize_mixed_filament_metadata(this->project_config, num_filaments, num_filaments); break; } diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 158991a92f..0ce364afe5 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -5884,7 +5884,7 @@ void Sidebar::add_custom_filament(wxColour new_col, const std::string& preset_na // Mixed-color slots are kept at the tail of the filament arrays, so a new physical // filament has to be inserted just after the last physical one rather than appended. - // Count off filament_is_mixed, not filament_presets or the combos: the extruder-count spinner + // Count actual colour slots, not filament_presets or the combos: the extruder-count spinner // reaches this before the sidebar has rebuilt, and update_multi_material_filament_presets() // can have grown filament_presets alone. auto *bundle = wxGetApp().preset_bundle; diff --git a/tests/libslic3r/test_preset_bundle_loading.cpp b/tests/libslic3r/test_preset_bundle_loading.cpp index 11e66a90b1..7dfba69cbd 100644 --- a/tests/libslic3r/test_preset_bundle_loading.cpp +++ b/tests/libslic3r/test_preset_bundle_loading.cpp @@ -1617,7 +1617,7 @@ const char *kMixedKeys[] = { } // namespace // Mixed-color filament metadata lives in project_config as parallel per-filament arrays. -// set_num_filaments() is the single place that grows them alongside filament_colour; if it +// set_num_filaments() grows them alongside filament_colour; if it // misses them, creating a mixed slot writes past the end of the short arrays. TEST_CASE("set_num_filaments keeps mixed-color arrays in step with the filament count", "[Preset][Bundle][FilamentMixer]") { @@ -4541,6 +4541,153 @@ TEST_CASE("Published 3MF overrides each extruder slot on a similar multi-extrude } } +TEST_CASE("Loading incomplete mixed metadata normalizes slots before adding a filament", "[Preset][Bundle][FilamentMixer]") +{ + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + const std::vector colors = { "#000000", "#FFFFFF", "#5E5C64" }; + config.option("filament_colour")->values = colors; + config.option("single_extruder_multi_material")->value = true; + config.option("filament_diameter")->values = { 1.75, 1.75, 1.75 }; + config.option("filament_settings_id", true)->values = { "Test PETG", "Test PLA", "Test TPU" }; + const std::vector bool_keys = { + "filament_is_mixed", "filament_mixed_gradient", "filament_mixed_gradient_per_part" + }; + const std::vector string_keys = { + "filament_mixed_components", "filament_mixed_sublayer_ratios", + "filament_mixed_gradient_range", "filament_mixed_gradient_curve" + }; + const size_t metadata_size = GENERATE(0u, 1u, 4u); + for (const auto &key : bool_keys) { + if (metadata_size == 0) + config.erase(key); + else { + auto &values = config.option(key)->values; + values.assign(metadata_size, false); + if (metadata_size > colors.size()) + values.back() = true; + } + } + for (const auto &key : string_keys) { + if (metadata_size == 0) + config.erase(key); + else { + auto &values = config.option(key)->values; + values.assign(metadata_size, ""); + if (metadata_size > colors.size()) + values.back() = "stale"; + } + } + Preset::normalize(config); + + PresetBundle bundle; + bundle.load_config_model("test.3mf", std::move(config), Semver()); + const auto presets = bundle.filament_presets; + REQUIRE(presets.size() == colors.size()); + REQUIRE(presets[0] != presets[1]); + REQUIRE(presets[1] != presets[2]); + REQUIRE(presets[0] != presets[2]); + for (const auto &key : bool_keys) { + CAPTURE(key, metadata_size); + CHECK(bundle.project_config.option(key)->values == + std::vector(colors.size(), false)); + } + for (const auto &key : string_keys) { + CAPTURE(key, metadata_size); + CHECK(bundle.project_config.option(key)->values == + std::vector(colors.size(), "")); + } + REQUIRE(bundle.num_physical_filaments() == colors.size()); + REQUIRE(bundle.num_mixed_filaments() == 0); + + bundle.set_num_filaments(bundle.num_physical_filaments() + bundle.num_mixed_filaments() + 1, "#FF0000"); + REQUIRE(bundle.filament_presets.size() == presets.size() + 1); + const auto &actual_colors = bundle.project_config.option("filament_colour")->values; + REQUIRE(actual_colors.size() == colors.size() + 1); + for (size_t i = 0; i < presets.size(); ++i) { + CHECK(bundle.filament_presets[i] == presets[i]); + CHECK(actual_colors[i] == colors[i]); + } + CHECK(bundle.num_physical_filaments() == colors.size() + 1); + CHECK_FALSE(bundle.is_mixed_filament(colors.size())); +} + +TEST_CASE("Loading a project preserves existing mixed filament definitions", "[Preset][Bundle][FilamentMixer]") +{ + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.option("single_extruder_multi_material")->value = true; + config.option("filament_diameter")->values = { 1.75, 1.75, 1.75 }; + config.option("filament_colour")->values = { "#000000", "#FFFFFF", "#808080" }; + config.option("filament_is_mixed")->values = { false, false, true }; + config.option("filament_mixed_components")->values = { "", "", "1,2" }; + config.option("filament_mixed_sublayer_ratios")->values = { "", "", "1,2" }; + config.option("filament_mixed_gradient")->values = { false, false, true }; + config.option("filament_mixed_gradient_range")->values = { "", "", "0,100" }; + config.option("filament_mixed_gradient_curve")->values = { "", "", "0,0.1|1,0.9" }; + config.option("filament_mixed_gradient_per_part")->values = { false, false, true }; + Preset::normalize(config); + const auto original = config; + + PresetBundle bundle; + bundle.load_config_model("test.3mf", std::move(config), Semver()); + for (const auto *key : kMixedKeys) { + CAPTURE(key); + CHECK(*bundle.project_config.option(key) == *original.option(key)); + } + CHECK(bundle.num_physical_filaments() == 2); + CHECK(bundle.num_mixed_filaments() == 1); +} + +TEST_CASE("Adding a filament preserves slots with incomplete mixed metadata", "[Preset][Bundle][FilamentMixer]") +{ + PresetBundle bundle; + bundle.set_num_filaments(3u, std::string("#000000")); + auto *colors = bundle.project_config.option("filament_colour"); + colors->values = { "#000000", "#FFFFFF", "#5E5C64" }; + bundle.filament_presets = { "Test PETG", "Test PLA", "Test TPU" }; + const auto original_presets = bundle.filament_presets; + const auto original_colors = colors->values; + auto *flags = bundle.project_config.option("filament_is_mixed"); + flags->values = GENERATE(std::vector{}, std::vector{ false }, + std::vector{ false, false, false, true }); + const std::vector string_keys = { + "filament_mixed_components", "filament_mixed_sublayer_ratios", + "filament_mixed_gradient_range", "filament_mixed_gradient_curve" + }; + const std::vector bool_keys = { + "filament_mixed_gradient", "filament_mixed_gradient_per_part" + }; + for (const auto &key : string_keys) + bundle.project_config.option(key)->values = { "", "", "", "stale" }; + for (const auto &key : bool_keys) + bundle.project_config.option(key)->values = { false, false, false, true }; + + REQUIRE(bundle.num_physical_filaments() == original_colors.size()); + REQUIRE(bundle.num_mixed_filaments() == 0); + bundle.set_num_filaments(bundle.num_physical_filaments() + bundle.num_mixed_filaments() + 1, "#FF0000"); + + REQUIRE(bundle.filament_presets.size() == original_presets.size() + 1); + REQUIRE(colors->values.size() == original_colors.size() + 1); + for (size_t i = 0; i < original_presets.size(); ++i) { + CHECK(bundle.filament_presets[i] == original_presets[i]); + CHECK(colors->values[i] == original_colors[i]); + } + CHECK(colors->values.back() == "#FF0000"); + CHECK(bundle.num_physical_filaments() == 4); + CHECK(bundle.num_mixed_filaments() == 0); + REQUIRE(flags->values.size() == 4); + CHECK_FALSE(bundle.is_mixed_filament(3)); + for (const auto &key : string_keys) { + CAPTURE(key); + CHECK(bundle.project_config.option(key)->values == + std::vector{ "", "", "", "" }); + } + for (const auto &key : bool_keys) { + CAPTURE(key); + CHECK(bundle.project_config.option(key)->values == + std::vector{ false, false, false, false }); + } +} + // The nozzle-count top-up in update_multi_material_filament_presets() grows filament_presets on // its own, so a physical count derived from that list reports a slot no per-filament array has // yet. That is what made the extruder-count handler conclude there was nothing to add and leave