mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-11 19:17:33 +00:00
Fixes issue with mixed filament being loaded into a real slot
This commit is contained in:
@@ -4759,6 +4759,98 @@ static void apply_mixed_config_relocations(DynamicPrintConfig&
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Relocate the per-slot cells of the receiver's OWN mixed-filament definitions (the ones that
|
||||||
|
// pre-existed in project_config) onto fresh tail slots, clearing each vacated source cell so an
|
||||||
|
// incoming published real filament can claim it. Unlike apply_mixed_config_relocations - which
|
||||||
|
// moves the incoming file's config and leaves sources alone - a displaced receiver mix must not
|
||||||
|
// keep its mixed flag in the physical region: the source slot becomes a physical slot, so its
|
||||||
|
// mixed flag and definition are reset, while its swatch colour and mapping travel with the
|
||||||
|
// definition to the tail slot. Reads come from a frozen snapshot so an earlier move's
|
||||||
|
// destination never overwrites a later move's still-unread source (sources sit in the physical
|
||||||
|
// region and destinations past it, so they cannot overlap, but the snapshot keeps the helper
|
||||||
|
// safe for any future reordering).
|
||||||
|
static void apply_receiver_mix_relocations(DynamicPrintConfig& config,
|
||||||
|
std::vector<std::vector<std::string>>& ams_multi_color_filment,
|
||||||
|
const std::vector<std::pair<size_t, size_t>>& moves)
|
||||||
|
{
|
||||||
|
if (moves.empty())
|
||||||
|
return;
|
||||||
|
|
||||||
|
auto move_bools = [&](const char* key, bool clear_source) {
|
||||||
|
ConfigOption* opt = config.optptr(key);
|
||||||
|
if (opt == nullptr)
|
||||||
|
return;
|
||||||
|
auto* live = static_cast<ConfigOptionBools*>(opt);
|
||||||
|
std::unique_ptr<ConfigOption> snapshot(opt->clone());
|
||||||
|
const auto* frozen = static_cast<const ConfigOptionBools*>(snapshot.get());
|
||||||
|
for (const auto [from, to] : moves) {
|
||||||
|
const bool cell = from < frozen->values.size() ? frozen->values[from] : false;
|
||||||
|
if (live->values.size() <= to)
|
||||||
|
live->values.resize(to + 1, false);
|
||||||
|
live->values[to] = cell;
|
||||||
|
if (clear_source && from < live->values.size())
|
||||||
|
live->values[from] = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
auto move_strings = [&](const char* key, bool clear_source) {
|
||||||
|
ConfigOption* opt = config.optptr(key);
|
||||||
|
if (opt == nullptr)
|
||||||
|
return;
|
||||||
|
auto* live = static_cast<ConfigOptionStrings*>(opt);
|
||||||
|
std::unique_ptr<ConfigOption> snapshot(opt->clone());
|
||||||
|
const auto* frozen = static_cast<const ConfigOptionStrings*>(snapshot.get());
|
||||||
|
for (const auto [from, to] : moves) {
|
||||||
|
const std::string cell = from < frozen->values.size() ? frozen->values[from] : std::string();
|
||||||
|
if (live->values.size() <= to)
|
||||||
|
live->values.resize(to + 1, std::string{});
|
||||||
|
live->values[to] = cell;
|
||||||
|
if (clear_source && from < live->values.size())
|
||||||
|
live->values[from] = std::string{};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
auto move_ints = [&](const char* key) {
|
||||||
|
ConfigOption* opt = config.optptr(key);
|
||||||
|
if (opt == nullptr)
|
||||||
|
return;
|
||||||
|
auto* live = static_cast<ConfigOptionInts*>(opt);
|
||||||
|
std::unique_ptr<ConfigOption> snapshot(opt->clone());
|
||||||
|
const auto* frozen = static_cast<const ConfigOptionInts*>(snapshot.get());
|
||||||
|
for (const auto [from, to] : moves) {
|
||||||
|
const int cell = from < frozen->values.size() ? frozen->values[from] : 0;
|
||||||
|
if (live->values.size() <= to)
|
||||||
|
live->values.resize(to + 1, 0);
|
||||||
|
live->values[to] = cell;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mixed-definition cells: move to the tail and clear the source - the vacated physical slot
|
||||||
|
// no longer holds a mix.
|
||||||
|
move_bools("filament_is_mixed", true);
|
||||||
|
move_strings("filament_mixed_components", true);
|
||||||
|
move_strings("filament_mixed_sublayer_ratios", true);
|
||||||
|
move_bools("filament_mixed_gradient", true);
|
||||||
|
move_strings("filament_mixed_gradient_range", true);
|
||||||
|
move_strings("filament_mixed_gradient_curve", true);
|
||||||
|
move_bools("filament_mixed_gradient_per_part", true);
|
||||||
|
// Swatch colour and mapping travel with the definition; the source colour is left for the
|
||||||
|
// incoming real's publish_color (or the slot's resolved preset) to fill in.
|
||||||
|
move_strings("filament_colour", false);
|
||||||
|
move_strings("filament_multi_colour", false);
|
||||||
|
move_strings("filament_colour_type", false);
|
||||||
|
move_ints("filament_map");
|
||||||
|
move_ints("filament_nozzle_map");
|
||||||
|
move_ints("filament_volume_map");
|
||||||
|
{
|
||||||
|
const std::vector<std::vector<std::string>> frozen = ams_multi_color_filment;
|
||||||
|
for (const auto [from, to] : moves) {
|
||||||
|
const std::vector<std::string> cell = from < frozen.size() ? frozen[from] : std::vector<std::string>();
|
||||||
|
if (ams_multi_color_filment.size() <= to)
|
||||||
|
ams_multi_color_filment.resize(to + 1, std::vector<std::string>{});
|
||||||
|
ams_multi_color_filment[to] = cell;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
//convert the old filament preset to new one after split
|
//convert the old filament preset to new one after split
|
||||||
static void convert_filament_preset_name(std::string& machine_name, std::string& filament_name)
|
static void convert_filament_preset_name(std::string& machine_name, std::string& filament_name)
|
||||||
@@ -5287,7 +5379,24 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool
|
|||||||
// counter preserving author order (dest = max(authored, next_free)). Appends past
|
// counter preserving author order (dest = max(authored, next_free)). Appends past
|
||||||
// the extruder limit are dropped and reported. Physical entries past capacity join
|
// the extruder limit are dropped and reported. Physical entries past capacity join
|
||||||
// the same counter as mixed_placeholder empties the GUI flags for assignment.
|
// the same counter as mixed_placeholder empties the GUI flags for assignment.
|
||||||
size_t next_free_slot = this->filament_presets.size();
|
//
|
||||||
|
// The finished project keeps the physical-first invariant (see the sidebar's
|
||||||
|
// add_custom_filament: mixed slots always sit at the tail, physical slots packed
|
||||||
|
// first). That invariant must survive an import, so every receiver mixed slot that
|
||||||
|
// would end up inside (or ahead of) the incoming physical region is displaced to a
|
||||||
|
// fresh tail slot, and the tail allocator starts at the physical boundary rather
|
||||||
|
// than the receiver's slot count - otherwise a relocated mix can collide with a
|
||||||
|
// keep-placed new real slot.
|
||||||
|
size_t physical_boundary = this->num_physical_filaments();
|
||||||
|
for (const PublishedMaterialEntry& entry : published_config->material_keys)
|
||||||
|
// Mirror the payload-real keep-place decision below: a real keeps its authored
|
||||||
|
// slot when that slot already exists on the receiver, or lies within the
|
||||||
|
// printer's physical capacity. Both end up as physical slots at index
|
||||||
|
// entry.slot, so they bound the physical region even past the capacity.
|
||||||
|
if (entry.slot >= 0 && !is_mixed_definition(entry) &&
|
||||||
|
(size_t(entry.slot) < this->filament_presets.size() || size_t(entry.slot) < physical_capacity))
|
||||||
|
physical_boundary = std::max(physical_boundary, size_t(entry.slot) + 1);
|
||||||
|
size_t next_free_slot = std::max(physical_boundary, this->filament_presets.size());
|
||||||
bool any_mixed_relocated = false;
|
bool any_mixed_relocated = false;
|
||||||
// All authored-slot -> destination moves decided by this pass, applied to the
|
// All authored-slot -> destination moves decided by this pass, applied to the
|
||||||
// incoming config in one batched snapshot step below (an earlier move's
|
// incoming config in one batched snapshot step below (an earlier move's
|
||||||
@@ -5295,6 +5404,33 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool
|
|||||||
// onto consecutive slots, so incremental in-place shifts would overwrite a
|
// onto consecutive slots, so incremental in-place shifts would overwrite a
|
||||||
// definition that has not been moved yet).
|
// definition that has not been moved yet).
|
||||||
std::vector<std::pair<size_t, size_t>> mixed_moves;
|
std::vector<std::pair<size_t, size_t>> mixed_moves;
|
||||||
|
// Receiver-mix relocations land in project_config, not the incoming config, so they
|
||||||
|
// get their own move list, applied below after the arrays grow. The swept set lets
|
||||||
|
// the payload loop below treat a displaced receiver mix as the physical slot it will
|
||||||
|
// become (a payload mix like-for-like overriding such a slot must itself relocate).
|
||||||
|
std::vector<std::pair<size_t, size_t>> receiver_mix_moves;
|
||||||
|
std::set<size_t> swept_mix_slots;
|
||||||
|
for (size_t slot = 0; slot < this->filament_presets.size(); ++slot) {
|
||||||
|
if (!this->is_mixed_filament(slot))
|
||||||
|
continue;
|
||||||
|
// Slot 0 is the receiver's base filament and is never a virtual mix; the
|
||||||
|
// sidebar's physical-first layout guarantees it, so never displace it.
|
||||||
|
if (slot == 0)
|
||||||
|
continue;
|
||||||
|
if (slot >= physical_boundary)
|
||||||
|
continue; // already lives in the tail region
|
||||||
|
const size_t dest = next_free_slot++;
|
||||||
|
swept_mix_slots.insert(slot);
|
||||||
|
receiver_mix_moves.emplace_back(slot, dest);
|
||||||
|
any_mixed_relocated = true;
|
||||||
|
// A payload mix authored at the same slot (a mix inside the physical region) is
|
||||||
|
// processed after this sweep and overrides its own destination below.
|
||||||
|
published_config->mixed_slot_relocations.insert_or_assign(int(slot), int(dest));
|
||||||
|
published_config->material_replacements.emplace_back("slot " + std::to_string(slot) + " -> slot " +
|
||||||
|
std::to_string(dest) + ": mixed filament");
|
||||||
|
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": published 3MF relocated receiver mixed filament slot " << slot
|
||||||
|
<< " -> " << dest << " (physical-first rebalance)";
|
||||||
|
}
|
||||||
for (auto entry_it = published_config->material_keys.begin(); entry_it != published_config->material_keys.end();) {
|
for (auto entry_it = published_config->material_keys.begin(); entry_it != published_config->material_keys.end();) {
|
||||||
PublishedMaterialEntry& entry = *entry_it;
|
PublishedMaterialEntry& entry = *entry_it;
|
||||||
if (entry.slot < 0) {
|
if (entry.slot < 0) {
|
||||||
@@ -5310,7 +5446,7 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool
|
|||||||
// above the printer's capacity (pre-existing state is never shrunk).
|
// above the printer's capacity (pre-existing state is never shrunk).
|
||||||
bool keep_place = false;
|
bool keep_place = false;
|
||||||
if (is_payload_mix)
|
if (is_payload_mix)
|
||||||
keep_place = this->is_mixed_filament(size_t(entry.slot));
|
keep_place = this->is_mixed_filament(size_t(entry.slot)) && swept_mix_slots.count(size_t(entry.slot)) == 0;
|
||||||
else
|
else
|
||||||
keep_place = size_t(entry.slot) < this->filament_presets.size() ||
|
keep_place = size_t(entry.slot) < this->filament_presets.size() ||
|
||||||
size_t(entry.slot) < physical_capacity;
|
size_t(entry.slot) < physical_capacity;
|
||||||
@@ -5357,7 +5493,7 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool
|
|||||||
entry.slot = dest_slot;
|
entry.slot = dest_slot;
|
||||||
any_mixed_relocated = true;
|
any_mixed_relocated = true;
|
||||||
mixed_moves.emplace_back(size_t(authored_slot), size_t(entry.slot));
|
mixed_moves.emplace_back(size_t(authored_slot), size_t(entry.slot));
|
||||||
published_config->mixed_slot_relocations.emplace(authored_slot, entry.slot);
|
published_config->mixed_slot_relocations.insert_or_assign(authored_slot, entry.slot);
|
||||||
published_config->material_replacements.emplace_back("slot " + std::to_string(authored_slot) + " -> slot " +
|
published_config->material_replacements.emplace_back("slot " + std::to_string(authored_slot) + " -> slot " +
|
||||||
std::to_string(entry.slot) + ": mixed filament");
|
std::to_string(entry.slot) + ": mixed filament");
|
||||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": published 3MF relocated mixed filament slot " << authored_slot
|
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": published 3MF relocated mixed filament slot " << authored_slot
|
||||||
@@ -5374,7 +5510,7 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool
|
|||||||
if (dest_slot != authored_slot) {
|
if (dest_slot != authored_slot) {
|
||||||
entry.slot = dest_slot;
|
entry.slot = dest_slot;
|
||||||
mixed_moves.emplace_back(size_t(authored_slot), size_t(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->mixed_slot_relocations.insert_or_assign(authored_slot, dest_slot);
|
||||||
}
|
}
|
||||||
published_config->material_replacements.emplace_back(
|
published_config->material_replacements.emplace_back(
|
||||||
(dest_slot != authored_slot ?
|
(dest_slot != authored_slot ?
|
||||||
@@ -5725,6 +5861,11 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool
|
|||||||
// slots wrote to it; that compound case is not chased.)
|
// slots wrote to it; that compound case is not chased.)
|
||||||
const bool edited_survives_load = this->filament_presets.empty() ||
|
const bool edited_survives_load = this->filament_presets.empty() ||
|
||||||
this->filament_presets.front() == this->filaments.get_edited_preset().name;
|
this->filament_presets.front() == this->filaments.get_edited_preset().name;
|
||||||
|
// Displaced receiver mixes (see the physical-first rebalance above): move their
|
||||||
|
// per-slot cells onto the grown tail slots and reset the vacated physical slots,
|
||||||
|
// so the incoming real filaments can claim them. Runs before mixed_final_slots is
|
||||||
|
// built, so the rebalanced layout is what the mix validation sees.
|
||||||
|
apply_receiver_mix_relocations(this->project_config, this->ams_multi_color_filment, receiver_mix_moves);
|
||||||
// Final layout for mix-definition validation: every slot that will hold a
|
// Final layout for mix-definition validation: every slot that will hold a
|
||||||
// mixed definition once this load completes - the receiver's own virtual
|
// mixed definition once this load completes - the receiver's own virtual
|
||||||
// slots, each published mixed entry's final (possibly relocated) slot, and
|
// slots, each published mixed entry's final (possibly relocated) slot, and
|
||||||
|
|||||||
@@ -2980,6 +2980,215 @@ TEST_CASE("Published 3MF relocates a mixed filament instead of overwriting a phy
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A receiver that already owns a MIXED filament must keep the physical-first invariant after a
|
||||||
|
// published-3MF import: when incoming physical filaments would land on (or ahead of) the
|
||||||
|
// receiver's mixed slot, that mix is displaced to a fresh tail slot instead of being left
|
||||||
|
// interleaved with them (the R,M,R bug).
|
||||||
|
TEST_CASE("Published 3MF relocates the receiver's mixed filament past the incoming physical slots", "[Preset][Bundle][Published]")
|
||||||
|
{
|
||||||
|
// Build the receiver's tool-changer with three slots, the third being the receiver's own
|
||||||
|
// mixed filament. A SEMM (single_extruder_multi_material) receiver sizes its slot list by
|
||||||
|
// hand, so a lower slot count than the printer's nozzle count is preserved on load - a
|
||||||
|
// non-SEMM tool-changer would top the preset list up to the nozzle count and shift the
|
||||||
|
// expected sizes (the rebalance logic under test is the same either way).
|
||||||
|
auto make_receiver = [](PresetBundle &bundle, const std::string &components, const std::string &ratios) {
|
||||||
|
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" };
|
||||||
|
bundle.set_num_filaments(3, "#123456");
|
||||||
|
bundle.printers.get_edited_preset().config.opt<ConfigOptionBool>("single_extruder_multi_material", true)->value = true;
|
||||||
|
bundle.project_config.opt<ConfigOptionBools>("filament_is_mixed")->values[2] = 1;
|
||||||
|
bundle.project_config.opt<ConfigOptionStrings>("filament_mixed_components")->values[2] = components;
|
||||||
|
bundle.project_config.opt<ConfigOptionStrings>("filament_mixed_sublayer_ratios")->values[2] = ratios;
|
||||||
|
bundle.project_config.opt<ConfigOptionStrings>("filament_colour")->values[2] = "#800080";
|
||||||
|
bundle.project_config.opt<ConfigOptionStrings>("filament_multi_colour")->values[2] = "#800080";
|
||||||
|
};
|
||||||
|
auto make_real_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;
|
||||||
|
};
|
||||||
|
// A four-physical author project with no mixed slots (colour publish only), as in the
|
||||||
|
// reported Ferrari reference file.
|
||||||
|
auto make_config_4_real = [] {
|
||||||
|
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", "#000000", "#FFFFFF", "#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;
|
||||||
|
};
|
||||||
|
|
||||||
|
// [R, R, M] + four colour-only physical slots at authored 0..3 -> [R, R, R, R, M].
|
||||||
|
{
|
||||||
|
PresetBundle bundle;
|
||||||
|
make_receiver(bundle, "1,2", "0.5,0.5");
|
||||||
|
|
||||||
|
PublishedConfig pub;
|
||||||
|
pub.published = true;
|
||||||
|
pub.material_keys = { make_real_entry(0, "#FF0000"), make_real_entry(1, "#000000"),
|
||||||
|
make_real_entry(2, "#FFFFFF"), make_real_entry(3, "#FFFF00") };
|
||||||
|
DynamicPrintConfig config = make_config_4_real();
|
||||||
|
Preset::normalize(config);
|
||||||
|
bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub);
|
||||||
|
|
||||||
|
// The receiver grows by one extra virtual slot; the mix lands at the tail.
|
||||||
|
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 definition travelled with its swatch colour; the vacated slot 2 became physical.
|
||||||
|
const auto &components = bundle.project_config.opt<ConfigOptionStrings>("filament_mixed_components")->values;
|
||||||
|
REQUIRE(components.size() == 5);
|
||||||
|
CHECK(components[4] == "1,2");
|
||||||
|
CHECK(components[2].empty());
|
||||||
|
const auto &colour = bundle.project_config.opt<ConfigOptionStrings>("filament_colour")->values;
|
||||||
|
REQUIRE(colour.size() == 5);
|
||||||
|
CHECK(colour[4] == "#800080");
|
||||||
|
CHECK(colour[2] == "#FFFFFF");
|
||||||
|
CHECK(pub.mixed_slot_relocations.at(2) == 4);
|
||||||
|
bool relocated_reported = false;
|
||||||
|
for (const std::string &message : pub.material_replacements)
|
||||||
|
if (message.find("slot 2 -> slot 4") != std::string::npos &&
|
||||||
|
message.find("mixed filament") != std::string::npos)
|
||||||
|
relocated_reported = true;
|
||||||
|
CHECK(relocated_reported);
|
||||||
|
CHECK(pub.skipped_keys.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
// [R, R, M] plus a payload mix authored at slot 3: the receiver mix (displaced to slot 3) sits
|
||||||
|
// ahead of the appended payload mix (slot 4), preserving physical-first tail ordering.
|
||||||
|
{
|
||||||
|
PresetBundle bundle;
|
||||||
|
make_receiver(bundle, "1,2", "0.5,0.5");
|
||||||
|
|
||||||
|
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", "#000000", "#0000FF", "#800080" };
|
||||||
|
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" };
|
||||||
|
// Authored slot 3 is a payload mixed definition blending slots 1 and 3.
|
||||||
|
config.opt<ConfigOptionBools>("filament_is_mixed")->values = { 0, 0, 0, 1 };
|
||||||
|
config.opt<ConfigOptionStrings>("filament_mixed_components")->values = { "", "", "", "1,3" };
|
||||||
|
config.opt<ConfigOptionStrings>("filament_mixed_sublayer_ratios")->values = { "", "", "", "0.6,0.4" };
|
||||||
|
config.opt<ConfigOptionBools>("filament_mixed_gradient")->values = { 0, 0, 0, 1 };
|
||||||
|
config.opt<ConfigOptionStrings>("filament_mixed_gradient_range")->values = { "", "", "", "0.9,0.1" };
|
||||||
|
config.opt<ConfigOptionStrings>("filament_mixed_gradient_curve")->values = { "", "", "", "0,0.1|1,0.9" };
|
||||||
|
config.opt<ConfigOptionBools>("filament_mixed_gradient_per_part")->values = { 0, 0, 0, 1 };
|
||||||
|
|
||||||
|
PublishedMaterialEntry mix;
|
||||||
|
mix.slot = 3;
|
||||||
|
mix.filament_type = "PLA";
|
||||||
|
mix.publish_color = true;
|
||||||
|
mix.color = "#800080";
|
||||||
|
mix.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" };
|
||||||
|
|
||||||
|
PublishedConfig pub;
|
||||||
|
pub.published = true;
|
||||||
|
pub.material_keys = { make_real_entry(0, "#FF0000"), make_real_entry(1, "#000000"),
|
||||||
|
make_real_entry(2, "#0000FF"), mix };
|
||||||
|
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_FALSE(is_mixed[0]);
|
||||||
|
CHECK_FALSE(is_mixed[1]);
|
||||||
|
CHECK_FALSE(is_mixed[2]);
|
||||||
|
CHECK(is_mixed[3]); // receiver's mix, displaced to slot 3 first
|
||||||
|
CHECK(is_mixed[4]); // payload's mix, appended after
|
||||||
|
const auto &components = bundle.project_config.opt<ConfigOptionStrings>("filament_mixed_components")->values;
|
||||||
|
REQUIRE(components.size() == 5);
|
||||||
|
CHECK(components[3] == "1,2");
|
||||||
|
CHECK(components[4] == "1,3");
|
||||||
|
REQUIRE(pub.mixed_slot_relocations.size() == 2);
|
||||||
|
CHECK(pub.mixed_slot_relocations.at(2) == 3);
|
||||||
|
CHECK(pub.mixed_slot_relocations.at(3) == 4);
|
||||||
|
CHECK(pub.skipped_keys.empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Multiple receiver mixed slots interleaved with multiple incoming physical slots all rebalance
|
||||||
|
// onto consecutive tail slots in index order (no cascade/overlap).
|
||||||
|
TEST_CASE("Published 3MF rebalances several receiver mixed slots past the physical region", "[Preset][Bundle][Published]")
|
||||||
|
{
|
||||||
|
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" };
|
||||||
|
bundle.set_num_filaments(3, "#123456");
|
||||||
|
bundle.printers.get_edited_preset().config.opt<ConfigOptionBool>("single_extruder_multi_material", true)->value = true;
|
||||||
|
// Receiver: slot 1 and slot 2 are mixed.
|
||||||
|
bundle.project_config.opt<ConfigOptionBools>("filament_is_mixed")->values[1] = 1;
|
||||||
|
bundle.project_config.opt<ConfigOptionBools>("filament_is_mixed")->values[2] = 1;
|
||||||
|
bundle.project_config.opt<ConfigOptionStrings>("filament_mixed_components")->values[1] = "1,2";
|
||||||
|
bundle.project_config.opt<ConfigOptionStrings>("filament_mixed_components")->values[2] = "1,3";
|
||||||
|
bundle.project_config.opt<ConfigOptionStrings>("filament_mixed_sublayer_ratios")->values[1] = "0.5,0.5";
|
||||||
|
bundle.project_config.opt<ConfigOptionStrings>("filament_mixed_sublayer_ratios")->values[2] = "0.4,0.6";
|
||||||
|
|
||||||
|
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
|
||||||
|
config.opt<ConfigOptionFloats>("filament_diameter")->values = { 1.75, 1.75, 1.75 };
|
||||||
|
config.opt<ConfigOptionInts>("filament_self_index")->values = { 1, 2, 3 };
|
||||||
|
config.opt<ConfigOptionStrings>("filament_extruder_variant")->values = {
|
||||||
|
"Direct Drive Standard", "Direct Drive Standard", "Direct Drive Standard"
|
||||||
|
};
|
||||||
|
config.opt<ConfigOptionStrings>("filament_colour")->values = { "#FF0000", "#00AA00", "#0000FF" };
|
||||||
|
config.opt<ConfigOptionStrings>("filament_type")->values = { "PLA", "PLA", "PLA" };
|
||||||
|
config.opt<ConfigOptionStrings>("filament_vendor")->values = { "Generic", "Generic", "Generic" };
|
||||||
|
config.opt<ConfigOptionStrings>("filament_ids")->values = { "GFL99", "GFL99", "GFL99" };
|
||||||
|
|
||||||
|
auto make_real_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;
|
||||||
|
};
|
||||||
|
|
||||||
|
PublishedConfig pub;
|
||||||
|
pub.published = true;
|
||||||
|
pub.material_keys = { make_real_entry(0, "#FF0000"), make_real_entry(1, "#00AA00"), make_real_entry(2, "#0000FF") };
|
||||||
|
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 < 3; ++i)
|
||||||
|
CHECK_FALSE(is_mixed[i]);
|
||||||
|
CHECK(is_mixed[3]);
|
||||||
|
CHECK(is_mixed[4]);
|
||||||
|
const auto &components = bundle.project_config.opt<ConfigOptionStrings>("filament_mixed_components")->values;
|
||||||
|
REQUIRE(components.size() == 5);
|
||||||
|
CHECK(components[3] == "1,2");
|
||||||
|
CHECK(components[4] == "1,3");
|
||||||
|
REQUIRE(pub.mixed_slot_relocations.size() == 2);
|
||||||
|
CHECK(pub.mixed_slot_relocations.at(1) == 3);
|
||||||
|
CHECK(pub.mixed_slot_relocations.at(2) == 4);
|
||||||
|
CHECK(pub.skipped_keys.empty());
|
||||||
|
}
|
||||||
|
|
||||||
// The receiver's printer gates how many PHYSICAL filament slots a published 3MF may add: a
|
// 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
|
// 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
|
// count cannot become a physical filament. It becomes an empty mixed-filament placeholder
|
||||||
|
|||||||
Reference in New Issue
Block a user