From 109d09e0b8a25490ee0394f55769b1d735c36bba Mon Sep 17 00:00:00 2001 From: Error404JoyNotFound Date: Fri, 16 Oct 2026 03:05:53 +0530 Subject: [PATCH] Fix multi-tool standby heaters by turning off unused extruders (#15849) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With ooze prevention on, an extruder that has no layers left is set to 0 °C when it is parked, instead of its standby temperature. On IMEX printers where several filaments share one heater, the heater is only turned off once every filament on it is done. --- src/libslic3r/GCode.cpp | 45 +++++--- src/libslic3r/GCode.hpp | 1 + src/libslic3r/GCode/ToolOrdering.cpp | 15 ++- src/libslic3r/GCode/ToolOrdering.hpp | 3 + .../wipe_tower_temperature_trace_main.txt | 2 +- tests/fff_print/test_multifilament.cpp | 109 ++++++++++++++++++ 6 files changed, 155 insertions(+), 20 deletions(-) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index ce2be2db58..508ab5b421 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -432,27 +432,36 @@ static std::vector get_path_of_change_filament(const Print& print) std::string OozePrevention::pre_toolchange(GCode& gcodegen) { - std::string gcode; - unsigned int extruder_id = gcodegen.writer().filament()->id(); - const auto& filament_idle_temp = gcodegen.config().idle_temperature; - if (filament_idle_temp.get_at(extruder_id) == 0) { - // There is no idle temperature defined in filament settings. - // Use the delta value from print config. - if (gcodegen.config().standby_temperature_delta.value != 0) { - // we assume that heating is always slower than cooling, so no need to block - gcode += gcodegen.writer().set_temperature - (this->_get_temp(gcodegen) + gcodegen.config().standby_temperature_delta.value, false, extruder_id); - gcode.pop_back(); - gcode += " ;cooldown\n"; // this is a marker for GCodeProcessor, so it can supress the commands when needed - } - } else { - // Use the value from filament settings. That one is absolute, not delta. - gcode += gcodegen.writer().set_temperature(filament_idle_temp.get_at(extruder_id), false, extruder_id); - gcode.pop_back(); - gcode += " ;cooldown\n"; // this is a marker for GCodeProcessor, so it can supress the commands when needed + + // Check if this tool will ever be used again in this print. + bool is_last_use = false; + if (gcodegen.m_print != nullptr && !gcodegen.m_print->tool_ordering().empty()) { + coordf_t current_z = gcodegen.layer() ? gcodegen.layer()->print_z : gcodegen.writer().get_position().z(); + const auto& tool_ordering = gcodegen.m_print->tool_ordering(); + const auto& cfg = gcodegen.config(); + const int heater = imex_physical_heater_for(cfg.is_imex.value, cfg.physical_extruder_map, int(extruder_id)); + is_last_use = tool_ordering.is_last_extrusion_layer(current_z, extruder_id); + for (unsigned int f : tool_ordering.all_extruders()) + if (is_last_use && imex_physical_heater_for(cfg.is_imex.value, cfg.physical_extruder_map, int(f)) == heater) + is_last_use = tool_ordering.is_last_extrusion_layer(current_z, f); } + int temp = 0; + if (!is_last_use) { + const auto& filament_idle_temp = gcodegen.config().idle_temperature; + if (filament_idle_temp.get_at(extruder_id) == 0) { + if (gcodegen.config().standby_temperature_delta.value == 0) + return std::string(); + temp = this->_get_temp(gcodegen) + gcodegen.config().standby_temperature_delta.value; + } else { + temp = filament_idle_temp.get_at(extruder_id); + } + } + + std::string gcode = gcodegen.writer().set_temperature(temp, false, extruder_id); + gcode.pop_back(); + gcode += " ;cooldown\n"; // this is a marker for GCodeProcessor, so it can supress the commands when needed return gcode; } diff --git a/src/libslic3r/GCode.hpp b/src/libslic3r/GCode.hpp index 08894574ed..b5898b2305 100644 --- a/src/libslic3r/GCode.hpp +++ b/src/libslic3r/GCode.hpp @@ -1004,6 +1004,7 @@ protected: size_t num_objects, size_t num_islands); + friend class OozePrevention; friend class Wipe; friend class WipeTowerIntegration; friend class PressureEqualizer; diff --git a/src/libslic3r/GCode/ToolOrdering.cpp b/src/libslic3r/GCode/ToolOrdering.cpp index 19f5c686f6..2ea34aef90 100644 --- a/src/libslic3r/GCode/ToolOrdering.cpp +++ b/src/libslic3r/GCode/ToolOrdering.cpp @@ -1276,6 +1276,13 @@ void ToolOrdering::collect_extruder_statistics(bool prime_multi_material) sort_remove_duplicates(m_all_printing_extruders); } + m_last_layer_per_extruder.clear(); + for (const LayerTools& lt : m_layer_tools) { + for (unsigned int ext : lt.extruders) { + m_last_layer_per_extruder[ext] = lt.print_z; + } + } + if (prime_multi_material && ! m_all_printing_extruders.empty()) { // Reorder m_all_printing_extruders in the sequence they will be primed, the last one will be m_first_printing_extruder. // Then set m_first_printing_extruder to the 1st extruder primed. @@ -3664,5 +3671,11 @@ int WipingExtrusions::get_support_interface_extruder_overrides(const PrintObject return -1; } - +bool ToolOrdering::is_last_extrusion_layer(coordf_t print_z, unsigned int extruder_id) const +{ + auto it = m_last_layer_per_extruder.find(extruder_id); + if (it == m_last_layer_per_extruder.end()) + return true; + return print_z >= it->second - EPSILON; +} } // namespace Slic3r diff --git a/src/libslic3r/GCode/ToolOrdering.hpp b/src/libslic3r/GCode/ToolOrdering.hpp index 5c11f482cd..b7aca7dd30 100644 --- a/src/libslic3r/GCode/ToolOrdering.hpp +++ b/src/libslic3r/GCode/ToolOrdering.hpp @@ -370,6 +370,7 @@ public: std::vector & initial_filaments); bool has_non_support_filament(const PrintConfig &config); + bool is_last_extrusion_layer(coordf_t print_z, unsigned int extruder_id) const; private: void initialize_layers(std::vector &zs); @@ -413,6 +414,8 @@ private: // which keeps every legacy per-object code path bit-identical (loops over an empty map are // no-ops; downstream emission falls through to the per-object branch). std::map>> m_gradient_volume_layers; + // Map: extruder_id -> print_z (z coordinate) of the last layer it is used on + std::map m_last_layer_per_extruder; const PrintObject* m_print_object_ptr = nullptr; Print* m_print; bool m_sorted = false; diff --git a/tests/data/wipe_tower_temperature_trace_main.txt b/tests/data/wipe_tower_temperature_trace_main.txt index f755008c26..2ff2b4f0a4 100644 --- a/tests/data/wipe_tower_temperature_trace_main.txt +++ b/tests/data/wipe_tower_temperature_trace_main.txt @@ -163,7 +163,7 @@ M109 S240 T0 ; set nozzle temperature and wait for it to be reached ; CP TOOLCHANGE END M104 S240 T1 ; preheat T1 time: 30s lead 30.0s ; CP TOOLCHANGE START -M104 S200 T0 ; set nozzle temperature ;cooldown +M104 S0 T0 ; set nozzle temperature ;cooldown T1 ; change extruder M109 S240 T1 ; set nozzle temperature and wait for it to be reached ; CP TOOLCHANGE END diff --git a/tests/fff_print/test_multifilament.cpp b/tests/fff_print/test_multifilament.cpp index 54a98d2268..b7fccf2890 100644 --- a/tests/fff_print/test_multifilament.cpp +++ b/tests/fff_print/test_multifilament.cpp @@ -1742,3 +1742,112 @@ TEST_CASE("Each filament cools with the fan speeds of its extruder variant", "[M CHECK(gcode.find("; start range high " + std::to_string(range_high) + "\n") != std::string::npos); } } + +TEST_CASE("Shared heater on IMEX printer stays hot until all filaments on it are done", "[MultiFilament]") +{ + DynamicPrintConfig cfg = multifilament_config(7); + cfg.set_deserialize_strict({ + {"ooze_prevention", 1}, + {"preheat_time", 0}, + {"single_extruder_multi_material", 0}, + }); + imex_7x4_printer(cfg); + + const std::string gcode = slice_with_object_overrides({make_cube(20., 20., 10.), make_cube(20., 20., 4.), make_cube(20., 20., 10.)}, + cfg, + {{{"extruder", 1}, {"layer_height", 0.30}}, + {{"extruder", 2}, {"layer_height", 0.15}}, + {{"extruder", 5}}}); + + bool premature_s0_t0 = false; + double current_z = 0.0; + + GCodeReader reader; + reader.parse_buffer(gcode, [&](GCodeReader& /*self*/, const GCodeReader::GCodeLine& line) { + if (line.has_z()) + current_z = line.z(); + + const std::string raw = line.raw(); + if (raw.find("M104 S0 T0") != std::string::npos && raw.find(";cooldown") != std::string::npos) { + if (current_z < 9.0) + premature_s0_t0 = true; + } + }); + + // Filaments 1 and 2 share head 0, and filament 1 prints up to 10mm. + // T0 must not shut down at ~4mm when filament 2 finishes. + CHECK_FALSE(premature_s0_t0); +} + +TEST_CASE("Unused extruder turns off after its final layer on multi-object print", "[MultiFilament]") +{ + const std::string gcode = slice_with_object_overrides( + { make_cube(20., 20., 10.), make_cube(20., 20., 4.) }, + multifilament_config(2, { + { "single_extruder_multi_material", 0 }, + { "ooze_prevention", 1 }, + { "standby_temperature_delta", -40 }, + { "nozzle_temperature", "240,240" }, + }), + { + { { "extruder", 1 }, { "layer_height", 0.20 } }, + { { "extruder", 2 }, { "layer_height", 0.15 } } + }); + + int t1_s0_cooldown_count = 0; + int t0_s0_cooldown_count = 0; + + std::istringstream stream(gcode); + for (std::string line; std::getline(stream, line);) { + if (line.find(";cooldown") != std::string::npos) { + if (line.find("M104 S0 T1") != std::string::npos) + ++t1_s0_cooldown_count; + if (line.find("M104 S0 T0") != std::string::npos) + ++t0_s0_cooldown_count; + } + } + + // T1 (extruder 2, 4mm cube) gets exactly one S0 after its last layer. + // T0 (extruder 1, 10mm cube) never gets S0 mid-print. + CHECK(t1_s0_cooldown_count == 1); + CHECK(t0_s0_cooldown_count == 0); +} + +TEST_CASE("Unused extruder turns off on non-IMEX three-tool print", "[MultiFilament]") +{ + const std::string gcode = slice_with_object_overrides( + { make_cube(20., 20., 4.), make_cube(20., 20., 10.), make_cube(20., 20., 10.) }, + multifilament_config(3, { + { "single_extruder_multi_material", 0 }, + { "ooze_prevention", 1 }, + { "standby_temperature_delta", -40 }, + { "nozzle_temperature", "240,240,240" }, + }), + { + { { "extruder", 1 } }, + { { "extruder", 2 } }, + { { "extruder", 3 } } + }); + + bool premature_s0_t1 = false; + bool premature_s0_t2 = false; + double current_z = 0.0; + + GCodeReader reader; + reader.parse_buffer(gcode, [&](GCodeReader& /*self*/, const GCodeReader::GCodeLine& line) { + if (line.has_z()) + current_z = line.z(); + + const std::string raw = line.raw(); + if (raw.find(";cooldown") != std::string::npos && current_z < 9.0) { + if (raw.find("M104 S0 T1") != std::string::npos) + premature_s0_t1 = true; + if (raw.find("M104 S0 T2") != std::string::npos) + premature_s0_t2 = true; + } + }); + + // T1 and T2 print up to 10mm — neither should get S0 before that. + CHECK_FALSE(premature_s0_t1); + CHECK_FALSE(premature_s0_t2); +} \ No newline at end of file