diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp index c58e6c30e7..fd3d4d24b6 100644 --- a/src/libslic3r/PresetBundle.cpp +++ b/src/libslic3r/PresetBundle.cpp @@ -5047,15 +5047,30 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool if (has_new_semantics) { // Defensive cap: never exceed the file's own filament count. target_slots = std::min(target_slots, num_filaments); + // Slots that carry published content (full/type/colour) must reference a stored + // preset that no other slot shares: the overlay mutates stored presets in place + // (colour and keys), so a shared preset would leak one slot's published values + // into every slot that references it. + std::set published_slots; + for (const PublishedMaterialEntry &entry : published_config->material_keys) + if ((entry.full || entry.publish_type || entry.publish_color) && entry.slot >= 0) + published_slots.insert(entry.slot); + std::set used_preset_names(this->filament_presets.begin(), this->filament_presets.end()); + // Mirror first_visible_idx()'s start index so suppressed default presets are + // never picked as a slot material. + const size_t first_candidate = this->filaments.is_default_suppressed() ? this->filaments.num_default_presets() : 0; while (this->filament_presets.size() < target_slots) { const size_t new_slot_idx = this->filament_presets.size(); std::string initial_preset; - // Proactively assign matching candidate preset if this slot carries a published type - for (const PublishedMaterialEntry &entry : published_config->material_keys) { - if (entry.slot == static_cast(new_slot_idx) && entry.publish_type && !entry.publish_type_value.empty()) { + if (published_slots.count(static_cast(new_slot_idx)) != 0) { + // Proactively assign a distinct matching candidate preset if this slot + // carries a published type... + for (const PublishedMaterialEntry &entry : published_config->material_keys) { + if (entry.slot != static_cast(new_slot_idx) || !entry.publish_type || entry.publish_type_value.empty()) + continue; for (size_t i = 0; i < this->filaments.size(); ++i) { const Preset &candidate = this->filaments.preset(i); - if (!candidate.is_visible) + if (!candidate.is_visible || used_preset_names.count(candidate.name) != 0) continue; if (normalize_filament_type(candidate.config.opt_string("filament_type", 0u)) == entry.publish_type_value) { initial_preset = candidate.name; @@ -5064,11 +5079,114 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool } break; } + // ...otherwise any visible preset not already used by another slot. + if (initial_preset.empty()) { + for (size_t i = first_candidate; i < this->filaments.size(); ++i) { + const Preset &candidate = this->filaments.preset(i); + if (!candidate.is_visible || used_preset_names.count(candidate.name) != 0) + continue; + initial_preset = candidate.name; + break; + } + } } if (initial_preset.empty()) - initial_preset = this->filaments.first_visible().name; + // Unpublished filler slot, or every visible preset is already used: repeat + // the receiver's last preset, mirroring the "Add one filament" behaviour + // (PresetBundle::set_num_filaments). + initial_preset = this->filament_presets.empty() ? this->filaments.first_visible().name + : this->filament_presets.back(); this->filament_presets.emplace_back(initial_preset); + used_preset_names.insert(initial_preset); } + // Slots that were grown before this block (e.g. by update_multi_material_filament_presets + // matching the extruder count) may still alias another slot; re-point them at a + // distinct preset. Slot 0, the receiver's own material, is never re-assigned. + for (size_t slot = 1; slot < this->filament_presets.size(); ++slot) { + if (published_slots.count(static_cast(slot)) == 0) + continue; + bool shared = false; + for (size_t other = 0; other < this->filament_presets.size(); ++other) + if (other != slot && this->filament_presets[other] == this->filament_presets[slot]) { + shared = true; + break; + } + if (!shared) + continue; + std::string replacement; + for (const PublishedMaterialEntry &entry : published_config->material_keys) { + if (entry.slot != static_cast(slot) || !entry.publish_type || entry.publish_type_value.empty()) + continue; + for (size_t i = 0; i < this->filaments.size(); ++i) { + const Preset &candidate = this->filaments.preset(i); + if (!candidate.is_visible || used_preset_names.count(candidate.name) != 0) + continue; + if (normalize_filament_type(candidate.config.opt_string("filament_type", 0u)) == entry.publish_type_value) { + replacement = candidate.name; + break; + } + } + break; + } + if (replacement.empty()) { + for (size_t i = first_candidate; i < this->filaments.size(); ++i) { + const Preset &candidate = this->filaments.preset(i); + if (!candidate.is_visible || used_preset_names.count(candidate.name) != 0) + continue; + replacement = candidate.name; + break; + } + } + if (replacement.empty()) + continue; // every visible preset is used: aliasing is unavoidable + used_preset_names.erase(this->filament_presets[slot]); + this->filament_presets[slot] = replacement; + used_preset_names.insert(replacement); + } + // Mirror set_num_filaments' project_config vector handling ("Add one filament"): + // resize the per-slot colour/type/map vectors to the grown slot count and seed the + // new entries so the slots render with colours instead of blank chips. Only the + // new entries are seeded; the receiver's existing values are left untouched. + ConfigOptionStrings *proj_colour = this->project_config.opt("filament_colour"); + ConfigOptionStrings *proj_multi_colour = this->project_config.opt("filament_multi_colour"); + ConfigOptionStrings *proj_colour_type = this->project_config.opt("filament_colour_type"); + ConfigOptionInts *proj_map = this->project_config.opt("filament_map"); + ConfigOptionInts *proj_nozzle_map = this->project_config.opt("filament_nozzle_map"); + ConfigOptionInts *proj_volume_map = this->project_config.opt("filament_volume_map"); + const size_t old_colour_count = (proj_colour != nullptr) ? proj_colour->values.size() : 0; + if (proj_colour) proj_colour->resize(target_slots); + if (proj_multi_colour) proj_multi_colour->values.resize(target_slots); + if (proj_colour_type) proj_colour_type->values.resize(target_slots); + if (proj_map) proj_map->values.resize(target_slots, 1); + if (proj_nozzle_map) proj_nozzle_map->values.resize(target_slots, 0); + if (proj_volume_map) proj_volume_map->values.resize(target_slots, static_cast(NozzleVolumeType::nvtStandard)); + this->ams_multi_color_filment.resize(target_slots); + for (size_t slot = old_colour_count; slot < target_slots; ++slot) { + std::string seed; + for (const PublishedMaterialEntry &entry : published_config->material_keys) + if (entry.slot == static_cast(slot) && entry.publish_color && !entry.color.empty()) { + seed = entry.color; + break; + } + if (seed.empty()) { + if (const Preset *preset = this->filaments.find_preset(this->filament_presets[slot], false)) { + const ConfigOptionStrings *colours = preset->config.opt("filament_colour"); + if (colours != nullptr && !colours->values.empty()) + seed = colours->values.front(); + } + if (seed.empty()) + seed = "#F2754E"; // filament_colour default + } + if (proj_colour && slot < proj_colour->values.size()) + proj_colour->values[slot] = seed; + if (proj_multi_colour && slot < proj_multi_colour->values.size()) + proj_multi_colour->values[slot] = seed; + if (proj_colour_type && slot < proj_colour_type->values.size()) + proj_colour_type->values[slot] = "1"; // default colour type + } + // Rebuild the flush volumes for the grown slot count (set_num_filaments does the + // same; without it the matrix would stay at the receiver's old size). + this->update_multi_material_filament_presets(); auto apply_slot_keys = [&](Preset &preset, const std::vector &slot_keys, int author_slot, const std::string &material_label) { @@ -5124,17 +5242,31 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool apply_slot = false; } else { // Type mismatch: replace the slot with the first visible same-type - // filament from the receiver's library. - std::string replacement; + // filament from the receiver's library, preferring one that no other + // slot references (a shared stored preset would leak this slot's + // published values into that slot). + std::string replacement, first_same_type; for (size_t i = 0; i < this->filaments.size(); ++i) { const Preset &candidate = this->filaments.preset(i); if (!candidate.is_visible) continue; if (normalize_filament_type(candidate.config.opt_string("filament_type", 0u)) != entry.publish_type_value) continue; - replacement = candidate.name; - break; + if (first_same_type.empty()) + first_same_type = candidate.name; + bool used_elsewhere = false; + for (size_t s = 0; s < this->filament_presets.size(); ++s) + if (s != slot && this->filament_presets[s] == candidate.name) { + used_elsewhere = true; + break; + } + if (!used_elsewhere) { + replacement = candidate.name; + break; + } } + if (replacement.empty()) + replacement = first_same_type; if (!replacement.empty()) { const std::string old_name = recv->name; this->filament_presets[slot] = replacement; diff --git a/tests/libslic3r/test_preset_bundle_loading.cpp b/tests/libslic3r/test_preset_bundle_loading.cpp index f1d2c79f95..8716af417d 100644 --- a/tests/libslic3r/test_preset_bundle_loading.cpp +++ b/tests/libslic3r/test_preset_bundle_loading.cpp @@ -1069,8 +1069,10 @@ TEST_CASE("Published 3MF grows the receiver's slots only as far as the published CHECK(bundle.filaments.find_preset("My PLA")->config.opt("filament_colour")->values == std::vector{ "#ABCDEF" }); } - // Slot 3 published: the receiver grows to 4 so the published slot exists; the filler is - // the receiver's own first visible material. + // Slot 3 published: the receiver grows to 4 so the published slot exists. The unpublished + // filler slots repeat the receiver's last preset ("Add one filament" behaviour); the + // published slot gets a visible preset not used by another slot (with a single-preset + // library it falls back to the receiver's last preset, aliasing being unavoidable). { PresetBundle bundle; add_pla_preset(bundle); @@ -1085,7 +1087,18 @@ TEST_CASE("Published 3MF grows the receiver's slots only as far as the published bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub); REQUIRE(bundle.filament_presets.size() == 4); + CHECK(bundle.filament_presets[1] == "My PLA"); + CHECK(bundle.filament_presets[2] == "My PLA"); CHECK(bundle.filament_presets[3] == filler); + // The project-level per-slot vectors were grown and seeded like "Add one filament": + // fillers take their preset's colour, the published slot its published colour. + CHECK(bundle.project_config.opt("filament_colour")->values.size() == 4); + CHECK(bundle.project_config.opt("filament_colour")->values[1] == "#123456"); + CHECK(bundle.project_config.opt("filament_colour")->values[3] == "#ABCDEF"); + CHECK(bundle.project_config.opt("filament_multi_colour")->values.size() == 4); + CHECK(bundle.project_config.opt("filament_colour_type")->values.size() == 4); + CHECK(bundle.project_config.opt("filament_map")->values.size() == 4); + CHECK(bundle.project_config.opt("flush_volumes_matrix")->values.size() == 16); } // Slots 0 and 2 published: the receiver grows to 3, never to the file's 4. @@ -1102,9 +1115,70 @@ TEST_CASE("Published 3MF grows the receiver's slots only as far as the published bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub); REQUIRE(bundle.filament_presets.size() == 3); + CHECK(bundle.filament_presets[1] == "My PLA"); } } +// The published overlay mutates stored filament presets in place per slot, so a slot carrying +// published content must never share its stored preset with another slot: its colour/keys +// would leak into the sibling slot - and, with "repeat the last preset" growth, into the +// receiver's own first slot. Regression for the slot-aliasing hazard. +TEST_CASE("Published 3MF gives grown published slots a distinct preset so values never leak", "[Preset][Bundle][Published]") +{ + auto make_file_config = [] { + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.opt("filament_diameter")->values = { 1.75, 1.75, 1.75, 1.75 }; + config.opt("filament_self_index")->values = { 1, 2, 3, 4 }; + config.opt("filament_extruder_variant")->values = { "Direct Drive Standard", "Direct Drive Standard", "Direct Drive Standard", "Direct Drive Standard" }; + config.opt("filament_colour")->values = { "#FF0000", "#00FF00", "#0000FF", "#FFFF00" }; + config.opt("filament_type")->values = { "PLA", "PLA", "PLA", "PLA" }; + config.opt("filament_vendor")->values = { "Generic", "Generic", "Generic", "Generic" }; + config.opt("filament_ids")->values = { "GFL99", "GFL99", "GFL99", "GFL99" }; + return config; + }; + + // The receiver has one slot of its own material plus one more preset in the library; the + // author publishes only slot 4 (Red). With naive repeat-last growth the new slot would + // reference the receiver's own preset and the published red would recolor it; the grown + // slot must point at a distinct preset. + PresetBundle bundle; + Preset &mine = add_inmemory_preset(bundle.filaments, "My PLA"); + mine.config.opt_string("filament_type", 0u) = "PLA"; + mine.config.opt("filament_colour", true)->values = { "#123456" }; + Preset &other = add_inmemory_preset(bundle.filaments, "Other PLA"); + other.config.opt_string("filament_type", 0u) = "PLA"; + other.config.opt("filament_colour", true)->values = { "#654321" }; + bundle.filament_presets = { "My PLA" }; + + PublishedMaterialEntry entry; + entry.slot = 3; + entry.publish_color = true; + entry.color = "#ABCDEF"; + PublishedConfig pub; + pub.published = true; + pub.material_keys = { entry }; + DynamicPrintConfig config = make_file_config(); + Preset::normalize(config); + bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub); + + REQUIRE(bundle.filament_presets.size() == 4); + // Unpublished filler slots repeat the receiver's last preset ("Add one filament"). + CHECK(bundle.filament_presets[1] == "My PLA"); + CHECK(bundle.filament_presets[2] == "My PLA"); + // The published slot references the unused library preset, not the receiver's own... + CHECK(bundle.filament_presets[3] == "Other PLA"); + // ...so the published colour landed there and never recoloured the receiver's material. + CHECK(bundle.filaments.find_preset("My PLA", false, true)->config.opt("filament_colour")->values == std::vector{ "#123456" }); + CHECK(bundle.filaments.find_preset("Other PLA", false, true)->config.opt("filament_colour")->values == std::vector{ "#ABCDEF" }); + // The project-level colours are sized and seeded for every grown slot. + CHECK(bundle.project_config.opt("filament_colour")->values.size() == 4); + CHECK(bundle.project_config.opt("filament_colour")->values[1] == "#123456"); + CHECK(bundle.project_config.opt("filament_colour")->values[3] == "#ABCDEF"); + CHECK(bundle.project_config.opt("filament_multi_colour")->values.size() == 4); + CHECK(bundle.project_config.opt("filament_colour_type")->values.size() == 4); + CHECK(bundle.project_config.opt("filament_map")->values.size() == 4); +} + // The GUI displays the edited preset, a snapshot of the selected collection preset taken at // selection time. The published overlay modifies the collection presets in place, so the load // must re-select the first slot's filament (mirroring a normal project load) for the applied