diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index df3b1205fb..151efbf6b2 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -8507,6 +8507,14 @@ 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 this path starts in, + // once per layer and at every change (CoolingBuffer::apply_belt_band_fan consumes it). + if (m_enable_cooling_markers && m_config.belt_printer.value && + (m_belt_band_tag_layer != m_layer_index || m_belt_band_tag != _layer)) { + gcode += ";_BELT_BAND:" + std::to_string(_layer) + "\n"; + m_belt_band_tag = _layer; + m_belt_band_tag_layer = m_layer_index; + } 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 3356219037..e390bc972f 100644 --- a/src/libslic3r/GCode.hpp +++ b/src/libslic3r/GCode.hpp @@ -2,6 +2,7 @@ #define slic3r_GCode_hpp_ #include "libslic3r.h" +#include #include "ExPolygon.hpp" #include "GCodeWriter.hpp" #include "GCode/BeltKinematics.hpp" @@ -855,6 +856,10 @@ 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()}; + std::set m_initial_layer_extruders; std::vector> m_sorted_layer_filaments; // BBS diff --git a/src/libslic3r/GCode/CoolingBuffer.cpp b/src/libslic3r/GCode/CoolingBuffer.cpp index 1df6ce3bb9..29ab528de5 100644 --- a/src/libslic3r/GCode/CoolingBuffer.cpp +++ b/src/libslic3r/GCode/CoolingBuffer.cpp @@ -1,11 +1,13 @@ #include "../GCode.hpp" -#include "../FirstLayerPlane.hpp" #include "CoolingBuffer.hpp" +#include "FanMover.hpp" #include #include #include #include #include +#include +#include #include #include #include @@ -33,15 +35,12 @@ CoolingBuffer::CoolingBuffer(GCode &gcodegen) : m_config(gcodegen.config()), m_t m_extruder_ids.emplace_back(ex.id()); } - // Borrow the first-layer plane from the GCode generator. When inactive - // (non-belt printers and belt printers without Z shear), per-line fan - // re-evaluation is skipped and behavior is bit-identical to the legacy - // per-layer path. - m_first_layer_plane = gcodegen.first_layer_plane(); } void CoolingBuffer::reset(const Vec3d &position) { + m_belt_band_fan = -1; + m_belt_band_layer_fan = -1; // 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()); @@ -335,16 +334,13 @@ 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); - // First-layer plane: per-segment fan re-evaluation post-pass. Walks - // the cooled-down gcode and inserts inline M106 commands at band - // crossings (where the path's perpendicular distance to the plane - // crosses close_fan_the_first_x_layers thresholds). No-op when - // the evaluator is inactive. - if (m_first_layer_plane && m_first_layer_plane->is_active()) - out = this->apply_first_layer_plane_fan_eval(std::move(out), layer_id, layer_time_stretched); + // 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; @@ -1078,7 +1074,7 @@ std::string CoolingBuffer::apply_layer_cooldown( // 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-line code in apply_first_layer_plane_fan_eval. +// 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 { @@ -1116,173 +1112,74 @@ int CoolingBuffer::compute_main_fan_speed(int effective_layer_id, float layer_ti return int(fan_speed_new); } -// Post-pass: walk the cooled-down gcode line by line, track XYZ position, -// and insert M106 commands at first-layer-plane band crossings so the fan -// follows perpendicular distance to the plane rather than the slicing-layer -// index. Only invoked when the FirstLayerPlane evaluator is active. +// 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.) // -// This implementation is intentionally minimal: it overrides only the MAIN -// fan (the one set by GCodeWriter::set_fan); overhang/internal-bridge/etc -// special fans remain at their layer-level values from apply_layer_cooldown. -// That keeps the per-line logic small while still giving the user precise -// fan control near the belt surface, which is the main quality concern. -std::string CoolingBuffer::apply_first_layer_plane_fan_eval( - std::string &&gcode_in, size_t /*layer_id*/, float layer_time) +// 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) { - if (!m_first_layer_plane || !m_first_layer_plane->is_active()) - return std::move(gcode_in); - + static constexpr std::string_view band_tag = ";_BELT_BAND:"; const std::string &gcode = gcode_in; std::string out; - out.reserve(gcode.size() + 256); + out.reserve(gcode.size()); - // Match the PWM floor applied at every other set_fan call in this file so - // band-crossing M106 emissions start the fan reliably at low speeds. + // 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; - // Track position in slicing-frame mm. Seed from m_current_pos which the - // CoolingBuffer keeps up-to-date across layers. - Vec3d cur_pos_mm(m_current_pos[0], m_current_pos[1], m_current_pos[2]); - - // Track current main fan speed by parsing M106 commands as we walk so - // we can restore it after a band exit. - int current_main_fan = m_fan_speed; - int pre_band_main_fan = current_main_fan; - // Implicit initial state: assume the layer started "out of the band" - // (i.e., the layer-level fan setting from apply_layer_cooldown is in - // effect). The first movement we encounter will reconcile this. - bool in_first_layer_band = false; - unsigned int active_extruder = m_current_extruder; - - auto parse_xyz_into = [](const std::string_view &line_sv, Vec3d &p) { - if (line_sv.size() < 3) return false; - if (line_sv[0] != 'G') return false; - if (line_sv[1] != '0' && line_sv[1] != '1') return false; - if (line_sv[2] != ' ' && line_sv[2] != '\t') return false; - const char *c = line_sv.data() + 3; - const char *end = line_sv.data() + line_sv.size(); - bool any = false; - while (c < end && *c != ';') { - while (c < end && (*c == ' ' || *c == '\t')) ++c; - if (c >= end || *c == ';' || *c == '\n' || *c == '\r') break; - char axis = *c; - ++c; - if (axis == 'X' || axis == 'Y' || axis == 'Z') { - char *next; - double v = std::strtod(c, &next); - if (next != c) { - if (axis == 'X') p.x() = v; - else if (axis == 'Y') p.y() = v; - else p.z() = v; - c = next; - any = true; - continue; - } - } - // Skip unrecognized word. - while (c < end && *c != ' ' && *c != '\t' && *c != ';' && *c != '\n') - ++c; - } - return any; - }; - - auto parse_m106 = [](const std::string_view &line_sv) -> int { - // Returns -1 if not an M106, otherwise the S value (0..255). - if (line_sv.size() < 4 || line_sv[0] != 'M') return -1; - if (!(line_sv[1] == '1' && line_sv[2] == '0' && line_sv[3] == '6')) - return -1; - // Find S - size_t s_pos = line_sv.find('S'); - if (s_pos == std::string_view::npos) return -1; - const char *c = line_sv.data() + s_pos + 1; - char *next; - long v = std::strtol(c, &next, 10); - if (next == c) return -1; - return int(std::clamp(v, 0, 255)); - }; - - auto parse_m107 = [](const std::string_view &line_sv) -> bool { - return line_sv.size() >= 4 && line_sv[0] == 'M' && - line_sv[1] == '1' && line_sv[2] == '0' && line_sv[3] == '7'; - }; - - auto parse_tool_change = [this](const std::string_view &line_sv) -> int { - // Returns the new extruder id, or -1 if not a toolchange. - if (line_sv.size() < m_toolchange_prefix.size() + 1) return -1; - if (line_sv.compare(0, m_toolchange_prefix.size(), m_toolchange_prefix) != 0) - return -1; - const char *c = line_sv.data() + m_toolchange_prefix.size(); - char *next; - long v = std::strtol(c, &next, 10); - if (next == c) return -1; - return int(v); - }; - - const char *p = gcode.c_str(); - const char *end = gcode.c_str() + gcode.size(); + const char *p = gcode.data(); + const char *end = p + gcode.size(); while (p < end) { - const char *line_end = p; - while (line_end < end && *line_end != '\n') ++line_end; - const char *next_line = line_end; - if (next_line < end) ++next_line; // include the '\n' + 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); - std::string_view line_sv(p, line_end - p); - - // Track tool changes so the per-line fan eval uses the right extruder. - int new_tool = parse_tool_change(line_sv); - if (new_tool >= 0) - active_extruder = unsigned(new_tool); - - // Track existing fan commands so we can restore the right value when - // exiting a band. - int m106_speed = parse_m106(line_sv); - if (m106_speed >= 0) { - current_main_fan = m106_speed; - if (!in_first_layer_band) - pre_band_main_fan = m106_speed; - } else if (parse_m107(line_sv)) { - current_main_fan = 0; - if (!in_first_layer_band) - pre_band_main_fan = 0; + 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; } - // Movement line: parse XYZ, evaluate plane, possibly emit a fan - // change BEFORE this line. - bool moved = parse_xyz_into(line_sv, cur_pos_mm); - if (moved) { - const int eff_idx = m_first_layer_plane->effective_layer_index(cur_pos_mm); - const int close_n = m_config.close_fan_the_first_x_layers.get_at(active_extruder); - const bool now_in_band = eff_idx < std::max(close_n, 1); - if (now_in_band != in_first_layer_band) { - // Band crossing: emit a M106 with the appropriate speed. - int target_fan; - if (now_in_band) { - // Entering the first-layer band: fan off. - pre_band_main_fan = current_main_fan; - target_fan = compute_main_fan_speed(eff_idx, layer_time, active_extruder); - } else { - // Exiting the band: restore the layer's normal fan speed. - // Use compute_main_fan_speed with the effective index so - // the linear ramp factor (close_fan→full_fan_speed_layer) - // also follows distance from the plane. - target_fan = compute_main_fan_speed(eff_idx, layer_time, active_extruder); - if (target_fan == 0) - target_fan = pre_band_main_fan; - } - if (target_fan != current_main_fan) { - out += GCodeWriter::set_fan(m_config.gcode_flavor, target_fan, part_cooling_fan_min_pwm); - current_main_fan = target_fan; - m_fan_speed = target_fan; - m_current_fan_speed = target_fan; - } - in_first_layer_band = now_in_band; + 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; } diff --git a/src/libslic3r/GCode/CoolingBuffer.hpp b/src/libslic3r/GCode/CoolingBuffer.hpp index 1adb6f5005..0d758b5a5b 100644 --- a/src/libslic3r/GCode/CoolingBuffer.hpp +++ b/src/libslic3r/GCode/CoolingBuffer.hpp @@ -10,7 +10,6 @@ namespace Slic3r { class GCode; class Layer; -class FirstLayerPlane; struct PerExtruderAdjustments; // A standalone G-code filter, to control cooling of the print. @@ -37,13 +36,10 @@ 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); - // First-layer plane: per-line fan re-evaluation post-pass. Walks the - // post-cooldown gcode, tracks XYZ position, and inserts M106 commands at - // band-crossing transitions in slicing-frame coordinates. Only runs - // when m_first_layer_plane is active. - std::string apply_first_layer_plane_fan_eval(std::string &&gcode_in, - size_t layer_id, - float layer_time); + // 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 @@ -74,9 +70,10 @@ private: unsigned int m_current_nozzle; //BBS: current fan speed int m_current_fan_speed; - // First-layer plane evaluator, borrowed from GCode. Null = inactive - // (legacy per-layer fan control). - const FirstLayerPlane *m_first_layer_plane = nullptr; + // 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; }; } diff --git a/src/libslic3r/GCode/FanMover.hpp b/src/libslic3r/GCode/FanMover.hpp index 17addd855c..9567e8e2d5 100644 --- a/src/libslic3r/GCode/FanMover.hpp +++ b/src/libslic3r/GCode/FanMover.hpp @@ -27,6 +27,11 @@ 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 6f0eb7b00e..6d8d3d2ec9 100644 --- a/tests/fff_print/test_print.cpp +++ b/tests/fff_print/test_print.cpp @@ -596,3 +596,58 @@ TEST_CASE("Belt printers refuse an object taller than the gantry clearance", "[P CHECK_FALSE(print.validate().string.empty()); } } + +// 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]") +{ + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.set_deserialize_strict({ + { "belt_printer", 1 }, + { "belt_slice_rotation", "x" }, + { "belt_slice_rotation_angle", 45 }, + { "belt_slice_rotation_global", 1 }, + { "gcode_remap_x", "rev_x" }, + { "gcode_remap_y", "pos_z" }, + { "gcode_remap_z", "pos_y" }, + { "layer_height", 0.2 }, + { "initial_layer_print_height", 0.2 }, + { "skirt_loops", 0 }, + { "z_hop", 0 }, + { "close_fan_the_first_x_layers", 1 }, + { "full_fan_speed_layer", 0 }, + { "fan_min_speed", 100 }, + { "fan_max_speed", 100 }, + { "slow_down_layer_time", 1000 }, + { "fan_cooling_layer_time", 1001 }, + { "reduce_fan_stop_start_freq", 0 }, + { "machine_start_gcode", "T[initial_tool]\n" }, + { "layer_change_gcode", "G92 E0\n" }, + }); + const std::string gcode = slice({ cube(20) }, config); + REQUIRE(! gcode.empty()); + + // The tags are consumed by the cooling buffer and never reach the file. + CHECK(gcode.find(";_BELT_BAND") == std::string::npos); + + size_t fan_off = 0, fan_on = 0; + GCodeReader parser; + parser.parse_buffer(gcode, [&](GCodeReader &, const GCodeReader::GCodeLine &line) { + if (line.cmd_is("M107")) + ++ fan_off; + else if (line.cmd_is("M106")) { + float s = 0.f; + if (line.has_value('S', s) && s <= 0.f) + ++ fan_off; + else + ++ fan_on; + } + }); + // A flat-bed print turns the fan on once. Here it cycles with the layers. + CHECK(fan_off > 10); + CHECK(fan_on > 10); +}