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