From d35af1806a8d6d918c2b24059504f2580502ab3d Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Fri, 2 Oct 2026 12:46:44 -0500 Subject: [PATCH] Belt cooling: keep the fan off in the band inside the layer cooling pass The band was a second pass over the finished layer that fought the fan commands the layer pass had already written (overhang, bridge and resume requests). The generator now marks where each segment enters and leaves the band and the layer pass treats the band as the strongest fan request, so there is one place that decides the fan. --- src/libslic3r/GCode.cpp | 25 ++--- src/libslic3r/GCode.hpp | 5 +- src/libslic3r/GCode/CoolingBuffer.cpp | 151 +++++--------------------- src/libslic3r/GCode/CoolingBuffer.hpp | 19 +--- src/libslic3r/GCode/FanMover.hpp | 5 - tests/fff_print/test_print.cpp | 75 +++++++++---- 6 files changed, 95 insertions(+), 185 deletions(-) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 4227bf7817..f98cf62fc4 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -8507,25 +8507,20 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, // the speed fade tracks perpendicular distance from the plane on // belt printers; otherwise this falls back to the slicing layer id. const int _layer = this->effective_layer_index_for_point(path_point_mm); - // Belt printers: tell the cooling buffer which band above the belt the next extrusion is - // in (CoolingBuffer::apply_belt_band_fan consumes the tag). A tilted layer runs from the - // belt to the top of the part, so this is decided per segment, not per path; a tag is - // written once per layer and at every change, capped where the fan stops depending on it. - const bool belt_band_tags = m_enable_cooling_markers && m_config.belt_printer.value; - const int belt_band_cap = belt_band_tags - ? std::max(m_config.close_fan_the_first_x_layers.get_at(m_writer->filament()->id()), - m_config.full_fan_speed_layer.get_at(m_writer->filament()->id())) + 1 : 0; - auto tag_belt_band = [this, &gcode, belt_band_tags, belt_band_cap, z = path_point_mm.z()](coord_t x, coord_t y) { + // Belt printers: a tilted layer runs from the belt to the top of the part, so the + // "first layers" the fan stays off for are a band along the belt. Mark where the + // extrusion enters and leaves it, per segment, for the cooling buffer. + const bool belt_band_tags = m_enable_cooling_markers && m_config.belt_printer.value; + const int belt_band_layers = belt_band_tags ? m_config.close_fan_the_first_x_layers.get_at(m_writer->filament()->id()) : 0; + auto tag_belt_band = [this, &gcode, belt_band_tags, belt_band_layers, z = path_point_mm.z()](coord_t x, coord_t y) { if (! belt_band_tags) return; - const int band = std::min(this->effective_layer_index_for_point(Vec3d(unscale(x), unscale(y), z)), belt_band_cap); - if (m_belt_band_tag_layer != m_layer_index || m_belt_band_tag != band) { - gcode += ";_BELT_BAND:" + std::to_string(band) + "\n"; - m_belt_band_tag = band; - m_belt_band_tag_layer = m_layer_index; + const bool in_band = this->effective_layer_index_for_point(Vec3d(unscale(x), unscale(y), z)) < belt_band_layers; + if (in_band != m_belt_in_band) { + gcode += in_band ? ";_BELT_BAND_START\n" : ";_BELT_BAND_END\n"; + m_belt_in_band = in_band; } }; - tag_belt_band(path.first_point().x(), path.first_point().y()); if (path_on_first_layer || object_layer_over_raft()) { //BBS: for solid infill of first layer, speed can be higher as long as //wall lines have be attached diff --git a/src/libslic3r/GCode.hpp b/src/libslic3r/GCode.hpp index e390bc972f..17221101fd 100644 --- a/src/libslic3r/GCode.hpp +++ b/src/libslic3r/GCode.hpp @@ -856,9 +856,8 @@ protected: ~BeltFloorObjectGuard() { slot = nullptr; } }; - // Last ";_BELT_BAND" tag written and the layer it was written on (see _extrude()). - int m_belt_band_tag{std::numeric_limits::min()}; - int m_belt_band_tag_layer{std::numeric_limits::min()}; + // The last extrusion segment was inside the belt's first-layer fan band (see _extrude()). + bool m_belt_in_band{false}; std::set m_initial_layer_extruders; std::vector> m_sorted_layer_filaments; diff --git a/src/libslic3r/GCode/CoolingBuffer.cpp b/src/libslic3r/GCode/CoolingBuffer.cpp index 29ab528de5..a47dd2c148 100644 --- a/src/libslic3r/GCode/CoolingBuffer.cpp +++ b/src/libslic3r/GCode/CoolingBuffer.cpp @@ -1,13 +1,10 @@ #include "../GCode.hpp" #include "CoolingBuffer.hpp" -#include "FanMover.hpp" #include #include #include #include #include -#include -#include #include #include #include @@ -39,8 +36,7 @@ CoolingBuffer::CoolingBuffer(GCode &gcodegen) : m_config(gcodegen.config()), m_t void CoolingBuffer::reset(const Vec3d &position) { - m_belt_band_fan = -1; - m_belt_band_layer_fan = -1; + m_belt_band_active = false; // BBS: add I and J axis to store center of arc m_current_pos.assign(7, 0.f); m_current_pos[0] = float(position.x()); @@ -80,6 +76,9 @@ struct CoolingLine // ORCA: Add support for ironing fan speed control TYPE_IRONING_FAN_START = 1 << 19, TYPE_IRONING_FAN_END = 1 << 20, + // Belt printers: extrusions within the first-layer band above the belt. + TYPE_BELT_BAND_START = 1 << 21, + TYPE_BELT_BAND_END = 1 << 22, }; CoolingLine(unsigned int type, size_t line_start, size_t line_end) : @@ -334,13 +333,9 @@ std::string CoolingBuffer::process_layer(std::string &&gcode, size_t layer_id, b if (flush) { // This is either an object layer or the very last print layer. Calculate cool down over the collected support layers // and one object layer. - const unsigned int extruder_at_start = m_current_extruder; std::vector per_extruder_adjustments = this->parse_layer_gcode(m_gcode, m_current_pos); float layer_time_stretched = this->calculate_layer_slowdown(per_extruder_adjustments); out = this->apply_layer_cooldown(m_gcode, layer_id, layer_time_stretched, per_extruder_adjustments); - // Belt printers: the fan follows each path's height above the belt (see apply_belt_band_fan). - if (out.find(";_BELT_BAND:") != std::string::npos) - out = this->apply_belt_band_fan(std::move(out), layer_time_stretched, extruder_at_start); m_gcode.clear(); } return out; @@ -544,6 +539,10 @@ std::vector CoolingBuffer::parse_layer_gcode(const std:: line.type = CoolingLine::TYPE_IRONING_FAN_START; } else if (boost::starts_with(sline, ";_IRONING_FAN_END")) { // ORCA: Add support for ironing fan speed control line.type = CoolingLine::TYPE_IRONING_FAN_END; + } else if (boost::starts_with(sline, ";_BELT_BAND_START")) { + line.type = CoolingLine::TYPE_BELT_BAND_START; + } else if (boost::starts_with(sline, ";_BELT_BAND_END")) { + line.type = CoolingLine::TYPE_BELT_BAND_END; } else if (boost::starts_with(sline, "G4 ")) { // Parse the wait time. line.type = CoolingLine::TYPE_G4; @@ -885,7 +884,9 @@ std::string CoolingBuffer::apply_layer_cooldown( {CoolingLine::TYPE_SUPPORT_INTERFACE_FAN_START, false}, {CoolingLine::TYPE_IRONING_FAN_START, false}, // ORCA: Add support for ironing fan speed control {CoolingLine::TYPE_FORCE_RESUME_FAN, false}}; - bool need_set_fan = false; + // Belt printers: a band still open from the previous layer has to take the fan back from + // the layer-level speed issued just above. + bool need_set_fan = m_belt_band_active; for (const CoolingLine *line : lines) { const char *line_start = gcode.c_str() + line->line_start; @@ -899,6 +900,8 @@ std::string CoolingBuffer::apply_layer_cooldown( if (new_extruder != m_current_extruder) { m_current_extruder = new_extruder; change_extruder_set_fan(true); + if (m_belt_band_active) + need_set_fan = true; } } new_gcode.append(line_start, line_end - line_start); @@ -951,6 +954,13 @@ std::string CoolingBuffer::apply_layer_cooldown( if (m_additional_fan_speed != -1 && m_config.auxiliary_fan.value) new_gcode += GCodeWriter::set_additional_fan(m_additional_fan_speed); } + else if (line->type & CoolingLine::TYPE_BELT_BAND_START) { + m_belt_band_active = true; + need_set_fan = true; + } else if (line->type & CoolingLine::TYPE_BELT_BAND_END) { + m_belt_band_active = false; + need_set_fan = true; + } else if (line->type & CoolingLine::TYPE_EXTRUDE_END) { // Just remove this comment. } else if (line->type & (CoolingLine::TYPE_ADJUSTABLE | CoolingLine::TYPE_EXTERNAL_PERIMETER | CoolingLine::TYPE_WIPE | CoolingLine::TYPE_HAS_F)) { @@ -1043,7 +1053,15 @@ std::string CoolingBuffer::apply_layer_cooldown( m_current_fan_speed = speed; } }; - if (fan_speed_change_requests[CoolingLine::TYPE_OVERHANG_FAN_START]){ + if (m_belt_band_active) { + // Belt printers: a tilted layer runs from the belt to the top of the part, so + // "the first layers" are a band along the belt rather than the first slicing + // layers. Extrusions GCode::_extrude() marks as inside that band print with the + // fan off, whatever overhang, bridge or resume request is pending, as the first + // layers of a flat bed do. Leaving the band falls through to the branches below. + set_fan(0); + fan_speed_change_requests[CoolingLine::TYPE_FORCE_RESUME_FAN] = false; + } else if (fan_speed_change_requests[CoolingLine::TYPE_OVERHANG_FAN_START]){ set_fan(overhang_fan_speed); } else if (fan_speed_change_requests[CoolingLine::TYPE_INTERNAL_BRIDGE_FAN_START]){ // ORCA: Add support for separate internal bridge fan speed control set_fan(internal_bridge_fan_speed); @@ -1072,115 +1090,4 @@ std::string CoolingBuffer::apply_layer_cooldown( return new_gcode; } -// Pure helper: compute the main fan speed for a given effective layer index. -// Mirrors the inline logic in change_extruder_set_fan but is callable from -// per-tag code in apply_belt_band_fan. -int CoolingBuffer::compute_main_fan_speed(int effective_layer_id, float layer_time, - unsigned int extruder_id) const -{ -#define EXTRUDER_CFG(opt) m_config.opt.get_at(extruder_id) - float fan_min_speed = EXTRUDER_CFG(fan_min_speed); - float fan_max_speed = EXTRUDER_CFG(fan_max_speed); - bool reduce_fan_stop_start_freq = EXTRUDER_CFG(reduce_fan_stop_start_freq); - int close_fan_the_first_x_layers = EXTRUDER_CFG(close_fan_the_first_x_layers); - int full_fan_speed_layer = EXTRUDER_CFG(full_fan_speed_layer); - float slow_down_layer_time = float(EXTRUDER_CFG(slow_down_layer_time)); - float fan_cooling_layer_time = float(EXTRUDER_CFG(fan_cooling_layer_time)); -#undef EXTRUDER_CFG - - if (close_fan_the_first_x_layers <= 0 && full_fan_speed_layer > 0) - close_fan_the_first_x_layers = 1; - - float fan_speed_new = reduce_fan_stop_start_freq ? fan_min_speed : 0.f; - if (effective_layer_id >= close_fan_the_first_x_layers) { - if (layer_time < slow_down_layer_time) { - fan_speed_new = fan_max_speed; - } else if (layer_time < fan_cooling_layer_time) { - double t = (layer_time - slow_down_layer_time) / - (fan_cooling_layer_time - slow_down_layer_time); - fan_speed_new = float(int(floor(t * fan_min_speed + - (1. - t) * fan_max_speed) + 0.5)); - } - if (effective_layer_id + 1 < full_fan_speed_layer) { - float factor = float(effective_layer_id + 1 - close_fan_the_first_x_layers) - / float(full_fan_speed_layer - close_fan_the_first_x_layers); - fan_speed_new = float(std::clamp(int(fan_speed_new * factor + 0.5f), 0, 255)); - } - } else { - fan_speed_new = 0.f; - } - return int(fan_speed_new); -} - -// Belt printers: a layer is a tilted slab, so "the first layer" is not a slicing layer but -// whatever lies within a layer height of the belt. GCode::_extrude() knows each path's -// height above the belt in the slicing frame and tags every change of it with -// ";_BELT_BAND:". This pass turns the tags into part-fan changes -// and strips them. (The G-code itself is in machine coordinates, which is why the band -// is not worked out from the moves here.) -// -// Only the main part-cooling fan is touched; overhang and bridge fans keep the values -// apply_layer_cooldown() gave them. -std::string CoolingBuffer::apply_belt_band_fan(std::string &&gcode_in, float layer_time, unsigned int extruder_at_start) -{ - static constexpr std::string_view band_tag = ";_BELT_BAND:"; - const std::string &gcode = gcode_in; - std::string out; - out.reserve(gcode.size()); - - // Match the PWM floor applied at every other set_fan call in this file. - const unsigned int part_cooling_fan_min_pwm = static_cast(std::max(0, m_config.part_cooling_fan_min_pwm.value)); - unsigned int active_extruder = extruder_at_start; - - const char *p = gcode.data(); - const char *end = p + gcode.size(); - while (p < end) { - const char *line_end = static_cast(std::memchr(p, '\n', end - p)); - if (line_end == nullptr) - line_end = end; - const char *next_line = line_end < end ? line_end + 1 : end; - const std::string_view line(p, line_end - p); - - if (line.size() > band_tag.size() && line.compare(0, band_tag.size(), band_tag) == 0) { - const int eff_idx = std::atoi(std::string(line.substr(band_tag.size())).c_str()); - const bool in_band = eff_idx < std::max(m_config.close_fan_the_first_x_layers.get_at(active_extruder), 1); - int target = this->compute_main_fan_speed(eff_idx, layer_time, active_extruder); - // Clear of the belt and nothing to add: fall back to what the layer asked for. - if (! in_band && target == 0 && m_belt_band_layer_fan >= 0) - target = m_belt_band_layer_fan; - if (target != m_belt_band_fan) { - out += GCodeWriter::set_fan(m_config.gcode_flavor, target, part_cooling_fan_min_pwm); - m_belt_band_fan = target; - } - // The tag itself is dropped. - p = next_line; - continue; - } - - if (! line.empty() && line.front() == 'M') { - // Follow the fan commands of the layer-level cooling. FanMover's parser ignores - // auxiliary and chamber fans (M106 P2, P3...), which must not be taken for the part fan. - const int16_t raw = get_fan_speed(std::string(line), m_config.gcode_flavor); - if (raw >= 0) { - m_belt_band_fan = int(std::lround(double(std::min(raw, 255)) * 100. / 255.)); - m_belt_band_layer_fan = m_belt_band_fan; - } - } else if (line.size() > m_toolchange_prefix.size() && line.compare(0, m_toolchange_prefix.size(), m_toolchange_prefix) == 0) { - char *num_end = nullptr; - const std::string num(line.substr(m_toolchange_prefix.size())); - const long tool = std::strtol(num.c_str(), &num_end, 10); - if (num_end != num.c_str() && tool >= 0) - active_extruder = unsigned(tool); - } - - out.append(p, next_line - p); - p = next_line; - } - - // The fan's real state, so the next layer's cooling re-issues its own speed if it differs. - if (m_belt_band_fan >= 0) - m_current_fan_speed = m_belt_band_fan; - return out; -} - } // namespace Slic3r diff --git a/src/libslic3r/GCode/CoolingBuffer.hpp b/src/libslic3r/GCode/CoolingBuffer.hpp index 0d758b5a5b..e0b5044921 100644 --- a/src/libslic3r/GCode/CoolingBuffer.hpp +++ b/src/libslic3r/GCode/CoolingBuffer.hpp @@ -36,18 +36,6 @@ private: // Returns the adjusted G-code. std::string apply_layer_cooldown(const std::string &gcode, size_t layer_id, float layer_time, std::vector &per_extruder_adjustments); - // Belt printers: turn the ";_BELT_BAND:" tags GCode::_extrude() leaves in the - // layer's G-code into part-fan changes, so the fan follows a path's height above the - // belt rather than the slicing layer index, and strip the tags. - std::string apply_belt_band_fan(std::string &&gcode_in, float layer_time, unsigned int extruder_at_start); - - // Pure helper: compute the main fan speed for a given effective layer - // index (layer-id units, mapped through the plane evaluator) and the - // current extruder. Mirrors the inline logic in the change_extruder_set_fan - // lambda but is callable from per-line code. - int compute_main_fan_speed(int effective_layer_id, float layer_time, - unsigned int extruder_id) const; - // G-code snippet cached for the support layers preceding an object layer. std::string m_gcode; // Internal data. @@ -70,10 +58,9 @@ private: unsigned int m_current_nozzle; //BBS: current fan speed int m_current_fan_speed; - // Belt band pass state, kept across layers. The part fan as this pass last saw or set - // it (percent, -1 unknown), and the last value the layer-level cooling asked for. - int m_belt_band_fan = -1; - int m_belt_band_layer_fan = -1; + // Belt printers: the extrusion being processed lies in the first-layer band above the + // belt (between a ";_BELT_BAND_START" and a ";_BELT_BAND_END"). Kept across layers. + bool m_belt_band_active = false; }; } diff --git a/src/libslic3r/GCode/FanMover.hpp b/src/libslic3r/GCode/FanMover.hpp index 9567e8e2d5..17addd855c 100644 --- a/src/libslic3r/GCode/FanMover.hpp +++ b/src/libslic3r/GCode/FanMover.hpp @@ -27,11 +27,6 @@ public: } }; -// The part-cooling fan value a G-code line sets: the raw S (0..255) of an M106 that -// addresses the part fan, 0 for a fan-off command, -1 for any other line (auxiliary -// and chamber fans included). -int16_t get_fan_speed(const std::string &line, GCodeFlavor flavor); - class FanMover { private: diff --git a/tests/fff_print/test_print.cpp b/tests/fff_print/test_print.cpp index 8cdd9e3c33..e9a98324d9 100644 --- a/tests/fff_print/test_print.cpp +++ b/tests/fff_print/test_print.cpp @@ -20,6 +20,9 @@ #include "test_utils.hpp" #include +#include +#include +#include #include #include #include @@ -628,13 +631,11 @@ TEST_CASE("Belt printers refuse an object taller than the gantry clearance", "[P } } -// On a belt every tilted layer starts on the belt, so "first layer" cooling is a band along -// the belt, not the first slicing layers: the part fan goes off for the paths that start -// within a layer height of the belt and back on above it, on every layer. The G-code is in -// machine coordinates, so the generator tags the band changes and the cooling buffer -// applies them; before that the buffer compared machine-frame moves with a slicing-frame -// plane and never switched the fan at all. -TEST_CASE("Belt printers switch the part fan by height above the belt", "[Print][belt][Cooling]") +// On a belt every tilted layer starts on the belt, so "the first layers" the fan stays off +// for are a band along the belt, not the first slicing layers. The generator marks where +// each extrusion segment enters and leaves that band and the cooling buffer keeps the fan +// off inside it, on every layer. +TEST_CASE("Belt printers keep the part fan off within the band above the belt", "[Print][belt][Cooling]") { DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); config.set_deserialize_strict({ @@ -649,8 +650,8 @@ TEST_CASE("Belt printers switch the part fan by height above the belt", "[Print] { "initial_layer_print_height", 0.2 }, { "skirt_loops", 0 }, { "z_hop", 0 }, - // Three layers: the lowest wall of each tilted layer is centred about 0.3 mm above - // the belt (half a line width in from the contact edge), outside a one-layer band. + // Three layers, 0.6 mm: the lowest wall of each tilted layer is centred about 0.3 mm + // above the belt (half a line width in from the contact edge). { "close_fan_the_first_x_layers", 3 }, { "full_fan_speed_layer", 0 }, { "fan_min_speed", 100 }, @@ -664,24 +665,50 @@ TEST_CASE("Belt printers switch the part fan by height above the belt", "[Print] const std::string gcode = slice({ cube(20) }, config); REQUIRE(! gcode.empty()); - // The tags are consumed by the cooling buffer and never reach the file. + // The markers are consumed by the cooling buffer and never reach the file. CHECK(gcode.find(";_BELT_BAND") == std::string::npos); - // The fan commands in order: '0' off, '1' on. - std::string fan_sequence; - GCodeReader parser; - parser.parse_buffer(gcode, [&](GCodeReader &, const GCodeReader::GCodeLine &line) { - if (line.cmd_is("M107")) - fan_sequence += '0'; - else if (line.cmd_is("M106")) { - float s = 0.f; - fan_sequence += (line.has_value('S', s) && s <= 0.f) ? '0' : '1'; + // With this axis mapping machine Y is the height above the belt along the gantry. Walk + // the moves with the fan state: extrusions that stay within 0.45 mm of the belt are well + // inside the band and must print with the fan off; extrusions that stay 5 mm clear of it + // must print with it on. The first three slicing layers have the fan off altogether. + size_t in_band = 0, in_band_fan_on = 0, clear = 0, clear_fan_off = 0; + int layer = -1; + bool fan_on = false; + double y = 0.; + std::istringstream lines(gcode); + for (std::string line; std::getline(lines, line); ) { + if (boost::starts_with(line, ";LAYER_CHANGE")) { + ++ layer; + } else if (boost::starts_with(line, "M107")) { + fan_on = false; + } else if (boost::starts_with(line, "M106")) { + const size_t s = line.find('S'); + fan_on = s != std::string::npos && std::atof(line.c_str() + s + 1) > 0.; + } else if (boost::starts_with(line, "G1 ")) { + const size_t comment = line.find(';'); + const std::string cmd = line.substr(0, comment); + const size_t ypos = cmd.find(" Y"), epos = cmd.find(" E"); + if (ypos == std::string::npos) + continue; + const double y_new = std::atof(cmd.c_str() + ypos + 2); + const bool extruding = epos != std::string::npos && std::atof(cmd.c_str() + epos + 2) > 0.; + if (extruding && layer >= 3) { + if (std::max(y, y_new) < 0.45) { + ++ in_band; + in_band_fan_on += fan_on; + } else if (std::min(y, y_new) > 5.) { + ++ clear; + clear_fan_off += ! fan_on; + } + } + y = y_new; } - }); - // A flat-bed print turns the fan on once and leaves it on. Here it goes off again for - // paths that start back down at the belt, and on again above it. - INFO("fan sequence: " << fan_sequence); - CHECK(fan_sequence.find("101") != std::string::npos); + } + CHECK(in_band > 20); + CHECK(in_band_fan_on == 0); + CHECK(clear > 20); + CHECK(clear_fan_off == 0); } // Organic supports under an overhang on a belt printer reach below the object's first layer,