Fix issues with filament import when receiver has fewer filament slots than the author's 3MF format

This commit is contained in:
Lam Wei Lun
2026-08-18 15:11:00 +08:00
parent 6aab2b22a1
commit b82d4f3af8
2 changed files with 217 additions and 11 deletions

View File

@@ -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<int> 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<std::string> 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<int>(new_slot_idx) && entry.publish_type && !entry.publish_type_value.empty()) {
if (published_slots.count(static_cast<int>(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<int>(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<int>(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<int>(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<ConfigOptionStrings>("filament_colour");
ConfigOptionStrings *proj_multi_colour = this->project_config.opt<ConfigOptionStrings>("filament_multi_colour");
ConfigOptionStrings *proj_colour_type = this->project_config.opt<ConfigOptionStrings>("filament_colour_type");
ConfigOptionInts *proj_map = this->project_config.opt<ConfigOptionInts>("filament_map");
ConfigOptionInts *proj_nozzle_map = this->project_config.opt<ConfigOptionInts>("filament_nozzle_map");
ConfigOptionInts *proj_volume_map = this->project_config.opt<ConfigOptionInts>("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<int>(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<int>(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<ConfigOptionStrings>("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<std::string> &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;

View File

@@ -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<ConfigOptionStrings>("filament_colour")->values == std::vector<std::string>{ "#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<ConfigOptionStrings>("filament_colour")->values.size() == 4);
CHECK(bundle.project_config.opt<ConfigOptionStrings>("filament_colour")->values[1] == "#123456");
CHECK(bundle.project_config.opt<ConfigOptionStrings>("filament_colour")->values[3] == "#ABCDEF");
CHECK(bundle.project_config.opt<ConfigOptionStrings>("filament_multi_colour")->values.size() == 4);
CHECK(bundle.project_config.opt<ConfigOptionStrings>("filament_colour_type")->values.size() == 4);
CHECK(bundle.project_config.opt<ConfigOptionInts>("filament_map")->values.size() == 4);
CHECK(bundle.project_config.opt<ConfigOptionFloats>("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<ConfigOptionFloats>("filament_diameter")->values = { 1.75, 1.75, 1.75, 1.75 };
config.opt<ConfigOptionInts>("filament_self_index")->values = { 1, 2, 3, 4 };
config.opt<ConfigOptionStrings>("filament_extruder_variant")->values = { "Direct Drive Standard", "Direct Drive Standard", "Direct Drive Standard", "Direct Drive Standard" };
config.opt<ConfigOptionStrings>("filament_colour")->values = { "#FF0000", "#00FF00", "#0000FF", "#FFFF00" };
config.opt<ConfigOptionStrings>("filament_type")->values = { "PLA", "PLA", "PLA", "PLA" };
config.opt<ConfigOptionStrings>("filament_vendor")->values = { "Generic", "Generic", "Generic", "Generic" };
config.opt<ConfigOptionStrings>("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<ConfigOptionStrings>("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<ConfigOptionStrings>("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<ConfigOptionStrings>("filament_colour")->values == std::vector<std::string>{ "#123456" });
CHECK(bundle.filaments.find_preset("Other PLA", false, true)->config.opt<ConfigOptionStrings>("filament_colour")->values == std::vector<std::string>{ "#ABCDEF" });
// The project-level colours are sized and seeded for every grown slot.
CHECK(bundle.project_config.opt<ConfigOptionStrings>("filament_colour")->values.size() == 4);
CHECK(bundle.project_config.opt<ConfigOptionStrings>("filament_colour")->values[1] == "#123456");
CHECK(bundle.project_config.opt<ConfigOptionStrings>("filament_colour")->values[3] == "#ABCDEF");
CHECK(bundle.project_config.opt<ConfigOptionStrings>("filament_multi_colour")->values.size() == 4);
CHECK(bundle.project_config.opt<ConfigOptionStrings>("filament_colour_type")->values.size() == 4);
CHECK(bundle.project_config.opt<ConfigOptionInts>("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