Fix adding filaments with incomplete mixed metadata (#15728)

This commit is contained in:
TheLegendTubaGuy
2026-10-04 13:03:42 -03:00
committed by GitHub
parent 67a976e002
commit 73a4ff9b16
3 changed files with 180 additions and 26 deletions
+31 -24
View File
@@ -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<ConfigOptionBools>("filament_is_mixed"));
resize(config.option<ConfigOptionStrings>("filament_mixed_components"));
resize(config.option<ConfigOptionStrings>("filament_mixed_sublayer_ratios"));
resize(config.option<ConfigOptionBools>("filament_mixed_gradient"));
resize(config.option<ConfigOptionStrings>("filament_mixed_gradient_range"));
resize(config.option<ConfigOptionStrings>("filament_mixed_gradient_curve"));
resize(config.option<ConfigOptionBools>("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<int>(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<ConfigOptionBools>("filament_is_mixed"))
opt->values.resize(n, false);
if (auto* opt = project_config.option<ConfigOptionStrings>("filament_mixed_components"))
opt->values.resize(n, std::string{});
if (auto* opt = project_config.option<ConfigOptionStrings>("filament_mixed_sublayer_ratios"))
opt->values.resize(n, std::string{});
if (auto* opt = project_config.option<ConfigOptionBools>("filament_mixed_gradient"))
opt->values.resize(n, false);
if (auto* opt = project_config.option<ConfigOptionStrings>("filament_mixed_gradient_range"))
opt->values.resize(n, std::string{});
if (auto* opt = project_config.option<ConfigOptionStrings>("filament_mixed_gradient_curve"))
opt->values.resize(n, std::string{});
if (auto* opt = project_config.option<ConfigOptionBools>("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<ConfigOptionBools>("filament_is_mixed");
return opt == nullptr ? 0 : size_t(std::count(opt->values.begin(), opt->values.end(), true));
const auto *opt = project_config.option<ConfigOptionBools>("filament_is_mixed");
if (opt == nullptr)
return 0;
const size_t slot_count = project_config.option<ConfigOptionStrings>("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<ConfigOptionBools>("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<ConfigOptionStrings>("filament_colour");
return colors->values.size() - num_mixed_filaments();
}
std::vector<size_t> 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;
}
+1 -1
View File
@@ -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;
+148 -1
View File
@@ -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<std::string> colors = { "#000000", "#FFFFFF", "#5E5C64" };
config.option<ConfigOptionStrings>("filament_colour")->values = colors;
config.option<ConfigOptionBool>("single_extruder_multi_material")->value = true;
config.option<ConfigOptionFloats>("filament_diameter")->values = { 1.75, 1.75, 1.75 };
config.option<ConfigOptionStrings>("filament_settings_id", true)->values = { "Test PETG", "Test PLA", "Test TPU" };
const std::vector<std::string> bool_keys = {
"filament_is_mixed", "filament_mixed_gradient", "filament_mixed_gradient_per_part"
};
const std::vector<std::string> 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<ConfigOptionBools>(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<ConfigOptionStrings>(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<ConfigOptionBools>(key)->values ==
std::vector<unsigned char>(colors.size(), false));
}
for (const auto &key : string_keys) {
CAPTURE(key, metadata_size);
CHECK(bundle.project_config.option<ConfigOptionStrings>(key)->values ==
std::vector<std::string>(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<ConfigOptionStrings>("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<ConfigOptionBool>("single_extruder_multi_material")->value = true;
config.option<ConfigOptionFloats>("filament_diameter")->values = { 1.75, 1.75, 1.75 };
config.option<ConfigOptionStrings>("filament_colour")->values = { "#000000", "#FFFFFF", "#808080" };
config.option<ConfigOptionBools>("filament_is_mixed")->values = { false, false, true };
config.option<ConfigOptionStrings>("filament_mixed_components")->values = { "", "", "1,2" };
config.option<ConfigOptionStrings>("filament_mixed_sublayer_ratios")->values = { "", "", "1,2" };
config.option<ConfigOptionBools>("filament_mixed_gradient")->values = { false, false, true };
config.option<ConfigOptionStrings>("filament_mixed_gradient_range")->values = { "", "", "0,100" };
config.option<ConfigOptionStrings>("filament_mixed_gradient_curve")->values = { "", "", "0,0.1|1,0.9" };
config.option<ConfigOptionBools>("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<ConfigOptionStrings>("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<ConfigOptionBools>("filament_is_mixed");
flags->values = GENERATE(std::vector<unsigned char>{}, std::vector<unsigned char>{ false },
std::vector<unsigned char>{ false, false, false, true });
const std::vector<std::string> string_keys = {
"filament_mixed_components", "filament_mixed_sublayer_ratios",
"filament_mixed_gradient_range", "filament_mixed_gradient_curve"
};
const std::vector<std::string> bool_keys = {
"filament_mixed_gradient", "filament_mixed_gradient_per_part"
};
for (const auto &key : string_keys)
bundle.project_config.option<ConfigOptionStrings>(key)->values = { "", "", "", "stale" };
for (const auto &key : bool_keys)
bundle.project_config.option<ConfigOptionBools>(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<ConfigOptionStrings>(key)->values ==
std::vector<std::string>{ "", "", "", "" });
}
for (const auto &key : bool_keys) {
CAPTURE(key);
CHECK(bundle.project_config.option<ConfigOptionBools>(key)->values ==
std::vector<unsigned char>{ 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