diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 645d8f416d..93a71dcd8d 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -149,6 +149,8 @@ void Print::clear() m_print_regions.clear(); m_model.clear_objects(); m_statistics_by_extruder_count.clear(); + m_same_shrinkage_compensations = false; + m_shrinkage_compensation = Vec3d::Ones(); } bool Print::has_tpu_filament() const @@ -584,32 +586,36 @@ std::vector Print::object_extruders() const for (const PrintRegion ®ion : object->all_regions()) region.collect_object_printing_extruders(*this, extruders); - for (const PrintObject* object : m_objects) { - const ModelObject* mo = object->model_object(); - for (const ModelVolume* mv : mo->volumes) { - std::vector volume_extruders = mv->get_extruders(); - for (int extruder : volume_extruders) { - assert(extruder > 0); - extruders.push_back(extruder - 1); - } - } - - // layer range - for (auto layer_range : mo->layer_config_ranges) { - if (layer_range.second.has("extruder")) { - //BBS: actually when user doesn't change filament by height range(value is default 0), height range should not save key "extruder". - //Don't know why height range always save key "extruder" because of no change(should only save difference)... - //Add protection here to avoid overflow - auto value = layer_range.second.option("extruder")->getInt(); - if (value > 0) - extruders.push_back(value - 1); - } - } - } + for (const PrintObject* object : m_objects) + append_model_object_extruders(*object->model_object(), extruders); sort_remove_duplicates(extruders); return extruders; } +// Appends 0-based indices of the filaments assigned to the volumes of model_object (painting included) and to its height ranges. +void Print::append_model_object_extruders(const ModelObject &mo, std::vector &extruders) +{ + for (const ModelVolume* mv : mo.volumes) { + std::vector volume_extruders = mv->get_extruders(); + for (int extruder : volume_extruders) { + assert(extruder > 0); + extruders.push_back(extruder - 1); + } + } + + // layer range + for (auto layer_range : mo.layer_config_ranges) { + if (layer_range.second.has("extruder")) { + //BBS: actually when user doesn't change filament by height range(value is default 0), height range should not save key "extruder". + //Don't know why height range always save key "extruder" because of no change(should only save difference)... + //Add protection here to avoid overflow + auto value = layer_range.second.option("extruder")->getInt(); + if (value > 0) + extruders.push_back(value - 1); + } + } +} + // returns 0-based indices of used extruders std::vector Print::support_material_extruders() const { @@ -619,22 +625,8 @@ std::vector Print::support_material_extruders() const auto num_extruders = (unsigned int)m_config.filament_diameter.size(); for (PrintObject *object : m_objects) { - if (object->has_support_material()) { - assert(object->config().support_filament >= 0); - if (object->config().support_filament == 0) - support_uses_current_extruder = true; - else { - unsigned int i = (unsigned int)object->config().support_filament - 1; - extruders.emplace_back((i >= num_extruders) ? 0 : i); - } - assert(object->config().support_interface_filament >= 0); - if (object->config().support_interface_filament == 0) - support_uses_current_extruder = true; - else { - unsigned int i = (unsigned int)object->config().support_interface_filament - 1; - extruders.emplace_back((i >= num_extruders) ? 0 : i); - } - } + if (object->has_support_material() && append_support_extruders(object->config(), num_extruders, extruders)) + support_uses_current_extruder = true; } if (support_uses_current_extruder) @@ -645,31 +637,61 @@ std::vector Print::support_material_extruders() const return extruders; } +// Appends 0-based indices of the support and support interface filaments of an object printing support material. +// Returns true if support or support interface is printed with the filament of the object. +bool Print::append_support_extruders(const PrintObjectConfig &config, unsigned int num_extruders, std::vector &extruders) +{ + bool uses_current_extruder = false; + assert(config.support_filament >= 0); + if (config.support_filament == 0) + uses_current_extruder = true; + else { + unsigned int i = (unsigned int)config.support_filament - 1; + extruders.emplace_back((i >= num_extruders) ? 0 : i); + } + assert(config.support_interface_filament >= 0); + if (config.support_interface_filament == 0) + uses_current_extruder = true; + else { + unsigned int i = (unsigned int)config.support_interface_filament - 1; + extruders.emplace_back((i >= num_extruders) ? 0 : i); + } + return uses_current_extruder; +} + // returns 0-based indices of used extruders std::vector Print::extruders(bool conside_custom_gcode) const { std::vector extruders = this->object_extruders(); append(extruders, this->support_material_extruders()); - if (conside_custom_gcode) { - //BBS - int num_extruders = m_config.filament_colour.size(); - if (m_model.plates_custom_gcodes.find(m_model.curr_plate_index) != m_model.plates_custom_gcodes.end()) { - for (auto item : m_model.plates_custom_gcodes.at(m_model.curr_plate_index).gcodes) { - if (item.type == CustomGCode::Type::ToolChange && item.extruder <= num_extruders) - extruders.push_back((unsigned int)(item.extruder - 1)); - } + if (conside_custom_gcode) + this->append_tool_change_extruders(extruders); + this->append_wipe_tower_extruder(extruders); + sort_remove_duplicates(extruders); + return extruders; +} + +// Appends 0-based indices of the filaments the tool changes of the current plate switch to. +void Print::append_tool_change_extruders(std::vector &extruders) const +{ + //BBS + int num_extruders = m_config.filament_colour.size(); + if (m_model.plates_custom_gcodes.find(m_model.curr_plate_index) != m_model.plates_custom_gcodes.end()) { + for (auto item : m_model.plates_custom_gcodes.at(m_model.curr_plate_index).gcodes) { + if (item.type == CustomGCode::Type::ToolChange && item.extruder <= num_extruders) + extruders.push_back((unsigned int)(item.extruder - 1)); } } +} +void Print::append_wipe_tower_extruder(std::vector &extruders) const +{ // If a wipe tower filament is explicitly set, ensure it participates in tool ordering. if (has_wipe_tower() && config().wipe_tower_filament != 0 && extruders.size() > 1) { assert(config().wipe_tower_filament > 0 && config().wipe_tower_filament <= int(config().filament_diameter.size())); extruders.emplace_back(config().wipe_tower_filament - 1); // config value is 1-based } - - sort_remove_duplicates(extruders); - return extruders; } unsigned int Print::num_object_instances() const @@ -5652,44 +5674,6 @@ std::string PrintStatistics::finalize_output_path(const std::string &path_in) co return final_path; } -// Orca: Implement prusa's filament shrink compensation approach -// Returns if all used filaments have same shrinkage compensations. - bool Print::has_same_shrinkage_compensations() const { - const std::vector extruders = this->extruders(); - if (extruders.empty()) - return false; - - const double filament_shrinkage_compensation_xy = m_config.filament_shrink.get_at(extruders.front()); - const double filament_shrinkage_compensation_z = m_config.filament_shrinkage_compensation_z.get_at(extruders.front()); - - for (unsigned int extruder : extruders) { - if (filament_shrinkage_compensation_xy != m_config.filament_shrink.get_at(extruder) || - filament_shrinkage_compensation_z != m_config.filament_shrinkage_compensation_z.get_at(extruder)) { - return false; - } - } - - return true; - } - -// Orca: Implement prusa's filament shrink compensation approach, but amended so 100% from the user is the equivalent to 0 in orca. - // Returns scaling for each axis representing shrinkage compensations in each axis. -Vec3d Print::shrinkage_compensation() const -{ - if (!this->has_same_shrinkage_compensations()) - return Vec3d::Ones(); - - const unsigned int first_extruder = this->extruders().front(); - - const double xy_shrinkage_percent = m_config.filament_shrink.get_at(first_extruder); - const double z_shrinkage_percent = m_config.filament_shrinkage_compensation_z.get_at(first_extruder); - - const double xy_compensation = 100.0 / xy_shrinkage_percent; - const double z_compensation = 100.0 / z_shrinkage_percent; - - return { xy_compensation, xy_compensation, z_compensation }; -} - const std::string PrintStatistics::FilamentUsedG = "filament used [g]"; const std::string PrintStatistics::FilamentUsedGMask = "; filament used [g] ="; diff --git a/src/libslic3r/Print.hpp b/src/libslic3r/Print.hpp index 735edac824..070e4f4ddc 100644 --- a/src/libslic3r/Print.hpp +++ b/src/libslic3r/Print.hpp @@ -503,9 +503,12 @@ public: std::vector> all_regions() const; const PrintObjectRegions* shared_regions() const throw() { return m_shared_regions; } - bool has_support() const { return m_config.enable_support || m_config.enforce_support_layers > 0; } - bool has_raft() const { return m_config.raft_layers > 0; } - bool has_support_material() const { return this->has_support() || this->has_raft(); } + static bool has_support(const PrintObjectConfig &config) { return config.enable_support || config.enforce_support_layers > 0; } + static bool has_raft(const PrintObjectConfig &config) { return config.raft_layers > 0; } + static bool has_support_material(const PrintObjectConfig &config) { return has_support(config) || has_raft(config); } + bool has_support() const { return has_support(m_config); } + bool has_raft() const { return has_raft(m_config); } + bool has_support_material() const { return has_support_material(m_config); } // Checks if the model object is painted using the multi-material painting gizmo. bool is_mm_painted() const { return this->model_object()->is_mm_painted(); } // Checks if the model object is painted using the fuzzy skin painting gizmo. @@ -1385,10 +1388,11 @@ public: size_t config_index_generation() const { return m_config_index_generation; } // Orca: Implement prusa's filament shrink compensation approach + // Both are decided by apply() from the filaments the model uses, before the PrintObject transformations are built. // Returns if all used filaments have same shrinkage compensations. - bool has_same_shrinkage_compensations() const; + bool has_same_shrinkage_compensations() const { return m_same_shrinkage_compensations; } // Returns scaling for each axis representing shrinkage compensations in each axis. - Vec3d shrinkage_compensation() const; + Vec3d shrinkage_compensation() const { return m_shrinkage_compensation; } std::tuple object_skirt_offset(double margin_height = 0) const; @@ -1484,11 +1488,19 @@ private: s_slicing_pipeline_hook_fn(*this, object, step); } + static void append_model_object_extruders(const ModelObject &mo, std::vector &extruders); + static bool append_support_extruders(const PrintObjectConfig &config, unsigned int num_extruders, std::vector &extruders); + void append_tool_change_extruders(std::vector &extruders) const; + void append_wipe_tower_extruder(std::vector &extruders) const; + void update_shrinkage_compensation(size_t num_extruders, std::vector &variant_index); + PrintConfig m_config; PrintObjectConfig m_default_object_config; PrintRegionConfig m_default_region_config; PrintObjectPtrs m_objects; PrintRegionPtrs m_print_regions; + bool m_same_shrinkage_compensations { false }; + Vec3d m_shrinkage_compensation { Vec3d::Ones() }; //SoftFever bool m_isBBLPrinter = false; diff --git a/src/libslic3r/PrintApply.cpp b/src/libslic3r/PrintApply.cpp index 5fed82211d..cb98f3550b 100644 --- a/src/libslic3r/PrintApply.cpp +++ b/src/libslic3r/PrintApply.cpp @@ -25,6 +25,7 @@ #include #include #include +#include #include #include #include @@ -1257,6 +1258,80 @@ static PrintObjectRegions* generate_print_object_regions( return out.release(); } +// Orca: The shrinkage compensation is baked into the PrintObject transformations, so it has to be known before apply() +// creates them. Derive it from the filaments the new model prints with instead of from the existing PrintObjects and +// their regions, which do not exist yet on the first apply(). Every feature filament of every part, height range and +// modifier counts, whether the feature ends up printing or not, so filaments that disagree switch the compensation +// off rather than letting a wrong compensation through. +void Print::update_shrinkage_compensation(size_t num_extruders, std::vector &variant_index) +{ + const auto set_compensation = [this](unsigned int extruder) { + // 100% from the user is the equivalent to 0 in orca. + const double xy_compensation = 100.0 / m_config.filament_shrink.get_at(extruder); + const double z_compensation = 100.0 / m_config.filament_shrinkage_compensation_z.get_at(extruder); + m_shrinkage_compensation = { xy_compensation, xy_compensation, z_compensation }; + }; + const auto same_shrinkage = [this](unsigned int a, unsigned int b) { + return m_config.filament_shrink.get_at(a) == m_config.filament_shrink.get_at(b) && + m_config.filament_shrinkage_compensation_z.get_at(a) == m_config.filament_shrinkage_compensation_z.get_at(b); + }; + // Usually all filaments share the same shrinkage, then it does not matter which of them print. + bool uniform = true; + for (unsigned int extruder = 1; extruder < num_extruders && uniform; ++ extruder) + uniform = same_shrinkage(0, extruder); + if (uniform) { + m_same_shrinkage_compensations = true; + set_compensation(0); + return; + } + + std::vector extruders; + const auto append_feature_extruders = [num_extruders, &extruders](const PrintRegionConfig &config) { + for (int id : { config.outer_wall_filament_id.value, config.inner_wall_filament_id.value, config.sparse_infill_filament_id.value, + config.internal_solid_filament_id.value, config.top_surface_filament_id.value, config.bottom_surface_filament_id.value }) { + const unsigned int i = (unsigned int)std::max(0, id - 1); + extruders.emplace_back(i >= num_extruders ? 0 : i); + } + }; + for (const ModelObject *model_object : m_model.objects) { + if (std::none_of(model_object->instances.begin(), model_object->instances.end(), [](const ModelInstance *mi) { return mi->is_printable(); })) + continue; + std::vector modifiers; + std::copy_if(model_object->volumes.begin(), model_object->volumes.end(), std::back_inserter(modifiers), [](const ModelVolume *mv) { return mv->is_modifier(); }); + for (const ModelVolume *part : model_object->volumes) { + if (! part->is_model_part()) + continue; + std::vector parents { region_config_from_model_volume(m_default_region_config, nullptr, *part, num_extruders, variant_index) }; + for (const auto &[range, range_config] : model_object->layer_config_ranges) + parents.emplace_back(region_config_from_model_volume(m_default_region_config, &range_config.get(), *part, num_extruders, variant_index)); + for (const PrintRegionConfig &parent : parents) { + append_feature_extruders(parent); + for (const ModelVolume *modifier : modifiers) + append_feature_extruders(region_config_from_model_volume(parent, nullptr, *modifier, num_extruders, variant_index)); + } + } + append_model_object_extruders(*model_object, extruders); + const PrintObjectConfig object_config = PrintObject::object_config_from_model_object(m_default_object_config, *model_object, num_extruders, variant_index); + if (PrintObject::has_support_material(object_config)) + append_support_extruders(object_config, (unsigned int)num_extruders, extruders); + } + this->append_tool_change_extruders(extruders); + // A mixed slot prints with its physical components. + const auto expand_and_sort = [this, &extruders]() { + extruders = expand_mixed_filaments(extruders, m_config.filament_is_mixed.values, m_config.filament_mixed_components.values); + }; + expand_and_sort(); + // The wipe tower filament may also be flushed into the objects. + this->append_wipe_tower_extruder(extruders); + expand_and_sort(); + + m_same_shrinkage_compensations = ! extruders.empty() && + std::all_of(extruders.begin(), extruders.end(), [&](unsigned int extruder) { return same_shrinkage(extruders.front(), extruder); }); + m_shrinkage_compensation = Vec3d::Ones(); + if (m_same_shrinkage_compensations) + set_compensation(extruders.front()); +} + Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_config, bool extruder_applied) { #ifdef _DEBUG @@ -1864,6 +1939,7 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ // 4) Generate PrintObjects from ModelObjects and their instances. { + this->update_shrinkage_compensation(num_extruders, print_variant_index); PrintObjectPtrs print_objects_new; print_objects_new.reserve(std::max(m_objects.size(), m_model.objects.size())); bool new_objects = false; diff --git a/tests/fff_print/test_mixed_filament.cpp b/tests/fff_print/test_mixed_filament.cpp index bfa8985fd4..3322020360 100644 --- a/tests/fff_print/test_mixed_filament.cpp +++ b/tests/fff_print/test_mixed_filament.cpp @@ -11,9 +11,15 @@ #include #include +#include +#include #include "libslic3r/GCode/ToolOrdering.hpp" #include "libslic3r/MultiNozzleUtils.hpp" #include "libslic3r/Print.hpp" +#include "libslic3r/BoundingBox.hpp" +#include "libslic3r/ExPolygon.hpp" +#include "libslic3r/Layer.hpp" +#include "libslic3r/libslic3r.h" #include "test_helpers.hpp" @@ -333,3 +339,72 @@ TEST_CASE("Print::validate warns when a gradient mixed filament is used without CHECK(count_opt(print, "enable_mixed_color_sublayer") == 0); } } + +TEST_CASE("Filament shrinkage compensation compares the physical filaments of a mixed slot", "[MixedFilament][Print][Regression]") +{ + // The mixed slot 3 blends filament 1 (90 %) and filament 2 (100 %); its own 90 % does not count. + DynamicPrintConfig config = mixed_config(false); + config.set_deserialize_strict({ + { "filament_shrink", "90%,100%,90%" }, + { "xy_contour_compensation", 0. }, + { "layer_change_gcode", "G92 E0\n" }, // validate() relative-E reset + }); + + Print print; + init_and_process_print({ cube(20.) }, print, config); + + const PrintObject &object = *print.objects().front(); + const BoundingBox extents = get_extents(object.get_layer(int(object.layer_count() / 2))->lslices); + CHECK_THAT(unscale(extents.size().x()), Catch::Matchers::WithinAbs(20., 0.01)); + std::vector warnings; + CHECK(print.validate(&warnings).string.empty()); + CHECK(std::any_of(warnings.begin(), warnings.end(), [](const StringObjectException &w) { + return w.string.find("Filament shrinkage will not be used") != std::string::npos; + })); +} + +TEST_CASE("Filament shrinkage compensation counts the wipe tower filament next to a mixed slot", "[MixedFilament][Print][Regression]") +{ + // The object prints only with the mixed slot 4 of filaments 1 and 2 (both 90 %). The wipe tower filament 3 (100 %) + // may be flushed into the object, so the compensation is off. + DynamicPrintConfig config = multifilament_config(4); + config.set_deserialize_strict({ + {"filament_is_mixed", "0,0,0,1"}, + {"filament_mixed_components", ";;;1,2"}, + {"filament_mixed_sublayer_ratios", ";;;0.5,0.5"}, + {"filament_mixed_gradient", "0,0,0,0"}, + {"filament_mixed_gradient_range", ";;;"}, + {"filament_mixed_gradient_curve", ";;;"}, + {"filament_mixed_gradient_per_part","0,0,0,0"}, + {"outer_wall_filament_id", "4"}, + {"inner_wall_filament_id", "4"}, + {"sparse_infill_filament_id", "4"}, + {"internal_solid_filament_id", "4"}, + {"top_surface_filament_id", "4"}, + {"bottom_surface_filament_id", "4"}, + {"enable_prime_tower", "1"}, + {"wipe_tower_filament", "3"}, + {"prime_tower_width", "35"}, + {"wipe_tower_x", "50"}, // inside the 200x200 test bed + {"wipe_tower_y", "50"}, + {"filament_shrink", "90%,90%,100%,90%"}, + {"xy_contour_compensation", "0"}, + {"layer_change_gcode", "G92 E0\n"}, // validate() relative-E reset + }); + + // The object itself is on the mixed slot too, so no other filament is assigned. + const std::vector> object_overrides { { { "extruder", 4 } } }; + Print print; + Model model; + init_print({ cube(20.) }, print, model, config, &object_overrides); + print.process(); + + const PrintObject &object = *print.objects().front(); + const BoundingBox extents = get_extents(object.get_layer(int(object.layer_count() / 2))->lslices); + CHECK_THAT(unscale(extents.size().x()), Catch::Matchers::WithinAbs(20., 0.01)); + std::vector warnings; + CHECK(print.validate(&warnings).string.empty()); + CHECK(std::any_of(warnings.begin(), warnings.end(), [](const StringObjectException &w) { + return w.string.find("Filament shrinkage will not be used") != std::string::npos; + })); +} diff --git a/tests/fff_print/test_print.cpp b/tests/fff_print/test_print.cpp index 1b81eea0ea..dd78ef5f5f 100644 --- a/tests/fff_print/test_print.cpp +++ b/tests/fff_print/test_print.cpp @@ -63,6 +63,8 @@ #include "libslic3r/ObjectID.hpp" #include "libslic3r/Point.hpp" #include "libslic3r/SurfaceCollection.hpp" +#include "libslic3r/BoundingBox.hpp" +#include "libslic3r/ExPolygon.hpp" using namespace Slic3r; using namespace Slic3r::Test; @@ -1652,3 +1654,130 @@ TEST_CASE("The build height check counts the raft", "[Print][validate][Regressio else CHECK(error.find("maximum build volume height") != std::string::npos); } + +TEST_CASE("Filament shrinkage compensation scales the sliced object", "[Print][Regression]") +{ + // A 20 mm cube printed with a filament that shrinks to xy % / z % is sliced 20 * 100 / xy wide and, + // up to one layer, 20 * 100 / z tall. + const auto [xy, z] = GENERATE(table({ { 100., 100. }, { 90., 100. }, { 100., 95. } })); + const double layer_height = 0.2; + + Print print; + init_and_process_print({ cube(20.) }, print, { + { "filament_shrink", xy }, + { "filament_shrinkage_compensation_z", z }, + { "layer_height", layer_height }, + { "initial_layer_print_height", layer_height }, + { "precise_z_height", false }, + { "raft_layers", 0 }, + { "xy_contour_compensation", 0. }, + }); + + const PrintObject &object = *print.objects().front(); + const Layer &middle = *object.get_layer(int(object.layer_count() / 2)); + const BoundingBox extents = get_extents(middle.lslices); + CHECK_THAT(unscale(extents.size().x()), Catch::Matchers::WithinAbs(20. * 100. / xy, 0.01)); + CHECK_THAT(unscale(extents.size().y()), Catch::Matchers::WithinAbs(20. * 100. / xy, 0.01)); + CHECK_THAT(object.layers().back()->print_z, Catch::Matchers::WithinAbs(20. * 100. / z, layer_height)); +} + +TEST_CASE("Filament shrinkage compensation follows the filaments the object prints with", "[Print][Regression]") +{ + // Filament 1 shrinks to 90 %, filament 2 not at all. An unused filament does not matter; two used filaments + // that disagree switch the compensation off and validate() warns about it. + const auto [wall_filament, expected_width] = GENERATE(table({ { 1, 20. * 100. / 90. }, { 2, 20. } })); + + Print print; + init_and_process_print({ cube(20.) }, print, multifilament_config(2, { + { "filament_shrink", "90%,100%" }, + { "outer_wall_filament_id", wall_filament }, + { "inner_wall_filament_id", wall_filament }, + { "xy_contour_compensation", 0. }, + { "layer_change_gcode", "G92 E0\n" }, // validate() relative-E reset + })); + + const PrintObject &object = *print.objects().front(); + const BoundingBox extents = get_extents(object.get_layer(int(object.layer_count() / 2))->lslices); + CHECK_THAT(unscale(extents.size().x()), Catch::Matchers::WithinAbs(expected_width, 0.01)); + + std::vector warnings; + CHECK(print.validate(&warnings).string.empty()); + const bool mismatch_warning = std::any_of(warnings.begin(), warnings.end(), [](const StringObjectException &w) { + return w.string.find("Filament shrinkage will not be used") != std::string::npos; + }); + CHECK(mismatch_warning == (wall_filament == 2)); +} + +TEST_CASE("Applying the same model again keeps the filament shrinkage compensation", "[Print][Regression]") +{ + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.set_deserialize_strict({ { "filament_shrink", "90%" }, { "xy_contour_compensation", 0. } }); + + Print print; + Model model; + init_print({ cube(20.) }, print, model, config); + const auto width = [&print]() { + print.process(); + const PrintObject &object = *print.objects().front(); + return unscale(get_extents(object.get_layer(int(object.layer_count() / 2))->lslices).size().x()); + }; + CHECK_THAT(width(), Catch::Matchers::WithinAbs(20. * 100. / 90., 0.01)); + print.apply(model, config); + CHECK_THAT(width(), Catch::Matchers::WithinAbs(20. * 100. / 90., 0.01)); +} + +TEST_CASE("Height ranges and modifiers count for the filament shrinkage compensation", "[Print][Regression]") +{ + // The model is complete before the first apply(), so no PrintRegion tells yet which filaments print. + enum class Setup { RangeWall, ModifierInfill, ModifierInheritingObjectFilament }; + const Setup setup = GENERATE(Setup::RangeWall, Setup::ModifierInfill, Setup::ModifierInheritingObjectFilament); + // The object prints with the filament that shrinks to 90 %, the other one does not shrink. + const bool object_on_filament_2 = setup == Setup::ModifierInheritingObjectFilament; + DynamicPrintConfig config = multifilament_config(2, { + { "filament_shrink", object_on_filament_2 ? "100%,90%" : "90%,100%" }, + { "xy_contour_compensation", 0. }, + { "layer_change_gcode", "G92 E0\n" }, // validate() relative-E reset + }); + + Model model; + ModelObject *object = model.add_object(); + object->add_volume(cube(20.)); + DynamicPrintConfig override; + switch (setup) { + case Setup::RangeWall: + // The walls of a height range print with the other filament. + override.set_key_value("outer_wall_filament_id", new ConfigOptionInt(2)); + // Every layer range must carry a layer_height (see layer_height_profile_from_ranges). + override.set_key_value("layer_height", new ConfigOptionFloat(0.2)); + object->layer_config_ranges[{ 5., 10. }].assign_config(std::move(override)); + break; + case Setup::ModifierInfill: + // The infill inside a modifier prints with the other filament. + override.set_key_value("sparse_infill_filament_id", new ConfigOptionInt(2)); + object->add_volume(cube(10.), ModelVolumeType::PARAMETER_MODIFIER, false)->config.apply(override); + break; + case Setup::ModifierInheritingObjectFilament: + // The modifier only changes the infill density and keeps printing with the filament of the object. + object->config.set_key_value("extruder", new ConfigOptionInt(2)); + override.set_key_value("sparse_infill_density", new ConfigOptionPercent(30)); + object->add_volume(cube(10.), ModelVolumeType::PARAMETER_MODIFIER, false)->config.apply(override); + break; + } + object->add_instance()->set_offset(Vec3d(90., 90., 0.)); + object->ensure_on_bed(); + + Print print; + print.apply(model, config); + print.process(); + + const PrintObject &print_object = *print.objects().front(); + const BoundingBox extents = get_extents(print_object.get_layer(int(print_object.layer_count() / 2))->lslices); + const double expected_width = object_on_filament_2 ? 20. * 100. / 90. : 20.; + CHECK_THAT(unscale(extents.size().x()), Catch::Matchers::WithinAbs(expected_width, 0.01)); + std::vector warnings; + CHECK(print.validate(&warnings).string.empty()); + const bool mismatch_warning = std::any_of(warnings.begin(), warnings.end(), [](const StringObjectException &w) { + return w.string.find("Filament shrinkage will not be used") != std::string::npos; + }); + CHECK(mismatch_warning == ! object_on_filament_2); +}