mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-10 18:56:13 +00:00
Fixes issue where user imports a 3MF file where filament slots exceeds the maximum number of slots that user has on its printer
This commit is contained in:
@@ -5407,6 +5407,11 @@ void PresetBundle::load_config_file_config(const std::string& name_or_path,
|
||||
// preset no other slot references wins on equal scores; with no replacement
|
||||
// available the receiver's material is kept and the keys are reported as skipped;
|
||||
// - colour: applied to the slot regardless of the type gate.
|
||||
// - capacity: on a non-SEMM receiver whose printer has fewer nozzles than the
|
||||
// authored slot needs, the entry becomes an empty mixed-filament placeholder
|
||||
// appended at the tail (the GUI flags it; the user assigns components from their
|
||||
// own filaments); on a single-physical-slot receiver it is dropped and reported
|
||||
// instead, since an empty mix could never be edited there.
|
||||
// Applied partial values land on the collection's edited layer when the slot references
|
||||
// it and that layer survives the load (visible as a modification, revertible, the user's
|
||||
// unsaved edits preserved), otherwise on the stored preset in place.
|
||||
@@ -5417,11 +5422,38 @@ void PresetBundle::load_config_file_config(const std::string& name_or_path,
|
||||
// Grow the receiver's slots only as far as the highest published slot (never
|
||||
// shrink, never pull filler materials for unpublished slots).
|
||||
bool has_published_entries = false;
|
||||
size_t grow_target = 0;
|
||||
// Physical filament capacity of the receiver's printer: a non-SEMM tool-changer
|
||||
// feeds filament N from nozzle N, so the nozzle count is the hard limit; a SEMM
|
||||
// printer (single_extruder_multi_material) sizes its slot list by hand, so only
|
||||
// the global slot limit applies (same condition as GUI_App::load_current_presets).
|
||||
// Published entries that would need a NEW physical slot past this capacity are
|
||||
// appended as empty mixed-filament placeholders instead of growing the list.
|
||||
size_t physical_capacity = size_t(EnforcerBlockerType::ExtruderMax);
|
||||
{
|
||||
const Preset& receiver_printer = this->printers.get_edited_preset();
|
||||
if (receiver_printer.printer_technology() == ptFFF &&
|
||||
!receiver_printer.config.opt_bool("single_extruder_multi_material")) {
|
||||
if (const auto* nozzle_diameter = receiver_printer.config.option<ConfigOptionFloats>("nozzle_diameter");
|
||||
nozzle_diameter != nullptr && !nozzle_diameter->values.empty())
|
||||
physical_capacity = nozzle_diameter->values.size();
|
||||
}
|
||||
}
|
||||
const std::set<std::string>& mixed_definitions = publish_mixed_keys();
|
||||
auto is_mixed_definition = [&mixed_definitions](const PublishedMaterialEntry& entry) {
|
||||
return std::any_of(entry.keys.begin(), entry.keys.end(), [&](const std::string& key) {
|
||||
return mixed_definitions.count(publish_base_key(key)) != 0;
|
||||
});
|
||||
};
|
||||
size_t grow_target = 0;
|
||||
for (const PublishedMaterialEntry& entry : published_config->material_keys) {
|
||||
has_published_entries = true;
|
||||
if (entry.slot >= 0)
|
||||
grow_target = std::max(grow_target, size_t(entry.slot) + 1);
|
||||
if (entry.slot < 0)
|
||||
continue;
|
||||
// A physical entry past the capacity becomes a tail placeholder below; its
|
||||
// growth is covered by the append counter, not the positional target.
|
||||
if (!is_mixed_definition(entry) && size_t(entry.slot) >= physical_capacity)
|
||||
continue;
|
||||
grow_target = std::max(grow_target, size_t(entry.slot) + 1);
|
||||
}
|
||||
// Mixed-filament definitions live in project-level virtual slots, so applying one
|
||||
// positionally onto a receiver slot that holds a real, physical filament would
|
||||
@@ -5439,13 +5471,12 @@ void PresetBundle::load_config_file_config(const std::string& name_or_path,
|
||||
// from the new index. No existing slot changes meaning.
|
||||
// - destinations are also capped: appends past the extruder limit are dropped
|
||||
// and reported instead of being forced onto a physical filament.
|
||||
const std::set<std::string>& mixed_definitions = publish_mixed_keys();
|
||||
auto is_mixed_definition = [&mixed_definitions](const PublishedMaterialEntry& entry) {
|
||||
return std::any_of(entry.keys.begin(), entry.keys.end(), [&](const std::string& key) {
|
||||
return mixed_definitions.count(publish_base_key(key)) != 0;
|
||||
});
|
||||
};
|
||||
size_t next_free_slot = this->filament_presets.size();
|
||||
// Physical entries past the printer's capacity join the same append counter
|
||||
// (dest = next_free, packed consecutively at the tail - never max(authored,
|
||||
// next_free), which would grow filler physical slots past the capacity) and are
|
||||
// flagged mixed_placeholder: they become empty mixed-filament placeholders the
|
||||
// GUI flags for the user to assign components to.
|
||||
size_t next_free_slot = this->filament_presets.size();
|
||||
bool any_mixed_relocated = false;
|
||||
// All authored-slot -> destination moves decided by this pass, applied to the
|
||||
// incoming config in one batched snapshot step below (an earlier move's
|
||||
@@ -5455,42 +5486,95 @@ void PresetBundle::load_config_file_config(const std::string& name_or_path,
|
||||
std::vector<std::pair<size_t, size_t>> mixed_moves;
|
||||
for (auto entry_it = published_config->material_keys.begin(); entry_it != published_config->material_keys.end();) {
|
||||
PublishedMaterialEntry& entry = *entry_it;
|
||||
if (entry.slot < 0 || !is_mixed_definition(entry) ||
|
||||
// Like-for-like override of a virtual receiver slot (bounds-checked).
|
||||
this->is_mixed_filament(size_t(entry.slot))) {
|
||||
if (entry.slot < 0) {
|
||||
++entry_it;
|
||||
continue;
|
||||
}
|
||||
const bool is_payload_mix = is_mixed_definition(entry);
|
||||
// Does this entry need a tail slot at all? A payload mixed definition does,
|
||||
// except when it like-for-like overrides a receiver slot that is already a
|
||||
// mix (bounds-checked). A physical entry only becomes a placeholder when it
|
||||
// would need a NEW physical slot: an authored position the receiver's list
|
||||
// already covers is applied positionally as before, even when that list sits
|
||||
// above the printer's capacity (pre-existing state is never shrunk).
|
||||
bool keep_place = false;
|
||||
if (is_payload_mix)
|
||||
keep_place = this->is_mixed_filament(size_t(entry.slot));
|
||||
else
|
||||
keep_place = size_t(entry.slot) < this->filament_presets.size() ||
|
||||
size_t(entry.slot) < physical_capacity;
|
||||
if (keep_place) {
|
||||
++entry_it;
|
||||
continue;
|
||||
}
|
||||
const std::string material_label = !entry.filament_id.empty() ? entry.filament_id :
|
||||
!entry.publish_type_value.empty() ? entry.publish_type_value :
|
||||
entry.filament_type;
|
||||
if (std::max(size_t(entry.slot), next_free_slot) >= size_t(EnforcerBlockerType::ExtruderMax)) {
|
||||
// No free virtual slot left: report instead of destroying a real filament.
|
||||
const std::string material_label = !entry.filament_id.empty() ? entry.filament_id :
|
||||
!entry.publish_type_value.empty() ? entry.publish_type_value :
|
||||
entry.filament_type;
|
||||
// The local skipped_keys is published wholesale at the end of the pass;
|
||||
// writing published_config->skipped_keys here would be clobbered by it.
|
||||
skipped_keys.emplace_back("material:" + material_label +
|
||||
" (mixed filament definition: filament slot limit reached)");
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": published 3MF mixed filament from slot " << entry.slot
|
||||
<< " could not be placed: all " << next_free_slot << " slots exhausted";
|
||||
skipped_keys.emplace_back("material:" + material_label + (is_payload_mix ?
|
||||
" (mixed filament definition: filament slot limit reached)" :
|
||||
" (filament slot limit reached)"));
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": published 3MF " << (is_payload_mix ? "mixed filament definition" : "material")
|
||||
<< " from slot " << entry.slot << " could not be placed: all " << next_free_slot
|
||||
<< " slots exhausted";
|
||||
entry_it = published_config->material_keys.erase(entry_it);
|
||||
continue;
|
||||
}
|
||||
if (!is_payload_mix && physical_capacity < 2) {
|
||||
// A single physical slot can never host a mixed-filament editor (the
|
||||
// sidebar's mixed section needs two physical filaments to mix), so a
|
||||
// placeholder would be invisible and unfixable: drop the entry and
|
||||
// report it like any other unappliable input.
|
||||
skipped_keys.emplace_back("material:" + material_label + " (printer supports only " +
|
||||
std::to_string(physical_capacity) + " filament)");
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": published 3MF material from slot " << entry.slot
|
||||
<< " dropped: printer supports only " << physical_capacity << " filament";
|
||||
entry_it = published_config->material_keys.erase(entry_it);
|
||||
continue;
|
||||
}
|
||||
const int authored_slot = entry.slot;
|
||||
const int dest_slot = int(std::max(size_t(authored_slot), next_free_slot));
|
||||
const int dest_slot = is_payload_mix ? int(std::max(size_t(authored_slot), next_free_slot)) : int(next_free_slot);
|
||||
next_free_slot = size_t(dest_slot) + 1;
|
||||
if (dest_slot == authored_slot)
|
||||
// Uncontended fresh tail slot: the definition is already readable there.
|
||||
++entry_it;
|
||||
else {
|
||||
entry.slot = dest_slot;
|
||||
if (is_payload_mix) {
|
||||
if (dest_slot == authored_slot)
|
||||
// Uncontended fresh tail slot: the definition is already readable there.
|
||||
++entry_it;
|
||||
else {
|
||||
entry.slot = dest_slot;
|
||||
any_mixed_relocated = true;
|
||||
mixed_moves.emplace_back(size_t(authored_slot), size_t(entry.slot));
|
||||
published_config->mixed_slot_relocations.emplace(authored_slot, entry.slot);
|
||||
published_config->material_replacements.emplace_back("slot " + std::to_string(authored_slot) + " -> slot " +
|
||||
std::to_string(entry.slot) +
|
||||
": mixed filament relocated (would have replaced a physical filament)");
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": published 3MF relocated mixed filament slot " << authored_slot
|
||||
<< " -> " << entry.slot;
|
||||
++entry_it;
|
||||
}
|
||||
} else {
|
||||
// Surplus published material beyond the printer's capacity: become an
|
||||
// empty mixed-filament placeholder at the next free tail slot. The flag
|
||||
// routes the entry to the placeholder finalize below; the slot's
|
||||
// definition stays empty until the user assigns components.
|
||||
entry.mixed_placeholder = true;
|
||||
any_mixed_relocated = true;
|
||||
mixed_moves.emplace_back(size_t(authored_slot), size_t(entry.slot));
|
||||
published_config->mixed_slot_relocations.emplace(authored_slot, entry.slot);
|
||||
published_config->material_replacements.emplace_back("slot " + std::to_string(authored_slot) + " -> slot " +
|
||||
std::to_string(entry.slot) +
|
||||
": mixed filament relocated (would have replaced a physical filament)");
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": published 3MF relocated mixed filament slot " << authored_slot
|
||||
<< " -> " << entry.slot;
|
||||
if (dest_slot != authored_slot) {
|
||||
entry.slot = dest_slot;
|
||||
mixed_moves.emplace_back(size_t(authored_slot), size_t(dest_slot));
|
||||
published_config->mixed_slot_relocations.emplace(authored_slot, dest_slot);
|
||||
}
|
||||
published_config->material_replacements.emplace_back(
|
||||
(dest_slot != authored_slot ?
|
||||
"slot " + std::to_string(authored_slot) + " -> slot " + std::to_string(dest_slot) :
|
||||
"slot " + std::to_string(dest_slot)) +
|
||||
": " + material_label + " placed as an unassigned mixed filament (printer supports only " +
|
||||
std::to_string(physical_capacity) + " filaments)");
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": published 3MF material from slot " << authored_slot
|
||||
<< " placed as an unassigned mixed filament at slot " << dest_slot
|
||||
<< " (printer supports only " << physical_capacity << " filaments)";
|
||||
++entry_it;
|
||||
}
|
||||
}
|
||||
@@ -5501,9 +5585,10 @@ void PresetBundle::load_config_file_config(const std::string& name_or_path,
|
||||
// Defensive cap: growth never exceeds the file's own filament count. The
|
||||
// receiver's current slot count is a floor: neither the preset list nor the
|
||||
// project vectors are ever shrunk, even when the file carries fewer filaments
|
||||
// than the receiver has slots. Relocated mixed entries legitimately land past
|
||||
// the file's own slot count (virtual slots consume no nozzle or tray), so
|
||||
// their destinations lift the ceiling explicitly.
|
||||
// than the receiver has slots. Relocated mixed entries and capacity
|
||||
// placeholders legitimately land past the file's own slot count (virtual
|
||||
// slots consume no nozzle or tray), so their append destinations lift the
|
||||
// ceiling explicitly.
|
||||
const size_t target_slots = std::max({this->filament_presets.size(), std::min(grow_target, num_filaments),
|
||||
any_mixed_relocated ? next_free_slot : size_t(0)});
|
||||
// Slots carrying published content, steering the initial preset selection of
|
||||
@@ -5884,12 +5969,14 @@ void PresetBundle::load_config_file_config(const std::string& name_or_path,
|
||||
this->filament_presets.front() == this->filaments.get_edited_preset().name;
|
||||
// Final layout for mix-definition validation: every slot that will hold a
|
||||
// mixed definition once this load completes - the receiver's own virtual
|
||||
// slots plus each published mixed entry's final (possibly relocated) slot.
|
||||
// Mix components are 1-based slot numbers, so a component is valid only
|
||||
// when the slot it names exists and does not itself hold a mixed filament.
|
||||
// slots, each published mixed entry's final (possibly relocated) slot, and
|
||||
// each capacity placeholder (they become mixes in the finalize below, before
|
||||
// this loop's is_mixed_filament scan would see them). Mix components are
|
||||
// 1-based slot numbers, so a component is valid only when the slot it names
|
||||
// exists and does not itself hold a mixed filament.
|
||||
std::set<int> mixed_final_slots;
|
||||
for (const PublishedMaterialEntry& mix_entry : published_config->material_keys)
|
||||
if (mix_entry.slot >= 0 && is_mixed_definition(mix_entry))
|
||||
if (mix_entry.slot >= 0 && (is_mixed_definition(mix_entry) || mix_entry.mixed_placeholder))
|
||||
mixed_final_slots.insert(mix_entry.slot);
|
||||
for (size_t i = 0; i < this->filament_presets.size(); ++i)
|
||||
if (this->is_mixed_filament(i))
|
||||
@@ -5903,6 +5990,22 @@ void PresetBundle::load_config_file_config(const std::string& name_or_path,
|
||||
if (entry.slot < 0 || size_t(entry.slot) >= this->filament_presets.size())
|
||||
continue; // out of range: nothing to do for this slot
|
||||
const size_t slot = size_t(entry.slot);
|
||||
|
||||
// Surplus published material beyond the printer's capacity: finalize the
|
||||
// tail placeholder - mark the slot virtual with an intentionally empty
|
||||
// definition. The GUI flags the empty mix (check_mixed_filament_integrity)
|
||||
// and blocks slicing until the user assigns components from their own
|
||||
// filaments. The entry's keys are deliberately not applied and no preset
|
||||
// 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;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Resolve the stored preset itself (real=true), never the edited snapshot:
|
||||
// find_preset would return &m_edited_preset for the selected slot. The
|
||||
// overlay target below decides between the edited layer and this preset.
|
||||
|
||||
@@ -80,6 +80,13 @@ struct PublishedMaterialEntry {
|
||||
// Required filament colour, applied on load regardless of the type match.
|
||||
bool publish_color{false};
|
||||
std::string color;
|
||||
// Import-side only, never serialized: the entry's authored slot sits past the receiver
|
||||
// printer's physical filament capacity, so instead of growing a physical slot the entry
|
||||
// is appended as an empty mixed-filament placeholder (virtual tail slot; the GUI flags
|
||||
// it and the user assigns components from their own filaments). The flag also keeps the
|
||||
// entry out of the payload mixed-definition validation and the value-apply passes,
|
||||
// which only make sense for a slot that carries a real material.
|
||||
bool mixed_placeholder{false};
|
||||
};
|
||||
|
||||
// "PLA High Speed" -> "PLA" (strip a space modifier); dash types like "PA-CF" are kept intact.
|
||||
|
||||
@@ -2932,6 +2932,302 @@ TEST_CASE("Published 3MF relocates a mixed filament instead of overwriting a phy
|
||||
}
|
||||
}
|
||||
|
||||
// The receiver's printer gates how many PHYSICAL filament slots a published 3MF may add: a
|
||||
// non-SEMM tool-changer feeds filament N from nozzle N, so a published slot past the nozzle
|
||||
// count cannot become a physical filament. It becomes an empty mixed-filament placeholder
|
||||
// instead - a virtual tail slot the GUI flags (broken mix) and the user fills with components
|
||||
// from their own filaments. SEMM receivers keep the ungated behaviour.
|
||||
TEST_CASE("Published 3MF turns a surplus slot past the printer's filament capacity into an empty mixed placeholder", "[Preset][Bundle][Published]")
|
||||
{
|
||||
// An author project with <num_author_slots> physical slots, no mixed ones.
|
||||
auto make_file_config = [](size_t num_author_slots) {
|
||||
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
|
||||
std::vector<double> diameters(num_author_slots, 1.75);
|
||||
std::vector<int> self_index;
|
||||
std::vector<std::string> variants;
|
||||
for (size_t i = 0; i < num_author_slots; ++i) {
|
||||
self_index.push_back(int(i + 1));
|
||||
variants.emplace_back("Direct Drive Standard");
|
||||
}
|
||||
config.opt<ConfigOptionFloats>("filament_diameter")->values = diameters;
|
||||
config.opt<ConfigOptionInts>("filament_self_index")->values = self_index;
|
||||
config.opt<ConfigOptionStrings>("filament_extruder_variant")->values = variants;
|
||||
config.opt<ConfigOptionStrings>("filament_colour")->values.resize(num_author_slots, "#808080");
|
||||
config.opt<ConfigOptionStrings>("filament_type")->values.assign(num_author_slots, "PLA");
|
||||
config.opt<ConfigOptionStrings>("filament_vendor")->values.assign(num_author_slots, "Generic");
|
||||
config.opt<ConfigOptionStrings>("filament_ids")->values.resize(num_author_slots);
|
||||
return config;
|
||||
};
|
||||
// A non-SEMM receiver with <nozzles> nozzles running <slots> copies of one preset.
|
||||
auto make_receiver = [](PresetBundle &bundle, size_t nozzles, size_t slots) {
|
||||
Preset &pla = add_inmemory_preset(bundle.filaments, "My PLA");
|
||||
pla.config.opt_string("filament_type", 0u) = "PLA";
|
||||
bundle.filament_presets.assign(slots, "My PLA");
|
||||
bundle.set_num_filaments(slots, "#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(nozzles, 0.4);
|
||||
};
|
||||
auto make_physical_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;
|
||||
return entry;
|
||||
};
|
||||
|
||||
// The reported case: an author publishes with a filament on slot 5; the receiver is a
|
||||
// 4-filament tool-changer. The receiver keeps its four physical slots and the surplus
|
||||
// material lands as an empty mixed placeholder at the tail.
|
||||
{
|
||||
PresetBundle bundle;
|
||||
make_receiver(bundle, 4, 4);
|
||||
const std::vector<std::string> receiver_colours =
|
||||
bundle.project_config.opt<ConfigOptionStrings>("filament_colour")->values;
|
||||
|
||||
PublishedConfig pub;
|
||||
pub.published = true;
|
||||
pub.material_keys = { make_physical_entry(4, "#ABCDEF") };
|
||||
DynamicPrintConfig config = make_file_config(5);
|
||||
Preset::normalize(config);
|
||||
bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub);
|
||||
|
||||
// The receiver grew by exactly one virtual slot, not a fifth physical one.
|
||||
REQUIRE(bundle.filament_presets.size() == 5);
|
||||
const auto &is_mixed = bundle.project_config.opt<ConfigOptionBools>("filament_is_mixed")->values;
|
||||
REQUIRE(is_mixed.size() == 5);
|
||||
for (size_t i = 0; i < 4; ++i)
|
||||
CHECK_FALSE(is_mixed[i]);
|
||||
CHECK(is_mixed[4]);
|
||||
// The placeholder carries no definition: the GUI's integrity check flags it and
|
||||
// blocks slicing until the user assigns components.
|
||||
const auto &components = bundle.project_config.opt<ConfigOptionStrings>("filament_mixed_components")->values;
|
||||
REQUIRE(components.size() == 5);
|
||||
CHECK(components[4].empty());
|
||||
// The four physical slots kept their meaning and colours.
|
||||
CHECK(std::equal(receiver_colours.begin(), receiver_colours.end(),
|
||||
bundle.project_config.opt<ConfigOptionStrings>("filament_colour")->values.begin()));
|
||||
CHECK(bundle.filament_presets[0] == "My PLA");
|
||||
CHECK(bundle.filament_presets[3] == "My PLA");
|
||||
// The published colour seeds the placeholder's swatch.
|
||||
CHECK(bundle.project_config.opt<ConfigOptionStrings>("filament_colour")->values[4] == "#ABCDEF");
|
||||
// The conversion is surfaced through the post-import notice.
|
||||
bool placeholder_reported = false;
|
||||
for (const std::string &message : pub.material_replacements)
|
||||
if (message.find("unassigned mixed filament") != std::string::npos)
|
||||
placeholder_reported = true;
|
||||
CHECK(placeholder_reported);
|
||||
CHECK(pub.skipped_keys.empty());
|
||||
CHECK(pub.mixed_slot_relocations.empty());
|
||||
}
|
||||
|
||||
// Two surplus slots (5 and 6) become two consecutive empty placeholders.
|
||||
{
|
||||
PresetBundle bundle;
|
||||
make_receiver(bundle, 4, 4);
|
||||
|
||||
PublishedConfig pub;
|
||||
pub.published = true;
|
||||
pub.material_keys = { make_physical_entry(4, "#ABCDEF"), make_physical_entry(5, "#F0F0F0") };
|
||||
DynamicPrintConfig config = make_file_config(6);
|
||||
Preset::normalize(config);
|
||||
bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub);
|
||||
|
||||
REQUIRE(bundle.filament_presets.size() == 6);
|
||||
const auto &is_mixed = bundle.project_config.opt<ConfigOptionBools>("filament_is_mixed")->values;
|
||||
REQUIRE(is_mixed.size() == 6);
|
||||
for (size_t i = 0; i < 4; ++i)
|
||||
CHECK_FALSE(is_mixed[i]);
|
||||
CHECK(is_mixed[4]);
|
||||
CHECK(is_mixed[5]);
|
||||
const auto &components = bundle.project_config.opt<ConfigOptionStrings>("filament_mixed_components")->values;
|
||||
REQUIRE(components.size() == 6);
|
||||
CHECK(components[4].empty());
|
||||
CHECK(components[5].empty());
|
||||
const auto &colour = bundle.project_config.opt<ConfigOptionStrings>("filament_colour")->values;
|
||||
REQUIRE(colour.size() == 6);
|
||||
CHECK(colour[4] == "#ABCDEF");
|
||||
CHECK(colour[5] == "#F0F0F0");
|
||||
CHECK(pub.skipped_keys.empty());
|
||||
CHECK(pub.mixed_slot_relocations.empty());
|
||||
}
|
||||
|
||||
// A surplus slot past both the receiver's list and the capacity packs onto the next free
|
||||
// tail slot (never max(authored, next_free), which would grow filler physical slots past
|
||||
// the capacity), and the relocation is recorded for the model-reference remapping.
|
||||
{
|
||||
PresetBundle bundle;
|
||||
make_receiver(bundle, 2, 2);
|
||||
|
||||
PublishedConfig pub;
|
||||
pub.published = true;
|
||||
pub.material_keys = { make_physical_entry(3, "#ABCDEF") };
|
||||
DynamicPrintConfig config = make_file_config(4);
|
||||
Preset::normalize(config);
|
||||
bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub);
|
||||
|
||||
REQUIRE(bundle.filament_presets.size() == 3);
|
||||
const auto &is_mixed = bundle.project_config.opt<ConfigOptionBools>("filament_is_mixed")->values;
|
||||
REQUIRE(is_mixed.size() == 3);
|
||||
CHECK_FALSE(is_mixed[0]);
|
||||
CHECK_FALSE(is_mixed[1]);
|
||||
CHECK(is_mixed[2]);
|
||||
const auto &components = bundle.project_config.opt<ConfigOptionStrings>("filament_mixed_components")->values;
|
||||
REQUIRE(components.size() == 3);
|
||||
CHECK(components[2].empty());
|
||||
REQUIRE(pub.mixed_slot_relocations.size() == 1);
|
||||
CHECK(pub.mixed_slot_relocations.at(3) == 2);
|
||||
bool relocation_reported = false;
|
||||
for (const std::string &message : pub.material_replacements)
|
||||
if (message.find("slot 3 -> slot 2") != std::string::npos &&
|
||||
message.find("unassigned mixed filament") != std::string::npos)
|
||||
relocation_reported = true;
|
||||
CHECK(relocation_reported);
|
||||
CHECK(pub.skipped_keys.empty());
|
||||
}
|
||||
|
||||
// A Full Publish entry past the capacity becomes a placeholder too: no standalone
|
||||
// detached copy is created for a material that got no physical slot.
|
||||
{
|
||||
PresetBundle bundle;
|
||||
make_receiver(bundle, 4, 4);
|
||||
|
||||
PublishedMaterialEntry entry = make_physical_entry(4, "#ABCDEF");
|
||||
entry.full = true;
|
||||
entry.preset_name = "Generic PLA @System";
|
||||
entry.filament_id = "GFL99";
|
||||
entry.full_keys = { "filament_retraction_length" };
|
||||
|
||||
PublishedConfig pub;
|
||||
pub.published = true;
|
||||
pub.material_keys = { entry };
|
||||
DynamicPrintConfig config = make_file_config(5);
|
||||
Preset::normalize(config);
|
||||
bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub);
|
||||
|
||||
REQUIRE(bundle.filament_presets.size() == 5);
|
||||
const auto &is_mixed = bundle.project_config.opt<ConfigOptionBools>("filament_is_mixed")->values;
|
||||
REQUIRE(is_mixed.size() == 5);
|
||||
CHECK(is_mixed[4]);
|
||||
// No detached copy under the stripped name or its uniquified forms.
|
||||
CHECK(bundle.filaments.find_preset("Generic PLA", false, true) == nullptr);
|
||||
CHECK(bundle.filaments.find_preset("Generic PLA (Published)", false, true) == nullptr);
|
||||
CHECK(pub.skipped_keys.empty());
|
||||
}
|
||||
|
||||
// A SEMM receiver (the default printer preset) sizes its slot list by hand: the published
|
||||
// slot past the nozzle count still grows physically, as before the capacity gate.
|
||||
{
|
||||
PresetBundle bundle;
|
||||
Preset &pla = add_inmemory_preset(bundle.filaments, "My PLA");
|
||||
pla.config.opt_string("filament_type", 0u) = "PLA";
|
||||
bundle.filament_presets = { "My PLA", "My PLA", "My PLA", "My PLA" };
|
||||
bundle.set_num_filaments(4, "#123456");
|
||||
|
||||
PublishedConfig pub;
|
||||
pub.published = true;
|
||||
pub.material_keys = { make_physical_entry(4, "#ABCDEF") };
|
||||
DynamicPrintConfig config = make_file_config(5);
|
||||
Preset::normalize(config);
|
||||
bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub);
|
||||
|
||||
REQUIRE(bundle.filament_presets.size() == 5);
|
||||
const auto &is_mixed = bundle.project_config.opt<ConfigOptionBools>("filament_is_mixed")->values;
|
||||
REQUIRE(is_mixed.size() == 5);
|
||||
for (size_t i = 0; i < 5; ++i)
|
||||
CHECK_FALSE(is_mixed[i]);
|
||||
CHECK(bundle.project_config.opt<ConfigOptionStrings>("filament_colour")->values[4] == "#ABCDEF");
|
||||
CHECK(pub.skipped_keys.empty());
|
||||
}
|
||||
|
||||
// A pre-existing oversized slot list is never shrunk: a published entry pointing at one
|
||||
// of its slots is applied positionally even though the list exceeds the nozzle count.
|
||||
{
|
||||
PresetBundle bundle;
|
||||
make_receiver(bundle, 4, 5);
|
||||
|
||||
PublishedConfig pub;
|
||||
pub.published = true;
|
||||
pub.material_keys = { make_physical_entry(4, "#ABCDEF") };
|
||||
DynamicPrintConfig config = make_file_config(5);
|
||||
Preset::normalize(config);
|
||||
bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub);
|
||||
|
||||
REQUIRE(bundle.filament_presets.size() == 5);
|
||||
const auto &is_mixed = bundle.project_config.opt<ConfigOptionBools>("filament_is_mixed")->values;
|
||||
REQUIRE(is_mixed.size() == 5);
|
||||
for (size_t i = 0; i < 5; ++i)
|
||||
CHECK_FALSE(is_mixed[i]);
|
||||
// The published colour reached the addressed slot's (shared) preset in place.
|
||||
CHECK(bundle.filaments.find_preset("My PLA", false, true)->config.opt<ConfigOptionStrings>("filament_colour")->values ==
|
||||
std::vector<std::string>{ "#ABCDEF" });
|
||||
CHECK(bundle.project_config.opt<ConfigOptionStrings>("filament_colour")->values[4] == "#ABCDEF");
|
||||
CHECK(pub.skipped_keys.empty());
|
||||
}
|
||||
|
||||
// On a single-physical-slot receiver an empty mix could never be edited (the sidebar's
|
||||
// mixed section needs two physical filaments), so the surplus entry is dropped and
|
||||
// reported instead of becoming an unfixable placeholder.
|
||||
{
|
||||
PresetBundle bundle;
|
||||
make_receiver(bundle, 1, 1);
|
||||
|
||||
PublishedConfig pub;
|
||||
pub.published = true;
|
||||
pub.material_keys = { make_physical_entry(1, "#ABCDEF") };
|
||||
DynamicPrintConfig config = make_file_config(2);
|
||||
Preset::normalize(config);
|
||||
bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub);
|
||||
|
||||
CHECK(bundle.filament_presets.size() == 1);
|
||||
const auto &is_mixed = bundle.project_config.opt<ConfigOptionBools>("filament_is_mixed")->values;
|
||||
REQUIRE(is_mixed.size() == 1);
|
||||
CHECK_FALSE(is_mixed[0]);
|
||||
REQUIRE(pub.skipped_keys.size() == 1);
|
||||
CHECK(pub.skipped_keys.front().find("printer supports only 1") != std::string::npos);
|
||||
}
|
||||
|
||||
// A payload mixed definition is exempt from the capacity gate: mixes are virtual slots
|
||||
// that consume no nozzle, so a published mix past the nozzle count still lands.
|
||||
{
|
||||
PresetBundle bundle;
|
||||
make_receiver(bundle, 4, 4);
|
||||
|
||||
PublishedMaterialEntry mix;
|
||||
mix.slot = 4;
|
||||
mix.publish_color = true;
|
||||
mix.color = "#800080";
|
||||
mix.keys = { "filament_is_mixed", "filament_mixed_components", "filament_mixed_sublayer_ratios" };
|
||||
|
||||
PublishedConfig pub;
|
||||
pub.published = true;
|
||||
pub.material_keys = { mix };
|
||||
DynamicPrintConfig config = make_file_config(5);
|
||||
config.opt<ConfigOptionBools>("filament_is_mixed")->values.assign(5, 0);
|
||||
config.opt<ConfigOptionStrings>("filament_mixed_components")->values.assign(5, "");
|
||||
config.opt<ConfigOptionStrings>("filament_mixed_sublayer_ratios")->values.assign(5, "");
|
||||
config.opt<ConfigOptionBools>("filament_is_mixed")->values[4] = 1;
|
||||
config.opt<ConfigOptionStrings>("filament_mixed_components")->values[4] = "1,2";
|
||||
config.opt<ConfigOptionStrings>("filament_mixed_sublayer_ratios")->values[4] = "0.6,0.4";
|
||||
Preset::normalize(config);
|
||||
bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub);
|
||||
|
||||
REQUIRE(bundle.filament_presets.size() == 5);
|
||||
const auto &is_mixed = bundle.project_config.opt<ConfigOptionBools>("filament_is_mixed")->values;
|
||||
REQUIRE(is_mixed.size() == 5);
|
||||
CHECK(is_mixed[4]);
|
||||
const auto &components = bundle.project_config.opt<ConfigOptionStrings>("filament_mixed_components")->values;
|
||||
REQUIRE(components.size() == 5);
|
||||
CHECK(components[4] == "1,2");
|
||||
const auto &ratios = bundle.project_config.opt<ConfigOptionStrings>("filament_mixed_sublayer_ratios")->values;
|
||||
REQUIRE(ratios.size() == 5);
|
||||
CHECK(ratios[4] == "0.6,0.4");
|
||||
CHECK(pub.skipped_keys.empty());
|
||||
}
|
||||
}
|
||||
|
||||
// A single-extruder receiver collapses the author's per-extruder printer slots onto its single
|
||||
// slot: the first serialized variant of a base key is applied, the remaining variants of that
|
||||
// base key are reported as skipped.
|
||||
|
||||
Reference in New Issue
Block a user