From f1658855c8e453f0ecca9ae48356254619653250 Mon Sep 17 00:00:00 2001 From: fa1k3 <268878418+fa1k3@users.noreply.github.com> Date: Wed, 14 Oct 2026 16:39:08 +0200 Subject: [PATCH] Apply filament shrinkage compensation on the first slice (#16312) Print::apply() decided the shrinkage compensation from the existing PrintObjects and their regions. On the first apply() they do not exist yet, so the first slice was not scaled. The CLI calls apply() only once, so filament_shrink and filament_shrinkage_compensation_z had no effect there. The compensation is now decided before the PrintObjects are built, from the filaments the model uses, and stored in Print. When the filaments have different shrinkage values, every filament assigned to a feature, height range, modifier, support, tool change or the wipe tower counts, even if it does not end up printing. If they disagree, compensation stays off with the existing warning. --- src/libslic3r/Print.cpp | 158 +++++++++++------------- src/libslic3r/Print.hpp | 22 +++- src/libslic3r/PrintApply.cpp | 76 ++++++++++++ tests/fff_print/test_mixed_filament.cpp | 75 +++++++++++ tests/fff_print/test_print.cpp | 129 +++++++++++++++++++ 5 files changed, 368 insertions(+), 92 deletions(-) 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); +}