mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-11 02:57:39 +00:00
Fix issues with filament import when receiver has fewer filament slots than the author's 3MF format
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user