mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-12 03:27:55 +00:00
Fixed issues with remapping mixed filaments when importing published 3MF
This commit is contained in:
@@ -3598,6 +3598,15 @@ void FacetsAnnotation::shift_states_above(const ModelVolume &mv, EnforcerBlocker
|
|||||||
this->set(selector);
|
this->set(selector);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void FacetsAnnotation::remap_states(const ModelVolume &mv, const EnforcerBlockerStateMap &state_map)
|
||||||
|
{
|
||||||
|
if (empty()) return;
|
||||||
|
TriangleSelector selector(mv.mesh());
|
||||||
|
selector.deserialize(m_data, false);
|
||||||
|
selector.remap_triangle_state(state_map);
|
||||||
|
this->set(selector);
|
||||||
|
}
|
||||||
|
|
||||||
void FacetsAnnotation::set_enforcer_block_type_limit(const ModelVolume &mv,
|
void FacetsAnnotation::set_enforcer_block_type_limit(const ModelVolume &mv,
|
||||||
EnforcerBlockerType max_type,
|
EnforcerBlockerType max_type,
|
||||||
EnforcerBlockerType to_delete_filament,
|
EnforcerBlockerType to_delete_filament,
|
||||||
@@ -3862,6 +3871,43 @@ bool model_has_advanced_features(const Model &model)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void remap_model_filament_slots(Model &model, const std::map<int, int> &slot_relocations)
|
||||||
|
{
|
||||||
|
if (slot_relocations.empty())
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Paint states and the object/volume "extruder" configs store one-based slot numbers
|
||||||
|
// (see Sidebar::on_action_add_filament's insertion remap for the same encoding).
|
||||||
|
std::map<int, int> one_based_slots;
|
||||||
|
for (const auto &[from, to] : slot_relocations)
|
||||||
|
one_based_slots.emplace(from + 1, to + 1);
|
||||||
|
|
||||||
|
EnforcerBlockerStateMap paint_state_map;
|
||||||
|
for (size_t state = 0; state < paint_state_map.size(); ++state)
|
||||||
|
paint_state_map[state] = EnforcerBlockerType(state);
|
||||||
|
for (const auto &[one_based_from, one_based_to] : one_based_slots) {
|
||||||
|
assert(one_based_from >= 0 && size_t(one_based_from) < paint_state_map.size());
|
||||||
|
assert(one_based_to > 0 && size_t(one_based_to) < paint_state_map.size());
|
||||||
|
paint_state_map[size_t(one_based_from)] = EnforcerBlockerType(one_based_to);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto remap_extruder_config = [&one_based_slots](ModelConfig &config) -> bool {
|
||||||
|
const auto it = config.has("extruder") ? one_based_slots.find(config.extruder()) : one_based_slots.end();
|
||||||
|
if (it == one_based_slots.end())
|
||||||
|
return false;
|
||||||
|
config.set("extruder", it->second);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
for (ModelObject *object : model.objects) {
|
||||||
|
remap_extruder_config(object->config);
|
||||||
|
for (ModelVolume *volume : object->volumes) {
|
||||||
|
remap_extruder_config(volume->config);
|
||||||
|
volume->mmu_segmentation_facets.remap_states(*volume, paint_state_map);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#ifndef NDEBUG
|
#ifndef NDEBUG
|
||||||
// Verify whether the IDs of Model / ModelObject / ModelVolume / ModelInstance / ModelMaterial are valid and unique.
|
// Verify whether the IDs of Model / ModelObject / ModelVolume / ModelInstance / ModelMaterial are valid and unique.
|
||||||
void check_model_ids_validity(const Model &model)
|
void check_model_ids_validity(const Model &model)
|
||||||
|
|||||||
@@ -745,6 +745,10 @@ public:
|
|||||||
// Shift painted filament indices >= threshold by delta. Used when a physical filament is
|
// Shift painted filament indices >= threshold by delta. Used when a physical filament is
|
||||||
// inserted ahead of existing slots (mixed-color slots are kept at the end of the list).
|
// inserted ahead of existing slots (mixed-color slots are kept at the end of the list).
|
||||||
void shift_states_above(const ModelVolume &mv, EnforcerBlockerType threshold, int delta);
|
void shift_states_above(const ModelVolume &mv, EnforcerBlockerType threshold, int delta);
|
||||||
|
// Relabel painted filament indices according to state_map (old state value -> new state
|
||||||
|
// value; untouched states keep their identity). Used when published-3MF import relocates
|
||||||
|
// mixed-filament definitions onto new slot numbers.
|
||||||
|
void remap_states(const ModelVolume &mv, const EnforcerBlockerStateMap &state_map);
|
||||||
indexed_triangle_set get_facets_strict(const ModelVolume& mv, EnforcerBlockerType type) const;
|
indexed_triangle_set get_facets_strict(const ModelVolume& mv, EnforcerBlockerType type) const;
|
||||||
bool has_facets(const ModelVolume& mv, EnforcerBlockerType type) const;
|
bool has_facets(const ModelVolume& mv, EnforcerBlockerType type) const;
|
||||||
bool empty() const { return m_data.triangles_to_split.empty(); }
|
bool empty() const { return m_data.triangles_to_split.empty(); }
|
||||||
@@ -1790,6 +1794,13 @@ bool model_has_multi_part_objects(const Model &model);
|
|||||||
// If the model has advanced features, then it cannot be processed in simple mode.
|
// If the model has advanced features, then it cannot be processed in simple mode.
|
||||||
bool model_has_advanced_features(const Model &model);
|
bool model_has_advanced_features(const Model &model);
|
||||||
|
|
||||||
|
// Remap the model's filament-slot references after a published-3MF import relocated
|
||||||
|
// mixed-filament definitions onto new slot numbers: object/volume "extruder" configs and
|
||||||
|
// multi-material color-painting states (paint state stores the one-based slot number).
|
||||||
|
// slot_relocations maps the author's zero-based slot number to its final zero-based slot;
|
||||||
|
// entries are applied simultaneously (no chained lookups), untouched slots keep everything.
|
||||||
|
void remap_model_filament_slots(Model &model, const std::map<int, int> &slot_relocations);
|
||||||
|
|
||||||
#ifndef NDEBUG
|
#ifndef NDEBUG
|
||||||
// Verify whether the IDs of Model / ModelObject / ModelVolume / ModelInstance / ModelMaterial are valid and unique.
|
// Verify whether the IDs of Model / ModelObject / ModelVolume / ModelInstance / ModelMaterial are valid and unique.
|
||||||
void check_model_ids_validity(const Model &model);
|
void check_model_ids_validity(const Model &model);
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
#include "Utils.hpp"
|
#include "Utils.hpp"
|
||||||
#include "LocalesUtils.hpp"
|
#include "LocalesUtils.hpp"
|
||||||
#include "Model.hpp"
|
#include "Model.hpp"
|
||||||
|
#include "TriangleSelector.hpp"
|
||||||
#include "libslic3r_version.h"
|
#include "libslic3r_version.h"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
@@ -4871,6 +4872,49 @@ static std::map<std::string, std::map<std::string, std::string>> filament_preset
|
|||||||
{{"Bambu PETG HF @BBL H2D 0.6 nozzle", "Bambu PETG HF @BBL H2D 0.8 nozzle"},
|
{{"Bambu PETG HF @BBL H2D 0.6 nozzle", "Bambu PETG HF @BBL H2D 0.8 nozzle"},
|
||||||
{"Bambu ASA @BBL H2D 0.6 nozzle", "Bambu ASA @BBL H2D 0.8 nozzle"}}}};
|
{"Bambu ASA @BBL H2D 0.6 nozzle", "Bambu ASA @BBL H2D 0.8 nozzle"}}}};
|
||||||
|
|
||||||
|
// Relocate the per-slot cells of one mixed-filament project vector inside the imported
|
||||||
|
// config, applying every authored-slot -> destination move at once. Each move reads its
|
||||||
|
// source cell from a frozen snapshot of the config's current cells, so relocations never
|
||||||
|
// cross-contaminate when an earlier destination overlaps a later source (adjacent published
|
||||||
|
// tail mixes relocate onto consecutive slots). Cells the snapshot lacks degrade to
|
||||||
|
// empty/false defaults - the missing-data handling in the material pass reports them
|
||||||
|
// downstream. Left-behind source cells stay as-is: nothing else consumes the imported config
|
||||||
|
// at those indices in published mode.
|
||||||
|
static void apply_mixed_config_relocations(DynamicPrintConfig& config,
|
||||||
|
const std::string& key,
|
||||||
|
const std::vector<std::pair<size_t, size_t>>& moves)
|
||||||
|
{
|
||||||
|
ConfigOption* opt = config.optptr(key);
|
||||||
|
if (opt == nullptr || moves.empty())
|
||||||
|
return;
|
||||||
|
std::unique_ptr<ConfigOption> snapshot(opt->clone());
|
||||||
|
switch (opt->type()) {
|
||||||
|
case coBools: {
|
||||||
|
auto* live = static_cast<ConfigOptionBools*>(opt);
|
||||||
|
const auto* frozen = static_cast<const ConfigOptionBools*>(snapshot.get());
|
||||||
|
for (const auto [from, to] : moves) {
|
||||||
|
const unsigned char cell = from < frozen->values.size() ? frozen->values[from] : 0;
|
||||||
|
if (live->values.size() <= to)
|
||||||
|
live->values.resize(to + 1, 0);
|
||||||
|
live->values[to] = cell;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case coStrings: {
|
||||||
|
auto* live = static_cast<ConfigOptionStrings*>(opt);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 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)
|
||||||
{
|
{
|
||||||
@@ -5379,12 +5423,87 @@ void PresetBundle::load_config_file_config(const std::string& name_or_path,
|
|||||||
if (entry.slot >= 0)
|
if (entry.slot >= 0)
|
||||||
grow_target = std::max(grow_target, size_t(entry.slot) + 1);
|
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
|
||||||
|
// silently convert hardware-backed state into a virtual mix. Compute each mixed
|
||||||
|
// entry's destination before anything consumes entry.slot (growth, seeding,
|
||||||
|
// de-aliasing, the overlay below):
|
||||||
|
// - a definition landing on a receiver slot that already carries a mixed
|
||||||
|
// definition keeps its place (a like-for-like override of a virtual slot);
|
||||||
|
// - everything else goes through one monotone append counter preserving author
|
||||||
|
// order: dest = max(authored, next_free). With a receiver shorter than the
|
||||||
|
// publish this keeps the authored positions intact; past them (or around a
|
||||||
|
// collision with a real filament) the mixes pack onto consecutive fresh slots
|
||||||
|
// AFTER every positional (real-filament) territory. The definition's cells are
|
||||||
|
// shifted inside the file-side per-slot mixed arrays so they stay readable
|
||||||
|
// 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();
|
||||||
|
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
|
||||||
|
// destination can overlap a later move's source: adjacent tail mixes relocate
|
||||||
|
// onto consecutive slots, so incremental in-place shifts would overwrite a
|
||||||
|
// definition that has not been moved yet).
|
||||||
|
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))) {
|
||||||
|
++entry_it;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
published_config->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";
|
||||||
|
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));
|
||||||
|
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;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!mixed_moves.empty())
|
||||||
|
for (const std::string& mixed_key : mixed_definitions)
|
||||||
|
apply_mixed_config_relocations(config, mixed_key, mixed_moves);
|
||||||
if (has_published_entries) {
|
if (has_published_entries) {
|
||||||
// Defensive cap: growth never exceeds the file's own filament count. The
|
// 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
|
// 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
|
// project vectors are ever shrunk, even when the file carries fewer filaments
|
||||||
// than the receiver has slots.
|
// than the receiver has slots. Relocated mixed entries legitimately land past
|
||||||
const size_t target_slots = std::max(this->filament_presets.size(), std::min(grow_target, num_filaments));
|
// the file's own slot count (virtual slots consume no nozzle or tray), so
|
||||||
|
// their 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
|
// Slots carrying published content, steering the initial preset selection of
|
||||||
// newly grown slots.
|
// newly grown slots.
|
||||||
std::set<int> published_slots;
|
std::set<int> published_slots;
|
||||||
@@ -5999,9 +6118,7 @@ void PresetBundle::load_config_file_config(const std::string& name_or_path,
|
|||||||
// A mixed-definition entry carries the mix's blended colour for the
|
// A mixed-definition entry carries the mix's blended colour for the
|
||||||
// swatch only: never write it into the slot's (possibly shared) preset
|
// swatch only: never write it into the slot's (possibly shared) preset
|
||||||
// config, only into the project-level colour arrays.
|
// config, only into the project-level colour arrays.
|
||||||
const bool is_mixed_entry = std::any_of(entry.keys.begin(), entry.keys.end(), [&](const std::string& key) {
|
const bool is_mixed_entry = is_mixed_definition(entry);
|
||||||
return publish_mixed_keys().count(publish_base_key(key)) != 0;
|
|
||||||
});
|
|
||||||
if (!is_mixed_entry && recv != nullptr) {
|
if (!is_mixed_entry && recv != nullptr) {
|
||||||
// Create the key when the target preset lacks it: the colour is a
|
// Create the key when the target preset lacks it: the colour is a
|
||||||
// requirement, not an override.
|
// requirement, not an override.
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
#include "enum_bitmask.hpp"
|
#include "enum_bitmask.hpp"
|
||||||
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
#include <map>
|
||||||
#include <set>
|
#include <set>
|
||||||
#include <shared_mutex>
|
#include <shared_mutex>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
@@ -186,6 +187,11 @@ struct PublishedConfig
|
|||||||
// Human-readable notices of the slot material replacements performed while loading a
|
// Human-readable notices of the slot material replacements performed while loading a
|
||||||
// published project, for the load notification.
|
// published project, for the load notification.
|
||||||
std::vector<std::string> material_replacements;
|
std::vector<std::string> material_replacements;
|
||||||
|
// Mixed-filament entries that had to be moved off their authored slot on load (a real,
|
||||||
|
// physical filament occupied it): maps the author's zero-based slot number to its final
|
||||||
|
// zero-based slot. Consumers (e.g. model extruder/color-painting remapping) use this to
|
||||||
|
// keep geometry references pointing at the relocated definitions.
|
||||||
|
std::map<int, int> mixed_slot_relocations;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Bundle of Print + Filament + Printer presets.
|
// Bundle of Print + Filament + Printer presets.
|
||||||
|
|||||||
@@ -9053,6 +9053,17 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
|
|||||||
|
|
||||||
preset_bundle->load_config_model(filename.string(), std::move(config), file_version, &published_config);
|
preset_bundle->load_config_model(filename.string(), std::move(config), file_version, &published_config);
|
||||||
|
|
||||||
|
// Mixed-filament definitions that collided with one of the
|
||||||
|
// receiver's real slots were relocated during the preset load.
|
||||||
|
// Re-point the freshly parsed model's extruder references and
|
||||||
|
// color painting from the author's slot numbers to where each
|
||||||
|
// definition landed, so volumes colored with a mix follow it.
|
||||||
|
// Runs before the objects are handed over to the plater below.
|
||||||
|
if (load_model && !published_config.mixed_slot_relocations.empty())
|
||||||
|
Slic3r::remap_model_filament_slots(model, published_config.mixed_slot_relocations);
|
||||||
|
|
||||||
|
// BBS: notify the user about published settings that could not be applied.
|
||||||
|
|
||||||
// BBS: notify the user about published settings that could not be applied.
|
// BBS: notify the user about published settings that could not be applied.
|
||||||
if (!published_config.skipped_keys.empty()) {
|
if (!published_config.skipped_keys.empty()) {
|
||||||
NotificationManager* notify_manager = q->get_notification_manager();
|
NotificationManager* notify_manager = q->get_notification_manager();
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
|
|
||||||
#include "libslic3r/PresetBundle.hpp"
|
#include "libslic3r/PresetBundle.hpp"
|
||||||
#include "libslic3r/AppConfig.hpp"
|
#include "libslic3r/AppConfig.hpp"
|
||||||
|
#include "libslic3r/Model.hpp"
|
||||||
|
#include "libslic3r/TriangleMesh.hpp"
|
||||||
|
|
||||||
#include "test_utils.hpp"
|
#include "test_utils.hpp"
|
||||||
|
|
||||||
@@ -2577,8 +2579,9 @@ TEST_CASE("Published 3MF applies a mixed filament definition onto the receiver's
|
|||||||
return config;
|
return config;
|
||||||
};
|
};
|
||||||
|
|
||||||
// A receiver that already carries the mix slot at index 2 (e.g. a two-physical-plus-one-mix
|
// A receiver that already carries the mix slot at index 2 as an actual mixed slot (e.g. a
|
||||||
// project with the same layout).
|
// two-physical-plus-one-mix project with the same layout): the incoming definition is a
|
||||||
|
// like-for-like override of the virtual slot and applies in place without relocation.
|
||||||
{
|
{
|
||||||
PresetBundle bundle;
|
PresetBundle bundle;
|
||||||
Preset &pla = add_inmemory_preset(bundle.filaments, "My PLA");
|
Preset &pla = add_inmemory_preset(bundle.filaments, "My PLA");
|
||||||
@@ -2587,8 +2590,12 @@ TEST_CASE("Published 3MF applies a mixed filament definition onto the receiver's
|
|||||||
petg.config.opt_string("filament_type", 0u) = "PETG";
|
petg.config.opt_string("filament_type", 0u) = "PETG";
|
||||||
bundle.filament_presets = { "My PLA", "My PETG", "My PLA" };
|
bundle.filament_presets = { "My PLA", "My PETG", "My PLA" };
|
||||||
|
|
||||||
// Grow the receiver's project arrays to 3 slots first, as set_num_filaments would.
|
// Grow the receiver's project arrays to 3 slots first, as set_num_filaments would,
|
||||||
|
// then mark the third slot as the receiver's own mixed filament.
|
||||||
bundle.set_num_filaments(3);
|
bundle.set_num_filaments(3);
|
||||||
|
bundle.project_config.opt<ConfigOptionBools>("filament_is_mixed")->values[2] = 1;
|
||||||
|
bundle.project_config.opt<ConfigOptionStrings>("filament_mixed_components")->values[2] = "1,1";
|
||||||
|
bundle.project_config.opt<ConfigOptionStrings>("filament_mixed_sublayer_ratios")->values[2] = "0.5,0.5";
|
||||||
|
|
||||||
PublishedMaterialEntry mix;
|
PublishedMaterialEntry mix;
|
||||||
mix.filament_type = "PLA";
|
mix.filament_type = "PLA";
|
||||||
@@ -2636,6 +2643,8 @@ TEST_CASE("Published 3MF applies a mixed filament definition onto the receiver's
|
|||||||
CHECK_FALSE(is_mixed[1]);
|
CHECK_FALSE(is_mixed[1]);
|
||||||
// Nothing skipped: every serialized mixed key was applied.
|
// Nothing skipped: every serialized mixed key was applied.
|
||||||
CHECK(pub.skipped_keys.empty());
|
CHECK(pub.skipped_keys.empty());
|
||||||
|
// Like-for-like override: no slot was relocated.
|
||||||
|
CHECK(pub.material_replacements.empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
// A receiver with fewer slots: the slot is grown and seeded before the definition applies.
|
// A receiver with fewer slots: the slot is grown and seeded before the definition applies.
|
||||||
@@ -2701,6 +2710,228 @@ TEST_CASE("Published 3MF reports an unappliable mixed filament definition as ski
|
|||||||
CHECK(contains_key(pub.skipped_keys, "material: (filament_mixed_sublayer_ratios)"));
|
CHECK(contains_key(pub.skipped_keys, "material: (filament_mixed_sublayer_ratios)"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A published mixed filament must never convert one of the receiver's real, physical slots
|
||||||
|
// into a virtual mix: definitions that collide with a physical slot are relocated past every
|
||||||
|
// positional (real-filament) destination, while ones colliding with an existing mixed slot
|
||||||
|
// override it in place.
|
||||||
|
TEST_CASE("Published 3MF relocates a mixed filament instead of overwriting a physical slot", "[Preset][Bundle][Published]")
|
||||||
|
{
|
||||||
|
// An author project with <num_author_slots> slots whose last slot is a mixed filament.
|
||||||
|
auto make_file_config = [](size_t num_author_slots, size_t num_tail_mixes = 1) {
|
||||||
|
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;
|
||||||
|
std::vector<std::string> types;
|
||||||
|
for (size_t i = 0; i < num_author_slots; ++i) {
|
||||||
|
self_index.push_back(int(i + 1));
|
||||||
|
variants.emplace_back("Direct Drive Standard");
|
||||||
|
types.push_back(i % 2 == 0 ? "PLA" : "PETG");
|
||||||
|
}
|
||||||
|
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 = { "#FF0000", "#00AA00", "#0000FF", "#FFFF00", "#800080" };
|
||||||
|
config.opt<ConfigOptionStrings>("filament_colour")->values.resize(num_author_slots, "#808080");
|
||||||
|
config.opt<ConfigOptionStrings>("filament_type")->values = types;
|
||||||
|
config.opt<ConfigOptionStrings>("filament_vendor")->values.assign(num_author_slots, "Generic");
|
||||||
|
config.opt<ConfigOptionStrings>("filament_ids")->values.resize(num_author_slots);
|
||||||
|
// The last <num_tail_mixes> author slots are mixed ones (components differ per slot so
|
||||||
|
// the definitions are distinguishable after relocation).
|
||||||
|
const size_t first_mix_slot = num_author_slots - num_tail_mixes;
|
||||||
|
config.opt<ConfigOptionBools>("filament_is_mixed")->values.assign(num_author_slots, 0);
|
||||||
|
config.opt<ConfigOptionStrings>("filament_mixed_components")->values.assign(num_author_slots, "");
|
||||||
|
config.opt<ConfigOptionStrings>("filament_mixed_sublayer_ratios")->values.assign(num_author_slots, "");
|
||||||
|
for (size_t i = first_mix_slot; i < num_author_slots; ++i) {
|
||||||
|
config.opt<ConfigOptionBools>("filament_is_mixed")->values[i] = 1;
|
||||||
|
config.opt<ConfigOptionStrings>("filament_mixed_components")->values[i] =
|
||||||
|
i % 2 == 0 ? std::string("1,2") : std::string("1,3");
|
||||||
|
config.opt<ConfigOptionStrings>("filament_mixed_sublayer_ratios")->values[i] =
|
||||||
|
i % 2 == 0 ? std::string("0.6,0.4") : std::string("0.3,0.7");
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
};
|
||||||
|
|
||||||
|
// The reported bug: an author publishes with physical filaments on slots 1-2 and a mixed
|
||||||
|
// filament on slot 5; the receiver runs five real filaments of his own. Slot 5 must stay
|
||||||
|
// untouched and the mix lands as a newly appended virtual slot 6.
|
||||||
|
{
|
||||||
|
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", "My PLA" };
|
||||||
|
bundle.set_num_filaments(5, "#123456");
|
||||||
|
const std::vector<std::string> receiver_colours =
|
||||||
|
bundle.project_config.opt<ConfigOptionStrings>("filament_colour")->values;
|
||||||
|
|
||||||
|
PublishedMaterialEntry mix;
|
||||||
|
mix.filament_type = "PLA";
|
||||||
|
mix.filament_vendor = "Generic";
|
||||||
|
mix.slot = 4;
|
||||||
|
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 = { mix };
|
||||||
|
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 extra virtual slot.
|
||||||
|
REQUIRE(bundle.filament_presets.size() == 6);
|
||||||
|
// All five physical slots kept their meaning: no mixed flag, untouched names/colours.
|
||||||
|
const auto &is_mixed = bundle.project_config.opt<ConfigOptionBools>("filament_is_mixed")->values;
|
||||||
|
REQUIRE(is_mixed.size() == 6);
|
||||||
|
CHECK_FALSE(is_mixed[0]);
|
||||||
|
CHECK_FALSE(is_mixed[1]);
|
||||||
|
CHECK_FALSE(is_mixed[2]);
|
||||||
|
CHECK_FALSE(is_mixed[3]);
|
||||||
|
CHECK_FALSE(is_mixed[4]);
|
||||||
|
CHECK(is_mixed[5]);
|
||||||
|
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[4] == "My PLA");
|
||||||
|
// The definition itself is readable at the new index.
|
||||||
|
const auto &components = bundle.project_config.opt<ConfigOptionStrings>("filament_mixed_components")->values;
|
||||||
|
REQUIRE(components.size() == 6);
|
||||||
|
CHECK(components[5] == "1,2");
|
||||||
|
const auto &ratios = bundle.project_config.opt<ConfigOptionStrings>("filament_mixed_sublayer_ratios")->values;
|
||||||
|
REQUIRE(ratios.size() == 6);
|
||||||
|
CHECK(ratios[5] == "0.6,0.4");
|
||||||
|
// The blended colour seeds the swatch of the new slot only.
|
||||||
|
const auto &colour = bundle.project_config.opt<ConfigOptionStrings>("filament_colour")->values;
|
||||||
|
REQUIRE(colour.size() == 6);
|
||||||
|
CHECK(colour[5] == "#800080");
|
||||||
|
// The relocation is surfaced to the user through the post-import notice (the de-alias
|
||||||
|
// pass may contribute further messages, so presence is asserted, not the count).
|
||||||
|
bool relocated_reported = false;
|
||||||
|
for (const std::string &message : pub.material_replacements)
|
||||||
|
if (message.find("slot 4 -> slot 5") != std::string::npos)
|
||||||
|
relocated_reported = true;
|
||||||
|
CHECK(relocated_reported);
|
||||||
|
CHECK(pub.skipped_keys.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
// A definition colliding with the receiver's own mixed filament is overridden in place:
|
||||||
|
// nothing grows, nothing is reported as moved.
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
bundle.project_config.opt<ConfigOptionBools>("filament_is_mixed")->values[2] = 1;
|
||||||
|
bundle.project_config.opt<ConfigOptionStrings>("filament_mixed_components")->values[2] = "1,1";
|
||||||
|
bundle.project_config.opt<ConfigOptionStrings>("filament_mixed_sublayer_ratios")->values[2] = "0.9,0.1";
|
||||||
|
|
||||||
|
PublishedMaterialEntry mix;
|
||||||
|
mix.slot = 2;
|
||||||
|
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(3);
|
||||||
|
Preset::normalize(config);
|
||||||
|
bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub);
|
||||||
|
|
||||||
|
CHECK(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] == "1,2");
|
||||||
|
const auto &ratios = bundle.project_config.opt<ConfigOptionStrings>("filament_mixed_sublayer_ratios")->values;
|
||||||
|
REQUIRE(ratios.size() == 3);
|
||||||
|
CHECK(ratios[2] == "0.6,0.4");
|
||||||
|
CHECK(pub.skipped_keys.empty());
|
||||||
|
CHECK(pub.material_replacements.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Author publishes four physical filaments plus two mixed ones on slots 5 and 6; the
|
||||||
|
// receiver runs five real filaments. Both mixes relocate onto consecutive fresh slots,
|
||||||
|
// preserving their author order (slot 5 -> slot 6, slot 6 -> slot 7); no receiver slot is
|
||||||
|
// converted into a virtual mix.
|
||||||
|
{
|
||||||
|
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", "My PLA" };
|
||||||
|
bundle.set_num_filaments(5, "#123456");
|
||||||
|
const std::vector<std::string> receiver_colours =
|
||||||
|
bundle.project_config.opt<ConfigOptionStrings>("filament_colour")->values;
|
||||||
|
|
||||||
|
auto make_mix_entry = [](int authored_slot, const char *color) {
|
||||||
|
PublishedMaterialEntry entry;
|
||||||
|
entry.slot = authored_slot;
|
||||||
|
entry.publish_color = true;
|
||||||
|
entry.color = color;
|
||||||
|
entry.keys = { "filament_is_mixed", "filament_mixed_components", "filament_mixed_sublayer_ratios" };
|
||||||
|
return entry;
|
||||||
|
};
|
||||||
|
PublishedMaterialEntry mix_a = make_mix_entry(4, "#800080");
|
||||||
|
PublishedMaterialEntry mix_b = make_mix_entry(5, "#FF69B4");
|
||||||
|
|
||||||
|
PublishedConfig pub;
|
||||||
|
pub.published = true;
|
||||||
|
pub.material_keys = { mix_a, mix_b };
|
||||||
|
DynamicPrintConfig config = make_file_config(6, 2);
|
||||||
|
Preset::normalize(config);
|
||||||
|
bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub);
|
||||||
|
|
||||||
|
// Two fresh virtual slots were appended.
|
||||||
|
REQUIRE(bundle.filament_presets.size() == 7);
|
||||||
|
const auto &is_mixed = bundle.project_config.opt<ConfigOptionBools>("filament_is_mixed")->values;
|
||||||
|
REQUIRE(is_mixed.size() == 7);
|
||||||
|
for (size_t i = 0; i < 5; ++i)
|
||||||
|
CHECK_FALSE(is_mixed[i]);
|
||||||
|
CHECK(is_mixed[5]);
|
||||||
|
CHECK(is_mixed[6]);
|
||||||
|
// The definitions follow their author order.
|
||||||
|
const auto &components = bundle.project_config.opt<ConfigOptionStrings>("filament_mixed_components")->values;
|
||||||
|
REQUIRE(components.size() == 7);
|
||||||
|
CHECK(components[5] == "1,2");
|
||||||
|
CHECK(components[6] == "1,3");
|
||||||
|
const auto &ratios = bundle.project_config.opt<ConfigOptionStrings>("filament_mixed_sublayer_ratios")->values;
|
||||||
|
REQUIRE(ratios.size() == 7);
|
||||||
|
CHECK(ratios[5] == "0.6,0.4");
|
||||||
|
CHECK(ratios[6] == "0.3,0.7");
|
||||||
|
// The five real slots kept their colours; each mix's blended colour seeded its new slot.
|
||||||
|
const auto &colour = bundle.project_config.opt<ConfigOptionStrings>("filament_colour")->values;
|
||||||
|
REQUIRE(colour.size() == 7);
|
||||||
|
CHECK(std::equal(receiver_colours.begin(), receiver_colours.end(), colour.begin()));
|
||||||
|
CHECK(colour[5] == "#800080");
|
||||||
|
CHECK(colour[6] == "#FF69B4");
|
||||||
|
// Both relocations are reported with the correct mapping.
|
||||||
|
bool a_reported = false, b_reported = false;
|
||||||
|
for (const std::string &message : pub.material_replacements) {
|
||||||
|
if (message.find("slot 4 -> slot 5") != std::string::npos)
|
||||||
|
a_reported = true;
|
||||||
|
if (message.find("slot 5 -> slot 6") != std::string::npos)
|
||||||
|
b_reported = true;
|
||||||
|
}
|
||||||
|
CHECK(a_reported);
|
||||||
|
CHECK(b_reported);
|
||||||
|
CHECK(pub.skipped_keys.empty());
|
||||||
|
// The relocation table is exposed for the model-reference remapping.
|
||||||
|
REQUIRE(pub.mixed_slot_relocations.size() == 2);
|
||||||
|
CHECK(pub.mixed_slot_relocations.at(4) == 5);
|
||||||
|
CHECK(pub.mixed_slot_relocations.at(5) == 6);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// A single-extruder receiver collapses the author's per-extruder printer slots onto its single
|
// 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
|
// slot: the first serialized variant of a base key is applied, the remaining variants of that
|
||||||
// base key are reported as skipped.
|
// base key are reported as skipped.
|
||||||
@@ -2980,3 +3211,71 @@ TEST_CASE("Sizing down to the nozzle count plus mixes is what eats the mixed tai
|
|||||||
CHECK(bundle.project_config.option<ConfigOptionStrings>("filament_mixed_components")->values[5] == "1,2");
|
CHECK(bundle.project_config.option<ConfigOptionStrings>("filament_mixed_components")->values[5] == "1,2");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// After a published-3MF import relocated mixed-filament definitions, the freshly loaded
|
||||||
|
// model's slot references must follow: object/volume "extruder" configs and multi-material
|
||||||
|
// color-painting states (which store the one-based slot number) are re-pointed to where each
|
||||||
|
// definition landed; everything else keeps its state.
|
||||||
|
TEST_CASE("remap_model_filament_slots repoints extruder configs and color painting", "[Preset][Bundle][Published]")
|
||||||
|
{
|
||||||
|
auto make_model = [] {
|
||||||
|
Model model;
|
||||||
|
ModelObject *object_a = model.add_object();
|
||||||
|
object_a->name = "relocated mix";
|
||||||
|
ModelVolume *vol_a = object_a->add_volume(make_cube(10., 10., 10.));
|
||||||
|
vol_a->config.set_key_value("extruder", new ConfigOptionInt(5)); // author slot 5 (0-based 4)
|
||||||
|
// Author painted one facet with the mix (slot 5) and another with a physical (slot 2).
|
||||||
|
{
|
||||||
|
TriangleSelector selector(vol_a->mesh());
|
||||||
|
selector.set_facet(0, EnforcerBlockerType(5));
|
||||||
|
selector.set_facet(1, EnforcerBlockerType(2));
|
||||||
|
vol_a->mmu_segmentation_facets.set_data(selector.serialize());
|
||||||
|
}
|
||||||
|
// A second object that does not reference the relocated slot at all. Painted with a
|
||||||
|
// real, non-relocated state (NONE is never serialized: an unsplit triangle without a
|
||||||
|
// state is the unpainted default and is skipped by TriangleSelector::serialize()).
|
||||||
|
ModelObject *object_b = model.add_object();
|
||||||
|
object_b->name = "untouched";
|
||||||
|
object_b->config.set_key_value("extruder", new ConfigOptionInt(1));
|
||||||
|
ModelVolume *vol_b = object_b->add_volume(make_cube(5., 5., 5.));
|
||||||
|
vol_b->config.set_key_value("extruder", new ConfigOptionInt(2));
|
||||||
|
{
|
||||||
|
TriangleSelector selector(vol_b->mesh());
|
||||||
|
selector.set_facet(0, EnforcerBlockerType(2));
|
||||||
|
vol_b->mmu_segmentation_facets.set_data(selector.serialize());
|
||||||
|
}
|
||||||
|
return model;
|
||||||
|
};
|
||||||
|
|
||||||
|
const std::map<int, int> relocations = {{4, 5}};
|
||||||
|
|
||||||
|
Model model = make_model();
|
||||||
|
Slic3r::remap_model_filament_slots(model, relocations);
|
||||||
|
|
||||||
|
const ModelVolume *vol_a = model.objects[0]->volumes.front();
|
||||||
|
CHECK(vol_a->config.extruder() == 6); // author slot 5 -> final slot 6
|
||||||
|
// Painted states follow: the mix facet moved 5 -> 6, the physical one is untouched.
|
||||||
|
REQUIRE(TriangleSelector::has_facets(vol_a->mmu_segmentation_facets.get_data(), EnforcerBlockerType(6)));
|
||||||
|
REQUIRE_FALSE(TriangleSelector::has_facets(vol_a->mmu_segmentation_facets.get_data(), EnforcerBlockerType(5)));
|
||||||
|
CHECK(TriangleSelector::has_facets(vol_a->mmu_segmentation_facets.get_data(), EnforcerBlockerType(2)));
|
||||||
|
|
||||||
|
const ModelVolume *vol_b = model.objects[1]->volumes.front();
|
||||||
|
CHECK(vol_b->config.extruder() == 2);
|
||||||
|
// The untouched volume's paint (a non-relocated state) survives as-is.
|
||||||
|
CHECK(TriangleSelector::has_facets(vol_b->mmu_segmentation_facets.get_data(), EnforcerBlockerType(2)));
|
||||||
|
|
||||||
|
// The mapping is applied simultaneously: each entry reads the original slot number, so
|
||||||
|
// relocating onto another relocated-from slot number must not chase chains. With the
|
||||||
|
// 0-based relocations {3->4, 4->6} the 1-based config map is {4->5, 5->7}: a volume on
|
||||||
|
// 1-based slot 4 lands on 5 and does NOT continue to 7.
|
||||||
|
Model chained = make_model();
|
||||||
|
chained.objects[0]->volumes.front()->config.set_key_value("extruder", new ConfigOptionInt(4));
|
||||||
|
Slic3r::remap_model_filament_slots(chained, std::map<int, int>{{3, 4}, {4, 6}});
|
||||||
|
CHECK(chained.objects[0]->volumes.front()->config.extruder() == 5);
|
||||||
|
// The chained model's paint follows its own single-step mapping: painted state 5 -> 7,
|
||||||
|
// and nothing lands back on 5.
|
||||||
|
CHECK(TriangleSelector::has_facets(chained.objects[0]->volumes.front()->mmu_segmentation_facets.get_data(),
|
||||||
|
EnforcerBlockerType(7)));
|
||||||
|
CHECK_FALSE(TriangleSelector::has_facets(chained.objects[0]->volumes.front()->mmu_segmentation_facets.get_data(),
|
||||||
|
EnforcerBlockerType(5)));
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user