Fixes filament import bug

This commit is contained in:
Lam Wei Lun
2026-08-31 13:57:21 +08:00
parent 6ffb20a7f5
commit fff0efdb27
2 changed files with 173 additions and 11 deletions

View File

@@ -5979,6 +5979,22 @@ void PresetBundle::load_config_file_config(const std::string& name_or_path,
if (this->is_mixed_filament(i))
mixed_final_slots.insert(int(i));
const size_t mixed_final_slot_count = this->filament_presets.size();
// Finalize a slot as an empty mixed-filament placeholder: mark it virtual with
// an intentionally empty definition, and add it to the final-layout set so a
// later entry's components validate against the new state. Used by the capacity
// placeholders and by any mixed definition that has to be rejected after its
// slot was already grown and seeded - without it such a slot would keep the
// seeded preset and masquerade as a real filament. No definition is written:
// the placeholder carries no material of its own (the GUI flags the empty mix
// via check_mixed_filament_integrity and blocks slicing until components are
// assigned). The slot's colour was already seeded by the growth pass above.
auto finalize_mixed_placeholder = [&](size_t slot_idx) {
if (ConfigOptionBools* is_mixed_opt = this->project_config.opt<ConfigOptionBools>("filament_is_mixed");
is_mixed_opt != nullptr && slot_idx < is_mixed_opt->values.size())
is_mixed_opt->values[slot_idx] = true;
mixed_final_slots.insert(int(slot_idx));
material_applied = true;
};
// Full Publish within-load dedup: identical Full materials (same setting_id
// + preset_name identity) share one created instance, so an author who
// pointed two slots at one preset yields one standalone copy here.
@@ -5996,10 +6012,7 @@ void PresetBundle::load_config_file_config(const std::string& name_or_path,
// is detached: the placeholder carries no material of its own. The colour
// was already seeded into the project arrays by the growth pass above.
if (entry.mixed_placeholder) {
if (ConfigOptionBools* is_mixed_opt = this->project_config.opt<ConfigOptionBools>("filament_is_mixed");
is_mixed_opt != nullptr && slot < is_mixed_opt->values.size())
is_mixed_opt->values[slot] = true;
material_applied = true;
finalize_mixed_placeholder(slot);
continue;
}
@@ -6007,8 +6020,19 @@ void PresetBundle::load_config_file_config(const std::string& name_or_path,
// find_preset would return &m_edited_preset for the selected slot. The
// overlay target below decides between the edited layer and this preset.
Preset* recv = this->filaments.find_preset(this->filament_presets[slot], false, true);
if (recv == nullptr)
if (recv == nullptr) {
// Defensive: the slot exists (grown and seeded above) but its preset
// could not be resolved. A mixed definition left unapplied on such a
// slot would keep the seeded preset and look like a real filament:
// finalize it as an empty placeholder instead, like a rejected
// definition below.
if (is_mixed_definition(entry) && !this->is_mixed_filament(slot)) {
finalize_mixed_placeholder(slot);
published_config->material_replacements.emplace_back("slot " + std::to_string(slot) +
": mixed filament definition could not be imported");
}
continue;
}
const std::string material_label = entry.filament_id.empty() ?
(entry.publish_type_value.empty() ? entry.filament_type :
@@ -6045,6 +6069,23 @@ void PresetBundle::load_config_file_config(const std::string& name_or_path,
skipped_keys.emplace_back("material:" + material_label + " (mixed filament definition: " + mix_error + ")");
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": published 3MF mixed filament from slot " << entry.slot
<< " rejected: " << mix_error;
// Degradation, not silent corruption: the slot was already grown and
// seeded by the pass above, so skipping the definition alone would
// leave it holding the seeded preset and looking like a real
// filament that carries the mix identity but no mix.
// - a slot that was NOT already a receiver mix is finalized as an
// empty mixed placeholder (consistent with the capacity
// placeholders; the user assigns components there);
// - a like-for-like override of the receiver's own mix leaves that
// valid definition untouched: nothing was applied, so the
// receiver's cells are intact and only the author's definition
// is reported as skipped above.
if (!this->is_mixed_filament(slot)) {
finalize_mixed_placeholder(slot);
published_config->material_replacements.emplace_back("slot " + std::to_string(slot) +
": mixed filament definition could not be imported (" +
mix_error + ")");
}
continue;
}
}
@@ -6247,6 +6288,16 @@ void PresetBundle::load_config_file_config(const std::string& name_or_path,
for (const std::string& key : entry.keys)
skipped_keys.emplace_back("material:" + material_label + " (" + key + ")");
apply_slot = false;
// A mixed definition left unapplied here would keep the grown
// slot's seeded preset and masquerade as a real filament:
// finalize it as an empty placeholder like a rejected
// definition (a like-for-like override of the receiver's own
// mix is left untouched).
if (is_mixed_definition(entry) && !this->is_mixed_filament(slot)) {
finalize_mixed_placeholder(slot);
published_config->material_replacements.emplace_back("slot " + std::to_string(slot) +
": mixed filament definition could not be imported");
}
}
}
}

View File

@@ -11,6 +11,7 @@
#include "test_utils.hpp"
#include <algorithm>
#include <iostream>
#include <initializer_list>
using namespace Slic3r;
@@ -3671,9 +3672,11 @@ TEST_CASE("Published 3MF rejects a mixed filament definition with impossible com
Preset::normalize(config);
bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub);
// The definition was rejected, not applied: the slot stays a plain (grown) slot.
// The definition was rejected, not applied: the grown slot is finalized as an empty
// mixed placeholder instead of keeping the seeded preset and masquerading as a real
// filament.
REQUIRE(bundle.filament_presets.size() == 3);
CHECK_FALSE(bundle.is_mixed_filament(2));
CHECK(bundle.is_mixed_filament(2));
CHECK(bundle.project_config.opt<ConfigOptionStrings>("filament_mixed_components")->values[2].empty());
CHECK(bundle.project_config.opt<ConfigOptionStrings>("filament_mixed_sublayer_ratios")->values[2].empty());
// Reported through the shared rejection channel.
@@ -3681,13 +3684,119 @@ TEST_CASE("Published 3MF rejects a mixed filament definition with impossible com
CHECK(contains_key(pub.skipped_keys, "material:GFL99 (mixed filament definition: needs at least two components)"));
else
CHECK(contains_key(pub.skipped_keys, "material:GFL99 (mixed filament definition: components reference missing slots)"));
// The slot change was surfaced like the other slot adaptations.
REQUIRE(pub.material_replacements.size() == 1);
CHECK(pub.material_replacements[0].find("slot 2: mixed filament definition could not be imported") != std::string::npos);
// The blended colour was not written into the (shared) slot preset either.
CHECK(bundle.filaments.find_preset("My PLA", false, true)->config.opt<ConfigOptionStrings>("filament_colour")->values ==
std::vector<std::string>{ "#123456" });
CHECK(pub.material_replacements.empty());
}
}
// The reported scenario: an author publishes two mixed filaments whose components are
// full-published physical slots; the receiver is a smaller tool-changer, so some of those
// component slots become empty mixed placeholders. A mix whose component turned into a
// placeholder can never be valid (mixes cannot reference mixes): it is rejected, and its
// grown slot must be finalized as an empty mixed placeholder too - not keep the seeded
// preset and masquerade as a real filament carrying the mix identity.
TEST_CASE("Published 3MF finalizes a mixed filament rejected over a placeholder component as an empty placeholder", "[Preset][Bundle][Published]")
{
// An author project: six physical slots plus two tail mixes, the second referencing the
// sixth physical slot (H2C-style: slot 7 = 1+2, slot 8 = 2+6).
auto make_file_config = [] {
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
config.opt<ConfigOptionFloats>("filament_diameter")->values = std::vector<double>(8, 1.75);
config.opt<ConfigOptionInts>("filament_self_index")->values = { 1, 2, 3, 4, 5, 6, 7, 8 };
config.opt<ConfigOptionStrings>("filament_extruder_variant")->values = std::vector<std::string>(8, "Direct Drive Standard");
config.opt<ConfigOptionStrings>("filament_colour")->values = { "#FF0000", "#00FF00", "#0000FF", "#FFFF00",
"#FF00FF", "#00FFFF", "#800080", "#804000" };
config.opt<ConfigOptionStrings>("filament_type")->values.assign(8, "PLA");
config.opt<ConfigOptionStrings>("filament_vendor")->values.assign(8, "Generic");
config.opt<ConfigOptionBools>("filament_is_mixed")->values = { 0, 0, 0, 0, 0, 0, 1, 1 };
config.opt<ConfigOptionStrings>("filament_mixed_components")->values = { "", "", "", "", "", "", "1,2", "2,6" };
config.opt<ConfigOptionStrings>("filament_mixed_sublayer_ratios")->values = { "", "", "", "", "", "", "0.6,0.4", "0.5,0.5" };
// The export always serializes all seven masked mixed arrays, not just the ones in
// use; the unused gradient arrays ride along as defaults.
config.opt<ConfigOptionBools>("filament_mixed_gradient")->values = { 0, 0, 0, 0, 0, 0, 0, 0 };
config.opt<ConfigOptionStrings>("filament_mixed_gradient_range")->values.assign(8, "");
config.opt<ConfigOptionStrings>("filament_mixed_gradient_curve")->values.assign(8, "");
config.opt<ConfigOptionBools>("filament_mixed_gradient_per_part")->values = { 0, 0, 0, 0, 0, 0, 0, 0 };
return config;
};
// A non-SEMM receiver with four nozzles and four slots (tool-changer style).
PresetBundle bundle;
Preset &pla = add_inmemory_preset(bundle.filaments, "My PLA");
pla.config.opt_string("filament_type", 0u) = "PLA";
bundle.filament_presets.assign(4, "My PLA");
bundle.set_num_filaments(4, "#123456");
auto &printer_config = bundle.printers.get_edited_preset().config;
printer_config.opt<ConfigOptionBool>("single_extruder_multi_material", true)->value = false;
printer_config.opt<ConfigOptionFloats>("nozzle_diameter", true)->values.assign(4, 0.4);
auto make_full_entry = [](int slot) {
PublishedMaterialEntry entry;
entry.slot = slot;
entry.filament_type = "PLA";
entry.filament_vendor = "Generic";
entry.full = true;
entry.full_keys = { "filament_retraction_length" };
return entry;
};
auto make_mix_entry = [](int slot, const char *color) {
PublishedMaterialEntry entry;
entry.slot = slot;
entry.filament_type = "PLA";
entry.filament_vendor = "Generic";
entry.publish_color = true;
entry.color = color;
entry.keys = { "filament_is_mixed", "filament_mixed_components",
"filament_mixed_sublayer_ratios", "filament_mixed_gradient",
"filament_mixed_gradient_range", "filament_mixed_gradient_curve",
"filament_mixed_gradient_per_part" };
return entry;
};
// The dialog's emit order: the full-published physical slots (1, 2, 5, 6) and both mixes.
PublishedConfig pub;
pub.published = true;
pub.material_keys = { make_full_entry(0), make_full_entry(1), make_full_entry(4), make_full_entry(5),
make_mix_entry(6, "#800080"), make_mix_entry(7, "#804000") };
DynamicPrintConfig config = make_file_config();
Preset::normalize(config);
bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub);
// The receiver grew to the author's slot count, all virtual territory at the tail.
REQUIRE(bundle.filament_presets.size() == 8);
const auto &is_mixed = bundle.project_config.opt<ConfigOptionBools>("filament_is_mixed")->values;
REQUIRE(is_mixed.size() == 8);
// Slots 0-3 stayed physical; authored slots 5 and 6 (0-based 4 and 5) became capacity
// placeholders.
for (size_t i = 0; i < 4; ++i)
CHECK_FALSE(is_mixed[i]);
CHECK(is_mixed[4]);
CHECK(is_mixed[5]);
CHECK(bundle.project_config.opt<ConfigOptionStrings>("filament_mixed_components")->values[4].empty());
CHECK(bundle.project_config.opt<ConfigOptionStrings>("filament_mixed_components")->values[5].empty());
// The first mix applied onto its uncontended tail slot.
CHECK(is_mixed[6]);
CHECK(bundle.project_config.opt<ConfigOptionStrings>("filament_mixed_components")->values[6] == "1,2");
// The second mix was rejected - its second component (authored slot 6) turned into a
// placeholder - and its slot was finalized as an empty mixed placeholder instead of
// keeping the seeded preset as a phantom real filament.
CHECK(is_mixed[7]);
CHECK(bundle.project_config.opt<ConfigOptionStrings>("filament_mixed_components")->values[7].empty());
CHECK(bundle.project_config.opt<ConfigOptionStrings>("filament_mixed_sublayer_ratios")->values[7].empty());
REQUIRE(pub.skipped_keys.size() == 1);
CHECK(pub.skipped_keys[0] == "material:PLA (mixed filament definition: components reference missing slots)");
// Two Full Publish detach lines (slots 0-1), two capacity placeholder lines (slots 4-5),
// and the rejected mix's finalization line (slot 7).
REQUIRE(pub.material_replacements.size() == 5);
CHECK(std::any_of(pub.material_replacements.begin(), pub.material_replacements.end(),
[](const std::string &line) {
return line.find("slot 7: mixed filament definition could not be imported") != std::string::npos;
}));
}
// The relocation shifts cells inside the file's per-slot mixed arrays; a payload too short to
// actually carry the definition degrades to empty cells, which the definition validation then
// reports - an empty mix must not ship as a virtual slot.
@@ -3734,15 +3843,17 @@ TEST_CASE("Published 3MF reports a relocated mixed filament whose payload cells
// The mix was relocated past the physical territory...
REQUIRE(pub.mixed_slot_relocations.size() == 1);
CHECK(pub.mixed_slot_relocations.at(3) == 5);
REQUIRE(pub.material_replacements.size() == 1);
REQUIRE(pub.material_replacements.size() == 2);
CHECK(pub.material_replacements[0].find("slot 3 -> slot 5") != std::string::npos);
// ...and the receiver grew to hold the destination slot, but the definition itself was
// rejected: the relocated cells degraded to empty defaults and were reported.
// rejected: the relocated cells degraded to empty defaults, the slot was finalized as an
// empty mixed placeholder (not a real filament), and both facts were reported.
REQUIRE(bundle.filament_presets.size() == 6);
CHECK_FALSE(bundle.is_mixed_filament(5));
CHECK(bundle.is_mixed_filament(5));
CHECK(bundle.project_config.opt<ConfigOptionStrings>("filament_mixed_components")->values[5].empty());
CHECK(bundle.project_config.opt<ConfigOptionStrings>("filament_mixed_sublayer_ratios")->values[5].empty());
CHECK(contains_key(pub.skipped_keys, "material:GFL99 (mixed filament definition: needs at least two components)"));
CHECK(pub.material_replacements[1].find("slot 5: mixed filament definition could not be imported") != std::string::npos);
// The five real slots kept their colours.
CHECK(std::equal(receiver_colours.begin(), receiver_colours.end(),
bundle.project_config.opt<ConfigOptionStrings>("filament_colour")->values.begin()));