diff --git a/t/cooling.t b/t/cooling.t index e6426546b..0f0aef618 100644 --- a/t/cooling.t +++ b/t/cooling.t @@ -15,88 +15,113 @@ use Slic3r::Test; my $gcodegen; sub buffer { - my $config = shift || Slic3r::Config->new; - + my $config = shift; + if (defined($config)) { + $config = $config->clone(); + } else { + $config = Slic3r::Config->new; + } + my $config_override = shift; + foreach my $key (keys %{$config_override}) { + $config->set($key, ${$config_override}{$key}); + } + my $print_config = Slic3r::Config::Print->new; $print_config->apply_dynamic($config); $gcodegen = Slic3r::GCode->new; $gcodegen->apply_print_config($print_config); $gcodegen->set_layer_count(10); + $gcodegen->set_elapsed_time(0); return Slic3r::GCode::CoolingBuffer->new($gcodegen); } +my $gcode1 = "G1 X100 E1 F3000\n"; +my $print_time1 = 100 / (3000 / 60); # 2 sec +my $gcode2 = $gcode1 . "G1 X0 E1 F3000\n"; +my $print_time2 = 2 * $print_time1; # 4 sec + my $config = Slic3r::Config->new_from_defaults; -$config->set('disable_fan_first_layers', [ 0 ]); +# Default cooling settings. +$config->set('bridge_fan_speed', [ 100 ]); +$config->set('cooling', [ 1 ]); +$config->set('fan_always_on', [ 0 ]); +$config->set('fan_below_layer_time', [ 60 ]); +$config->set('max_fan_speed', [ 100 ]); +$config->set('min_print_speed', [ 10 ]); +$config->set('slowdown_below_layer_time', [ 5 ]); +# Default print speeds. +$config->set('bridge_speed', 60); +$config->set('external_perimeter_speed', '50%'); +$config->set('first_layer_speed', 30); +$config->set('gap_fill_speed', 20); +$config->set('infill_speed', 80); +$config->set('perimeter_speed', 60); +$config->set('small_perimeter_speed', 15); +$config->set('solid_infill_speed', 20); +$config->set('top_solid_infill_speed', 15); +$config->set('max_print_speed', 80); +# Override for tests. +$config->set('disable_fan_first_layers', [ 0 ]); { - my $buffer = buffer($config); - $buffer->gcodegen->set_elapsed_time($buffer->gcodegen->config->slowdown_below_layer_time->[0] + 1); - my $gcode = $buffer->process_layer('G1 F3000;_EXTRUDE_SET_SPEED\nG1 X100 E1', 0); + my $gcode_src = "G1 F3000;_EXTRUDE_SET_SPEED\nG1 X100 E1"; + # Print time of $gcode. + my $print_time = 100 / (3000 / 60); + my $buffer = buffer($config, { 'slowdown_below_layer_time' => [ $print_time * 0.999 ] }); + my $gcode = $buffer->process_layer($gcode_src, 0); like $gcode, qr/F3000/, 'speed is not altered when elapsed time is greater than slowdown threshold'; } { - my $buffer = buffer($config); - $buffer->gcodegen->set_elapsed_time($buffer->gcodegen->config->slowdown_below_layer_time->[0] - 1); - my $gcode = $buffer->process_layer( + my $gcode_src = "G1 X50 F2500\n" . "G1 F3000;_EXTRUDE_SET_SPEED\n" . "G1 X100 E1\n" . "G1 E4 F400", - 0); + # Print time of $gcode. + my $print_time = 50 / (2500 / 60) + 100 / (3000 / 60) + 4 / (400 / 60); + my $buffer = buffer($config, { 'slowdown_below_layer_time' => [ $print_time * 1.001 ] }); + my $gcode = $buffer->process_layer($gcode_src, 0); unlike $gcode, qr/F3000/, 'speed is altered when elapsed time is lower than slowdown threshold'; like $gcode, qr/F2500/, 'speed is not altered for travel moves'; like $gcode, qr/F400/, 'speed is not altered for extruder-only moves'; } { - my $buffer = buffer($config); - $buffer->gcodegen->set_elapsed_time($buffer->gcodegen->config->fan_below_layer_time->[0] + 1); - my $gcode = $buffer->process_layer('G1 X100 E1 F3000', 0); + my $buffer = buffer($config, { + 'fan_below_layer_time' => [ $print_time1 * 0.88 ], + 'slowdown_below_layer_time' => [ $print_time1 * 0.99 ] + }); + my $gcode = $buffer->process_layer($gcode1, 0); unlike $gcode, qr/M106/, 'fan is not activated when elapsed time is greater than fan threshold'; } { - my $buffer = buffer($config); - my $gcode = ""; - for my $obj_id (0 .. 1) { - $gcode .= "G1 X100 E1 F3000\n"; - } - # use an elapsed time which is < the slowdown threshold but greater than it when summed twice - $buffer->gcodegen->set_elapsed_time(2 * ($buffer->gcodegen->config->slowdown_below_layer_time->[0] - 1)); - $gcode .= $buffer->process_layer($gcode, 0); + my $gcode .= buffer($config, { 'slowdown_below_layer_time', [ $print_time2 * 0.99 ] })->process_layer($gcode2, 0); like $gcode, qr/F3000/, 'slowdown is computed on all objects printing at the same Z'; } { - my $buffer = buffer($config); - my $gcode = ""; - for my $layer_id (0 .. 1) { - my $layer_gcode = ""; - for my $obj_id (0 .. 1) { - $layer_gcode .= "G1 X100 E1 F3000\n"; - } - # use an elapsed time which is < the threshold but greater than it when summed twice - $buffer->gcodegen->set_elapsed_time(2 * ($buffer->gcodegen->config->fan_below_layer_time->[0] - 1)); - $gcode .= $buffer->process_layer($layer_gcode, $layer_id); - } - unlike $gcode, qr/M106/, 'fan activation is computed on all objects printing at different Z'; + # use an elapsed time which is < the threshold but greater than it when summed twice + my $buffer = buffer($config, { + 'fan_below_layer_time' => [ $print_time2 * 0.65], + 'slowdown_below_layer_time' => [ $print_time2 * 0.7 ] + }); + my $gcode = $buffer->process_layer($gcode2, 0) . + $buffer->process_layer($gcode2, 1); + unlike $gcode, qr/M106/, 'fan is not activated on all objects printing at different Z'; } { - my $buffer = buffer($config); - my $gcode = ""; - for my $layer_id (0 .. 1) { - my $layer_gcode = ""; - for my $obj_id (0 .. 1) { - $layer_gcode .= "G1 X100 E1 F3000\n"; - } - # use an elapsed time which is < the threshold even when summed twice - $buffer->gcodegen->set_elapsed_time(2 * ($buffer->gcodegen->config->fan_below_layer_time->[0]/2 - 1)); - $gcode .= $buffer->process_layer($layer_gcode, $layer_id); - } - like $gcode, qr/M106/, 'fan activation is computed on all objects printing at different Z'; + # use an elapsed time which is < the threshold even when summed twice + my $buffer = buffer($config, { + 'fan_below_layer_time' => [ $print_time2 + 1 ], + 'slowdown_below_layer_time' => [ $print_time2 + 2 ] + }); + my $gcode = $buffer->process_layer($gcode2, 0) . + $buffer->process_layer($gcode2, 1); + like $gcode, qr/M106/, 'fan is activated on all objects printing at different Z'; } { diff --git a/xs/src/libslic3r/GCode.cpp b/xs/src/libslic3r/GCode.cpp index 6ff0775af..635a017eb 100644 --- a/xs/src/libslic3r/GCode.cpp +++ b/xs/src/libslic3r/GCode.cpp @@ -171,6 +171,7 @@ std::string WipeTowerIntegration::append_tcr(GCode &gcodegen, const WipeTower::T // Accumulate the elapsed time for the correct calculation of layer cooling. //FIXME currently disabled as Slic3r PE needs to be updated to differentiate the moves it could slow down // from the moves it could not. + gcodegen.writer().elapsed_time()->total += tcr.elapsed_time; gcodegen.writer().elapsed_time()->other += tcr.elapsed_time; // A phony move to the end position at the wipe tower. gcodegen.writer().travel_to_xy(Pointf(tcr.end_pos.x, tcr.end_pos.y)); @@ -570,7 +571,6 @@ bool GCode::do_export(FILE *file, Print &print) initial_extruder_id = new_extruder_id; final_extruder_id = tool_ordering.last_extruder(); assert(final_extruder_id != (unsigned int)-1); - m_cooling_buffer->set_current_extruder(initial_extruder_id); } this->set_origin(unscale(copy.x), unscale(copy.y)); if (finished_objects > 0) { @@ -590,6 +590,9 @@ bool GCode::do_export(FILE *file, Print &print) // Set first layer extruder. this->_print_first_layer_extruder_temperatures(file, print, initial_extruder_id, false); } + // Reset the cooling buffer internal state (the current position, feed rate, accelerations). + m_cooling_buffer->reset(); + m_cooling_buffer->set_current_extruder(initial_extruder_id); // Pair the object layers with the support layers by z, extrude them. std::vector layers_to_print = collect_layers_to_print(object); for (const LayerToPrint <p : layers_to_print) { @@ -597,6 +600,8 @@ bool GCode::do_export(FILE *file, Print &print) lrs.emplace_back(std::move(ltp)); this->process_layer(file, print, lrs, tool_ordering.tools_for_layer(ltp.print_z()), © - object._shifted_copies.data()); } + if (m_pressure_equalizer) + write(file, m_pressure_equalizer->process("", true)); ++ finished_objects; // Flag indicating whether the nozzle temperature changes from 1st to 2nd layer were performed. // Reset it when starting another object from 1st layer. @@ -624,6 +629,8 @@ bool GCode::do_export(FILE *file, Print &print) m_wipe_tower->next_layer(); this->process_layer(file, print, layer.second, layer_tools, size_t(-1)); } + if (m_pressure_equalizer) + write(file, m_pressure_equalizer->process("", true)); if (m_wipe_tower) // Purge the extruder, pull out the active filament. write(file, m_wipe_tower->finalize(*this)); @@ -1099,28 +1106,21 @@ void GCode::process_layer( // (we must feed all the G-code into the post-processor, including the first // bottom non-spiral layers otherwise it will mess with positions) // we apply spiral vase at this stage because it requires a full layer. - // Just a reminder: A spiral vase mode is allowed for a single object, single material print only. + // Just a reminder: A spiral vase mode is allowed for a single object per layer, single material print only. if (m_spiral_vase) gcode = m_spiral_vase->process_layer(gcode); // Apply cooling logic; this may alter speeds. if (m_cooling_buffer) - //FIXME Update the CoolingBuffer class to ignore the object ID, which does not make sense anymore - // once all extrusions of a layer are processed at once. - // Update the test cases. gcode = m_cooling_buffer->process_layer(gcode, layer.id()); - write(file, this->filter(std::move(gcode), false)); -} -std::string GCode::filter(std::string &&gcode, bool flush) -{ - // apply pressure equalization if enabled; + // Apply pressure equalization if enabled; // printf("G-code before filter:\n%s\n", gcode.c_str()); - std::string out = m_pressure_equalizer ? - m_pressure_equalizer->process(gcode.c_str(), flush) : - std::move(gcode); + if (m_pressure_equalizer) + gcode = m_pressure_equalizer->process(gcode.c_str(), false); // printf("G-code after filter:\n%s\n", out.c_str()); - return out; + + write(file, gcode); } void GCode::apply_print_config(const PrintConfig &print_config) @@ -1841,12 +1841,17 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, gcode += buf; } } - if (is_bridge(path.role()) && m_enable_cooling_markers) - gcode += ";_BRIDGE_FAN_START\n"; - std::string comment = ";_EXTRUDE_SET_SPEED"; - if (path.role() == erExternalPerimeter) - comment += ";_EXTERNAL_PERIMETER"; - gcode += m_writer.set_speed(F, "", m_enable_cooling_markers ? comment : ""); + std::string comment; + if (m_enable_cooling_markers) { + if (is_bridge(path.role())) + gcode += ";_BRIDGE_FAN_START\n"; + else + comment = ";_EXTRUDE_SET_SPEED"; + if (path.role() == erExternalPerimeter) + comment += ";_EXTERNAL_PERIMETER"; + } + // F is mm per minute. + gcode += m_writer.set_speed(F, "", comment); double path_length = 0.; { std::string comment = m_config.gcode_comments ? description : ""; @@ -1859,18 +1864,26 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, comment); } } - if (is_bridge(path.role()) && m_enable_cooling_markers) - gcode += ";_BRIDGE_FAN_END\n"; + if (m_enable_cooling_markers) + gcode += is_bridge(path.role()) ? ";_BRIDGE_FAN_END\n" : ";_EXTRUDE_END\n"; this->set_last_pos(path.last_point()); if (m_config.cooling.values.front()) { - float t = path_length / F * 60.f; + float t = float(path_length / F * 60); m_writer.elapsed_time()->total += t; + assert(! (is_bridge(path.role()) && path.role() == erExternalPerimeter)); if (is_bridge(path.role())) m_writer.elapsed_time()->bridges += t; - if (path.role() == erExternalPerimeter) - m_writer.elapsed_time()->external_perimeters += t; + else { + // Maximum print time of this extrusion, respecting the min_print_speed. + float t_max = std::max(t, float(path_length / std::max(0.1, EXTRUDER_CONFIG(min_print_speed)))); + if (path.role() == erExternalPerimeter) + m_writer.elapsed_time()->external_perimeters += t; + else + m_writer.elapsed_time()->max_stretch_time_no_ext_perimetes += t_max; + m_writer.elapsed_time()->max_stretch_time_total += t_max; + } } return gcode; diff --git a/xs/src/libslic3r/GCode.hpp b/xs/src/libslic3r/GCode.hpp index c85f28daa..c09ee68a4 100644 --- a/xs/src/libslic3r/GCode.hpp +++ b/xs/src/libslic3r/GCode.hpp @@ -267,8 +267,6 @@ protected: // this flag triggers first layer speeds bool on_first_layer() const { return m_layer != nullptr && m_layer->id() == 0; } - std::string filter(std::string &&gcode, bool flush); - friend ObjectByExtruder& object_by_extruder( std::map> &by_extruder, unsigned int extruder_id, diff --git a/xs/src/libslic3r/GCode/CoolingBuffer.cpp b/xs/src/libslic3r/GCode/CoolingBuffer.cpp index 4d8a7ef34..007553699 100644 --- a/xs/src/libslic3r/GCode/CoolingBuffer.cpp +++ b/xs/src/libslic3r/GCode/CoolingBuffer.cpp @@ -4,125 +4,463 @@ #include #include +#if 0 + #define DEBUG + #define _DEBUG + #undef NDEBUG +#endif + +#include + namespace Slic3r { -void apply_speed_factor(std::string &line, float speed_factor, float min_print_speed) +CoolingBuffer::CoolingBuffer(GCode &gcodegen) : m_gcodegen(gcodegen), m_current_extruder(0) { - // find pos of F - size_t pos = line.find_first_of('F'); - size_t last_pos = line.find_first_of(' ', pos+1); - - // extract current speed - float speed; - { - std::istringstream iss(line.substr(pos+1, last_pos)); - iss >> speed; - } - - // change speed - speed *= speed_factor; - speed = std::max(speed, min_print_speed); - - // replace speed in string - { - std::ostringstream oss; - oss << speed; - line.replace(pos+1, last_pos-pos, oss.str()); - } + this->reset(); } -std::string CoolingBuffer::process_layer(const std::string &gcode_src, size_t layer_id) +void CoolingBuffer::reset() { - const FullPrintConfig &config = m_gcodegen.config(); + m_current_pos.assign(5, 0.f); + Pointf3 pos = m_gcodegen.writer().get_position(); + m_current_pos[0] = float(pos.x); + m_current_pos[1] = float(pos.y); + m_current_pos[2] = float(pos.z); + m_current_pos[4] = float(m_gcodegen.config().travel_speed.value); +} - std::string gcode = gcode_src; - int fan_speed = config.fan_always_on.values.front() ? config.min_fan_speed.values.front() : 0; - float speed_factor = 1.0; - bool slowdown_external = true; +#define EXTRUDER_CONFIG(OPT) config.OPT.get_at(m_current_extruder) - const std::vector &elapsed_times = m_gcodegen.writer().elapsed_times(); - ElapsedTime elapsed_time; - for (const ElapsedTime &et : elapsed_times) - elapsed_time += et; +std::string CoolingBuffer::process_layer(const std::string &gcode, size_t layer_id) +{ + const FullPrintConfig &config = m_gcodegen.config(); + const auto &elapsed_times = m_gcodegen.writer().elapsed_times(); + const size_t num_extruders = elapsed_times.size(); - if (config.cooling.values.front()) { - #ifdef SLIC3R_DEBUG - printf("Layer %zu estimated printing time: %f seconds\n", layer_id, elapsed_time.total); - #endif - if (elapsed_time.total < (float)config.slowdown_below_layer_time.values.front()) { - // Layer time very short. Enable the fan to a full throttle and slow down the print - // (stretch the layer print time to slowdown_below_layer_time). - fan_speed = config.max_fan_speed.values.front(); - - // We are not altering speed of bridges. - float time_to_stretch = elapsed_time.stretchable(); - float target_time = (float)config.slowdown_below_layer_time.values.front() - elapsed_time.non_stretchable(); - - // If we spend most of our time on external perimeters include them in the slowdown, - // otherwise only alter other extrusions. - if (elapsed_time.external_perimeters < 0.5f * time_to_stretch) { - time_to_stretch -= elapsed_time.external_perimeters; - target_time -= elapsed_time.external_perimeters; - slowdown_external = false; - } - - speed_factor = time_to_stretch / target_time; - } else if (elapsed_time.total < (float)config.fan_below_layer_time.values.front()) { - // Layer time quite short. Enable the fan proportionally according to the current layer time. - fan_speed = config.max_fan_speed.values.front() - - (config.max_fan_speed.values.front() - config.min_fan_speed.values.front()) - * (elapsed_time.total - (float)config.slowdown_below_layer_time.values.front()) - / (config.fan_below_layer_time.values.front() - config.slowdown_below_layer_time.values.front()); + // Calculate the required per extruder time stretches. + struct Adjustment { + Adjustment() {} + // Calculate the total elapsed time per this extruder, adjusted for the slowdown. + float elapsed_time_total() { + float time_total = 0.f; + for (const Line &line : lines) + time_total += line.time; + return time_total; } - - #ifdef SLIC3R_DEBUG - printf(" fan = %d%%, speed = %f%%\n", fan_speed, speed_factor * 100); - #endif - - if (speed_factor < 1.0) { - // Adjust feed rate of G1 commands marked with an _EXTRUDE_SET_SPEED - // as long as they are not _WIPE moves (they cannot if they are _EXTRUDE_SET_SPEED) - // and they are not preceded directly by _BRIDGE_FAN_START (do not adjust bridging speed). - std::string new_gcode; - std::istringstream ss(gcode); - std::string line; - bool bridge_fan_start = false; - float min_print_speed = float(config.min_print_speed.values.front() * 60.); - while (std::getline(ss, line)) { - if (boost::starts_with(line, "G1") - && boost::contains(line, ";_EXTRUDE_SET_SPEED") - && !boost::contains(line, ";_WIPE") - && !bridge_fan_start - && (slowdown_external || !boost::contains(line, ";_EXTERNAL_PERIMETER"))) { - apply_speed_factor(line, speed_factor, min_print_speed); - boost::replace_first(line, ";_EXTRUDE_SET_SPEED", ""); + // Calculate the maximum time when slowing down. + float maximum_time(bool slowdown_external_perimeters) { + float time_total = 0.f; + for (const Line &line : lines) + if (line.adjustable(slowdown_external_perimeters)) { + if (line.time_max == FLT_MAX) + return FLT_MAX; + else + time_total += line.time_max; + } else + time_total += line.time; + return time_total; + } + // Calculate the non-adjustable part of the total time. + float non_adjustable_time(bool slowdown_external_perimeters) { + float time_total = 0.f; + for (const Line &line : lines) + if (! line.adjustable(slowdown_external_perimeters)) + time_total += line.time; + return time_total; + } + float slow_down_maximum(bool slowdown_external_perimeters) { + float time_total = 0.f; + for (Line &line : lines) { + if (line.adjustable(slowdown_external_perimeters)) { + assert(line.time_max >= 0.f && line.time_max < FLT_MAX); + line.slowdown = true; + line.time = line.time_max; } - bridge_fan_start = boost::starts_with(line, ";_BRIDGE_FAN_START"); - new_gcode += line + '\n'; + time_total += line.time; } - gcode = new_gcode; + return time_total; } + float slow_down_proportional(float factor, bool slowdown_external_perimeters) { + assert(factor >= 1.f); + float time_total = 0.f; + for (Line &line : lines) { + if (line.adjustable(slowdown_external_perimeters)) { + line.slowdown = true; + line.time = std::min(line.time_max, line.time * factor); + } + time_total += line.time; + } + return time_total; + } + + struct Line + { + enum Type { + TYPE_SET_TOOL = 1 << 0, + TYPE_EXTRUDE_END = 1 << 1, + TYPE_BRIDGE_FAN_START = 1 << 2, + TYPE_BRIDGE_FAN_END = 1 << 3, + TYPE_G0 = 1 << 4, + TYPE_G1 = 1 << 5, + TYPE_ADJUSTABLE = 1 << 6, + TYPE_EXTERNAL_PERIMETER = 1 << 7, + TYPE_WIPE = 1 << 8, + TYPE_G4 = 1 << 9, + TYPE_G92 = 1 << 10, + }; + + Line(unsigned int type, size_t line_start, size_t line_end) : + type(type), line_start(line_start), line_end(line_end), + length(0.f), time(0.f), time_max(0.f), slowdown(false) {} + + bool adjustable(bool slowdown_external_perimeters) const { + return (this->type & TYPE_ADJUSTABLE) && + (! (this->type & TYPE_EXTERNAL_PERIMETER) || slowdown_external_perimeters) && + this->time < this->time_max; + } + + size_t type; + // Start of this line at the G-code snippet. + size_t line_start; + // End of this line at the G-code snippet. + size_t line_end; + // XY Euclidian length of this segment. + float length; + // Current duration of this segment. + float time; + // Maximum duration of this segment. + float time_max; + // If marked with the "slowdown" flag, the line has been slowed down. + bool slowdown; + }; + + // Parsed lines. + std::vector lines; + }; + std::vector adjustments(num_extruders, Adjustment()); + const std::string toolchange_prefix = m_gcodegen.writer().toolchange_prefix(); + // Parse the layer G-code for the moves, which could be adjusted. + { + float min_print_speed = float(EXTRUDER_CONFIG(min_print_speed)); + auto it_elapsed_time = std::lower_bound(elapsed_times.begin(), elapsed_times.end(), ElapsedTime(m_current_extruder)); + Adjustment *adjustment = &adjustments[it_elapsed_time - elapsed_times.begin()]; + unsigned int initial_extruder = m_current_extruder; + const char *line_start = gcode.c_str(); + const char *line_end = line_start; + const char extrusion_axis = config.get_extrusion_axis()[0]; + // Index of an existing Adjustment::Line of the current adjustment, which holds the feedrate setting command + // for a sequence of extrusion moves. + size_t active_speed_modifier = size_t(-1); + for (; *line_start != 0; line_start = line_end) { + while (*line_end != '\n' && *line_end != 0) + ++ line_end; + std::string sline(line_start, line_end); + Adjustment::Line line(0, line_start - gcode.c_str(), line_end - gcode.c_str()); + if (boost::starts_with(sline, "G0 ")) + line.type = Adjustment::Line::TYPE_G0; + else if (boost::starts_with(sline, "G1 ")) + line.type = Adjustment::Line::TYPE_G1; + else if (boost::starts_with(sline, "G92 ")) + line.type = Adjustment::Line::TYPE_G92; + if (line.type) { + // G0, G1 or G92 + // Parse the G-code line. + std::vector new_pos(m_current_pos); + const char *c = sline.data() + 3; + for (;;) { + // Skip whitespaces. + for (; *c == ' ' || *c == '\t'; ++ c); + if (*c == 0 || *c == ';') + break; + // Parse the axis. + size_t axis = (*c >= 'X' && *c <= 'Z') ? (*c - 'X') : + (*c == extrusion_axis) ? 3 : (*c == 'F') ? 4 : size_t(-1); + if (axis != size_t(-1)) { + new_pos[axis] = float(atof(++c)); + if (axis == 4) + // Convert mm/min to mm/sec. + new_pos[4] /= 60.f; + } + // Skip this word. + for (; *c != ' ' && *c != '\t' && *c != 0; ++ c); + } + bool external_perimeter = boost::contains(sline, ";_EXTERNAL_PERIMETER"); + bool wipe = boost::contains(sline, ";_WIPE"); + if (external_perimeter) + line.type |= Adjustment::Line::TYPE_EXTERNAL_PERIMETER; + if (wipe) + line.type |= Adjustment::Line::TYPE_WIPE; + if (boost::contains(sline, ";_EXTRUDE_SET_SPEED") && ! wipe) { + line.type |= Adjustment::Line::TYPE_ADJUSTABLE; + active_speed_modifier = adjustment->lines.size(); + } + if ((line.type & Adjustment::Line::TYPE_G92) == 0) { + // G0 or G1. Calculate the duration. + if (config.use_relative_e_distances.value) + // Reset extruder accumulator. + m_current_pos[3] = 0.f; + float dif[4]; + for (size_t i = 0; i < 4; ++ i) + dif[i] = new_pos[i] - m_current_pos[i]; + float dxy2 = dif[0] * dif[0] + dif[1] * dif[1]; + float dxyz2 = dxy2 + dif[2] * dif[2]; + if (dxyz2 > 0.f) { + // Movement in xyz, calculate time from the xyz Euclidian distance. + line.length = sqrt(dxyz2); + } else if (std::abs(dif[3]) > 0.f) { + // Movement in the extruder axis. + line.length = std::abs(dif[3]); + } + if (line.length > 0) + line.time = line.length / new_pos[4]; // current F + line.time_max = line.time; + if ((line.type & Adjustment::Line::TYPE_ADJUSTABLE) || active_speed_modifier != size_t(-1)) + line.time_max = (min_print_speed == 0.f) ? FLT_MAX : std::max(line.time, line.length / min_print_speed); + if (active_speed_modifier < adjustment->lines.size() && (line.type & Adjustment::Line::TYPE_G1)) { + Adjustment::Line &sm = adjustment->lines[active_speed_modifier]; + sm.length += line.length; + sm.time += line.time; + if (sm.time_max != FLT_MAX) { + if (line.time_max == FLT_MAX) + sm.time_max = FLT_MAX; + else + sm.time_max += line.time_max; + } + // Don't store this line. + line.type = 0; + } + } + m_current_pos = std::move(new_pos); + } else if (boost::starts_with(sline, ";_EXTRUDE_END")) { + line.type = Adjustment::Line::TYPE_EXTRUDE_END; + active_speed_modifier = size_t(-1); + } else if (boost::starts_with(sline, toolchange_prefix)) { + // Switch the tool. + unsigned int new_extruder = (unsigned int)atoi(sline.c_str() + toolchange_prefix.size()); + if (new_extruder != m_current_extruder) { + m_current_extruder = new_extruder; + min_print_speed = float(EXTRUDER_CONFIG(min_print_speed)); + it_elapsed_time = std::lower_bound(elapsed_times.begin(), elapsed_times.end(), ElapsedTime(m_current_extruder)); + adjustment = &adjustments[it_elapsed_time - elapsed_times.begin()]; + } + } else if (boost::starts_with(sline, ";_BRIDGE_FAN_START")) { + line.type = Adjustment::Line::TYPE_BRIDGE_FAN_START; + } else if (boost::starts_with(sline, ";_BRIDGE_FAN_END")) { + line.type = Adjustment::Line::TYPE_BRIDGE_FAN_END; + } else if (boost::starts_with(sline, "G4 ")) { + // Parse the wait time. + line.type = Adjustment::Line::TYPE_G4; + size_t pos_S = sline.find('S', 3); + size_t pos_P = sline.find('P', 3); + line.time = line.time_max = float( + (pos_S > 0) ? atof(sline.c_str() + pos_S + 1) : + (pos_P > 0) ? atof(sline.c_str() + pos_P + 1) * 0.001 : 0.); + } + if (line.type != 0) + adjustment->lines.emplace_back(std::move(line)); + while (*line_end == '\n') + ++ line_end; + } + m_current_extruder = initial_extruder; } - if (layer_id < config.disable_fan_first_layers.values.front()) - fan_speed = 0; - - gcode = m_gcodegen.writer().set_fan(fan_speed) + gcode; - - // bridge fan speed - if (!config.cooling.values.front() || config.bridge_fan_speed.values.front() == 0 || layer_id < config.disable_fan_first_layers.values.front()) { - boost::replace_all(gcode, ";_BRIDGE_FAN_START", ""); - boost::replace_all(gcode, ";_BRIDGE_FAN_END", ""); - } else { - boost::replace_all(gcode, ";_BRIDGE_FAN_START", m_gcodegen.writer().set_fan(config.bridge_fan_speed.values.front(), true)); - boost::replace_all(gcode, ";_BRIDGE_FAN_END", m_gcodegen.writer().set_fan(fan_speed, true)); + + // Sort the extruders by the increasing slowdown_below_layer_time. + std::vector by_slowdown_layer_time; + by_slowdown_layer_time.reserve(num_extruders); + // Only insert entries, which are adjustable (have cooling enabled and non-zero stretchable time). + // Collect total print time of non-adjustable extruders. + float elapsed_time_total_non_adjustable = 0.f; + for (size_t i = 0; i < num_extruders; ++ i) { + if (config.cooling.get_at(elapsed_times[i].extruder_id)) + by_slowdown_layer_time.emplace_back(i); + else + elapsed_time_total_non_adjustable += adjustments[i].elapsed_time_total(); } - boost::replace_all(gcode, ";_WIPE", ""); - boost::replace_all(gcode, ";_EXTRUDE_SET_SPEED", ""); - boost::replace_all(gcode, ";_EXTERNAL_PERIMETER", ""); - - m_object_ids_visited.clear(); + std::sort(by_slowdown_layer_time.begin(), by_slowdown_layer_time.end(), + [&config, &elapsed_times](const size_t idx1, const size_t idx2){ + return config.slowdown_below_layer_time.get_at(elapsed_times[idx1].extruder_id) < + config.slowdown_below_layer_time.get_at(elapsed_times[idx2].extruder_id); + }); + + // Elapsed time after adjustment. + float elapsed_time_total = 0.f; + { + // Elapsed time for the already adjusted extruders. + float elapsed_time_total0 = elapsed_time_total_non_adjustable; + for (size_t i_by_slowdown_layer_time = 0; i_by_slowdown_layer_time < by_slowdown_layer_time.size(); ++ i_by_slowdown_layer_time) { + // Idx in elapsed_times and adjustments. + size_t idx = by_slowdown_layer_time[i_by_slowdown_layer_time]; + // Macro to sum or adjust all sections starting with i_by_slowdown_layer_time. + #define FORALL_UNPROCESSED(ACCUMULATOR, ACTION) \ + ACCUMULATOR = elapsed_time_total0;\ + for (size_t j = i_by_slowdown_layer_time; j < by_slowdown_layer_time.size(); ++ j) \ + ACCUMULATOR += adjustments[by_slowdown_layer_time[j]].ACTION + // Calculate the current adjusted elapsed_time_total over the non-finalized extruders. + float total; + FORALL_UNPROCESSED(total, elapsed_time_total()); + float slowdown_below_layer_time = float(config.slowdown_below_layer_time.get_at(elapsed_times[idx].extruder_id)) * 1.001f; + if (total > slowdown_below_layer_time) { + // The current total time is above the minimum threshold of the rest of the extruders, don't adjust anything. + } else { + // Adjust this and all the following (higher config.slowdown_below_layer_time) extruders. + // Sum maximum slow down time as if everything was slowed down including the external perimeters. + float max_time; + FORALL_UNPROCESSED(max_time, maximum_time(true)); + if (max_time > slowdown_below_layer_time) { + // By slowing every possible movement, the layer time could be reached. Now decide + // whether the external perimeters shall be slowed down as well. + float max_time_nep; + FORALL_UNPROCESSED(max_time_nep, maximum_time(false)); + if (max_time_nep > slowdown_below_layer_time) { + // It is sufficient to slow down the non-external perimeter moves to reach the target layer time. + // Slow down the non-external perimeters proportionally. + float non_adjustable_time; + FORALL_UNPROCESSED(non_adjustable_time, non_adjustable_time(false)); + // The following step is a linear programming task due to the minimum movement speeds of the print moves. + // Run maximum 5 iterations until a good enough approximation is reached. + for (size_t iter = 0; iter < 5; ++ iter) { + float factor = (slowdown_below_layer_time - non_adjustable_time) / (total - non_adjustable_time); + assert(factor > 1.f); + FORALL_UNPROCESSED(total, slow_down_proportional(factor, false)); + if (total > 0.95f * slowdown_below_layer_time) + break; + } + } else { + // Slow down everything. First slow down the non-external perimeters to maximum. + FORALL_UNPROCESSED(total, slow_down_maximum(false)); + // Slow down the external perimeters proportionally. + float non_adjustable_time; + FORALL_UNPROCESSED(non_adjustable_time, non_adjustable_time(true)); + for (size_t iter = 0; iter < 5; ++ iter) { + float factor = (slowdown_below_layer_time - non_adjustable_time) / (total - non_adjustable_time); + assert(factor > 1.f); + FORALL_UNPROCESSED(total, slow_down_proportional(factor, true)); + if (total > 0.95f * slowdown_below_layer_time) + break; + } + } + } else { + // Slow down to maximum possible. + FORALL_UNPROCESSED(total, slow_down_maximum(true)); + } + } + #undef FORALL_UNPROCESSED + // Sum the final elapsed time for all extruders up to i_by_slowdown_layer_time. + if (i_by_slowdown_layer_time + 1 == by_slowdown_layer_time.size()) + // Optimization for single extruder prints. + elapsed_time_total0 = total; + else + elapsed_time_total0 += adjustments[idx].elapsed_time_total(); + } + elapsed_time_total = elapsed_time_total0; + } + + // Transform the G-code. + // First sort the adjustment lines by their position in the source G-code. + std::vector lines; + { + size_t n_lines = 0; + for (const Adjustment &adj : adjustments) + n_lines += adj.lines.size(); + lines.reserve(n_lines); + for (const Adjustment &adj : adjustments) + for (const Adjustment::Line &line : adj.lines) + lines.emplace_back(&line); + std::sort(lines.begin(), lines.end(), [](const Adjustment::Line *ln1, const Adjustment::Line *ln2) { return ln1->line_start < ln2->line_start; } ); + } + // Second generate the adjusted G-code. + std::string new_gcode; + new_gcode.reserve(gcode.size() * 2); + int fan_speed = -1; + bool bridge_fan_control = false; + int bridge_fan_speed = 0; + auto change_extruder_set_fan = [ this, layer_id, elapsed_time_total, &new_gcode, &fan_speed, &bridge_fan_control, &bridge_fan_speed ]() { + const FullPrintConfig &config = m_gcodegen.config(); + int min_fan_speed = EXTRUDER_CONFIG(min_fan_speed); + int fan_speed_new = EXTRUDER_CONFIG(fan_always_on) ? min_fan_speed : 0; + if (layer_id >= EXTRUDER_CONFIG(disable_fan_first_layers)) { + int max_fan_speed = EXTRUDER_CONFIG(max_fan_speed); + float slowdown_below_layer_time = float(EXTRUDER_CONFIG(slowdown_below_layer_time)); + float fan_below_layer_time = float(EXTRUDER_CONFIG(fan_below_layer_time)); + if (EXTRUDER_CONFIG(cooling)) { + if (elapsed_time_total < slowdown_below_layer_time) { + // Layer time very short. Enable the fan to a full throttle. + fan_speed_new = max_fan_speed; + } else if (elapsed_time_total < fan_below_layer_time) { + // Layer time quite short. Enable the fan proportionally according to the current layer time. + assert(elapsed_time_total >= slowdown_below_layer_time); + double t = (elapsed_time_total - slowdown_below_layer_time) / (fan_below_layer_time - slowdown_below_layer_time); + fan_speed_new = int(floor(t * min_fan_speed + (1. - t) * max_fan_speed) + 0.5); + } + } + bridge_fan_speed = EXTRUDER_CONFIG(bridge_fan_speed); + bridge_fan_control = bridge_fan_speed > fan_speed_new; + } else { + bridge_fan_control = false; + bridge_fan_speed = 0; + } + if (fan_speed_new != fan_speed) { + fan_speed = fan_speed_new; + new_gcode += m_gcodegen.writer().set_fan(fan_speed); + } + }; + change_extruder_set_fan(); + + size_t pos = 0; + for (const Adjustment::Line *line : lines) { + if (line->line_start > pos) + new_gcode.append(gcode.c_str() + pos, line->line_start - pos); + if (line->type & Adjustment::Line::TYPE_SET_TOOL) { + unsigned int new_extruder = (unsigned int)atoi(gcode.c_str() + line->line_start + toolchange_prefix.size()); + if (new_extruder != m_current_extruder) { + m_current_extruder = new_extruder; + change_extruder_set_fan(); + } + } else if (line->type & Adjustment::Line::TYPE_BRIDGE_FAN_START) { + if (bridge_fan_control) + new_gcode += m_gcodegen.writer().set_fan(bridge_fan_speed, true) + "\n"; + } else if (line->type & Adjustment::Line::TYPE_BRIDGE_FAN_END) { + if (bridge_fan_control) + new_gcode += m_gcodegen.writer().set_fan(fan_speed, true) + "\n"; + } else if (line->type & Adjustment::Line::TYPE_EXTRUDE_END) { + // Just remove this comment. + } else if (line->type & (Adjustment::Line::TYPE_ADJUSTABLE | Adjustment::Line::TYPE_EXTERNAL_PERIMETER | Adjustment::Line::TYPE_WIPE)) { + // Start of the comment. The line type indicates there must be some comment present. + const char *end = strchr(gcode.c_str() + line->line_start, ';'); + if (line->slowdown) { + // Replace the feedrate. + const char *pos = strstr(gcode.c_str() + line->line_start + 2, " F") + 2; + new_gcode.append(gcode.c_str() + line->line_start, pos - gcode.c_str() - line->line_start); + char buf[64]; + sprintf(buf, "%d", int(floor(60. * (line->length / line->time) + 0.5))); + new_gcode += buf; + // Skip the non-whitespaces up to the comment. + for (; *pos != ' ' && *pos != ';'; ++ pos); + // Append the rest of the line without the comment. + if (pos < end) + new_gcode.append(pos, end - pos); + } else { + // Append the line without the comment. + new_gcode.append(gcode.c_str() + line->line_start, end - gcode.c_str() - line->line_start); + } + // Process the comments, remove ";_EXTRUDE_SET_SPEED", ";_EXTERNAL_PERIMETER", ";_WIPE" + std::string comment(end, gcode.c_str() + line->line_end); + boost::replace_all(comment, ";_EXTRUDE_SET_SPEED", ""); + if (line->type & Adjustment::Line::TYPE_EXTERNAL_PERIMETER) + boost::replace_all(comment, ";_EXTERNAL_PERIMETER", ""); + if (line->type & Adjustment::Line::TYPE_WIPE) + boost::replace_all(comment, ";_WIPE", ""); + new_gcode += comment; + } else { + new_gcode.append(gcode.c_str() + line->line_start, line->line_end - line->line_start); + } + pos = line->line_end; + } + if (pos < gcode.size()) + new_gcode.append(gcode.c_str() + pos, gcode.size() - pos); + m_gcodegen.writer().reset_elapsed_times(); - return gcode; + return new_gcode; } -} +} // namespace Slic3r diff --git a/xs/src/libslic3r/GCode/CoolingBuffer.hpp b/xs/src/libslic3r/GCode/CoolingBuffer.hpp index 354a51768..baabf597b 100644 --- a/xs/src/libslic3r/GCode/CoolingBuffer.hpp +++ b/xs/src/libslic3r/GCode/CoolingBuffer.hpp @@ -13,7 +13,10 @@ class Layer; struct ElapsedTime { ElapsedTime(unsigned int extruder_id = 0) : extruder_id(extruder_id) { this->reset(); } - void reset() { total = bridges = external_perimeters = travel = other = 0.f; } + void reset() { + total = bridges = external_perimeters = travel = other = 0.f; + max_stretch_time_total = max_stretch_time_no_ext_perimetes = 0.f; + } ElapsedTime& operator+=(const ElapsedTime &rhs) { this->total += rhs.total; @@ -21,13 +24,17 @@ struct ElapsedTime this->external_perimeters += rhs.external_perimeters; this->travel += rhs.travel; this->other += rhs.other; + this->max_stretch_time_total += rhs.max_stretch_time_total; + this->max_stretch_time_no_ext_perimetes += rhs.max_stretch_time_no_ext_perimetes; return *this; } // Potion of the total time, which cannot be stretched to heed the minimum layer print time. - float non_stretchable() const { return this->bridges + this->travel + this->other; } + float non_stretchable(bool stretch_external_perimeters = true) const + { return this->bridges + this->travel + this->other + (stretch_external_perimeters ? 0.f : this->external_perimeters); } // Potion of the total time, which could be stretched to heed the minimum layer print time. - float stretchable() const { return this->total - this->non_stretchable(); } + float stretchable(bool stretch_external_perimeters = true) const + { return this->total - this->non_stretchable(stretch_external_perimeters); } // For which extruder ID has this statistics been collected? unsigned int extruder_id; @@ -38,6 +45,11 @@ struct ElapsedTime float external_perimeters; float travel; float other; + // Per feature maximum time, to which the extrusion could be stretched to respect the extruder specific min_print_speed. + // Maximum stretch time, to which the time this->stretchable() could be extended. + float max_stretch_time_total; + // Maximum stretch time, to which the time (this->stretchable() - external_perimeters) could be extended. + float max_stretch_time_no_ext_perimetes; }; // Sort ElapsedTime objects by the extruder id by default. @@ -54,7 +66,8 @@ and the print is modified to stretch over a minimum layer time. class CoolingBuffer { public: - CoolingBuffer(GCode &gcodegen) : m_gcodegen(gcodegen) {} + CoolingBuffer(GCode &gcodegen); + void reset(); void set_current_extruder(unsigned int extruder_id) { m_current_extruder = extruder_id; } std::string process_layer(const std::string &gcode, size_t layer_id); GCode* gcodegen() { return &m_gcodegen; } @@ -64,8 +77,11 @@ private: GCode& m_gcodegen; std::string m_gcode; + // Internal data. + // X,Y,Z,E,F + std::vector m_axis; + std::vector m_current_pos; unsigned int m_current_extruder; - std::set m_object_ids_visited; }; } diff --git a/xs/src/libslic3r/GCodeReader.cpp b/xs/src/libslic3r/GCodeReader.cpp index 6ad937bbd..7df370d1c 100644 --- a/xs/src/libslic3r/GCodeReader.cpp +++ b/xs/src/libslic3r/GCodeReader.cpp @@ -6,15 +6,13 @@ namespace Slic3r { -void -GCodeReader::apply_config(const PrintConfigBase &config) +void GCodeReader::apply_config(const PrintConfigBase &config) { - this->_config.apply(config, true); - this->_extrusion_axis = this->_config.get_extrusion_axis()[0]; + m_config.apply(config, true); + m_extrusion_axis = m_config.get_extrusion_axis()[0]; } -void -GCodeReader::parse(const std::string &gcode, callback_t callback) +void GCodeReader::parse(const std::string &gcode, callback_t callback) { std::istringstream ss(gcode); std::string line; @@ -22,8 +20,7 @@ GCodeReader::parse(const std::string &gcode, callback_t callback) this->parse_line(line, callback); } -void -GCodeReader::parse_line(std::string line, callback_t callback) +void GCodeReader::parse_line(std::string line, callback_t callback) { GCodeLine gline(this); gline.raw = line; @@ -55,15 +52,15 @@ GCodeReader::parse_line(std::string line, callback_t callback) } // convert extrusion axis - if (this->_extrusion_axis != 'E') { - const auto it = gline.args.find(this->_extrusion_axis); + if (m_extrusion_axis != 'E') { + const auto it = gline.args.find(m_extrusion_axis); if (it != gline.args.end()) { std::swap(gline.args['E'], it->second); gline.args.erase(it); } } - if (gline.has('E') && this->_config.use_relative_e_distances) + if (gline.has('E') && m_config.use_relative_e_distances) this->E = 0; if (callback) callback(*this, gline); @@ -78,8 +75,7 @@ GCodeReader::parse_line(std::string line, callback_t callback) } } -void -GCodeReader::parse_file(const std::string &file, callback_t callback) +void GCodeReader::parse_file(const std::string &file, callback_t callback) { std::ifstream f(file); std::string line; @@ -87,8 +83,7 @@ GCodeReader::parse_file(const std::string &file, callback_t callback) this->parse_line(line, callback); } -void -GCodeReader::GCodeLine::set(char arg, std::string value) +void GCodeReader::GCodeLine::set(char arg, std::string value) { const std::string space(" "); if (this->has(arg)) { diff --git a/xs/src/libslic3r/GCodeReader.hpp b/xs/src/libslic3r/GCodeReader.hpp index 03380f32e..3ff5f56bb 100644 --- a/xs/src/libslic3r/GCodeReader.hpp +++ b/xs/src/libslic3r/GCodeReader.hpp @@ -22,7 +22,7 @@ public: GCodeLine(GCodeReader* _reader) : reader(_reader) {}; - bool has(char arg) const { return this->args.count(arg) > 0; }; + bool has(char arg) const { return this->args.count(arg) > 0; }; float get_float(char arg) const { return float(atof(this->args.at(arg).c_str())); }; float new_X() const { return this->has('X') ? float(atof(this->args.at('X').c_str())) : this->reader->X; }; float new_Y() const { return this->has('Y') ? float(atof(this->args.at('Y').c_str())) : this->reader->Y; }; @@ -38,9 +38,9 @@ public: float y = this->dist_Y(); return sqrt(x*x + y*y); }; - bool extruding() const { return this->cmd == "G1" && this->dist_E() > 0; }; + bool extruding() const { return this->cmd == "G1" && this->dist_E() > 0; }; bool retracting() const { return this->cmd == "G1" && this->dist_E() < 0; }; - bool travel() const { return this->cmd == "G1" && !this->has('E'); }; + bool travel() const { return this->cmd == "G1" && ! this->has('E'); }; void set(char arg, std::string value); }; typedef std::function callback_t; @@ -49,15 +49,15 @@ public: bool verbose; callback_t callback; - GCodeReader() : X(0), Y(0), Z(0), E(0), F(0), verbose(false), _extrusion_axis('E') {}; + GCodeReader() : X(0), Y(0), Z(0), E(0), F(0), verbose(false), m_extrusion_axis('E') {}; void apply_config(const PrintConfigBase &config); void parse(const std::string &gcode, callback_t callback); void parse_line(std::string line, callback_t callback); void parse_file(const std::string &file, callback_t callback); private: - GCodeConfig _config; - char _extrusion_axis; + GCodeConfig m_config; + char m_extrusion_axis; }; } /* namespace Slic3r */ diff --git a/xs/src/libslic3r/GCodeWriter.cpp b/xs/src/libslic3r/GCodeWriter.cpp index 2de35b664..fa8fbd208 100644 --- a/xs/src/libslic3r/GCodeWriter.cpp +++ b/xs/src/libslic3r/GCodeWriter.cpp @@ -181,11 +181,14 @@ std::string GCodeWriter::set_acceleration(unsigned int acceleration) std::ostringstream gcode; if (FLAVOR_IS(gcfRepetier)) { + // M201: Set max printing acceleration gcode << "M201 X" << acceleration << " Y" << acceleration; if (this->config.gcode_comments) gcode << " ; adjust acceleration"; gcode << "\n"; + // M202: Set max travel acceleration gcode << "M202 X" << acceleration << " Y" << acceleration; } else { + // M204: Set default acceleration gcode << "M204 S" << acceleration; } if (this->config.gcode_comments) gcode << " ; adjust acceleration"; @@ -233,6 +236,12 @@ std::string GCodeWriter::update_progress(unsigned int num, unsigned int tot, boo return gcode.str(); } +std::string GCodeWriter::toolchange_prefix() const +{ + return FLAVOR_IS(gcfMakerWare) ? "M135 T" : + FLAVOR_IS(gcfSailfish) ? "M108 T" : "T"; +} + std::string GCodeWriter::toolchange(unsigned int extruder_id) { // set the new extruder @@ -248,17 +257,10 @@ std::string GCodeWriter::toolchange(unsigned int extruder_id) // if we are running a single-extruder setup, just set the extruder and return nothing std::ostringstream gcode; if (this->multiple_extruders) { - if (FLAVOR_IS(gcfMakerWare)) { - gcode << "M135 T"; - } else if (FLAVOR_IS(gcfSailfish)) { - gcode << "M108 T"; - } else { - gcode << "T"; - } - gcode << extruder_id; - if (this->config.gcode_comments) gcode << " ; change extruder"; + gcode << this->toolchange_prefix() << extruder_id; + if (this->config.gcode_comments) + gcode << " ; change extruder"; gcode << "\n"; - gcode << this->reset_e(true); } return gcode.str(); diff --git a/xs/src/libslic3r/GCodeWriter.hpp b/xs/src/libslic3r/GCodeWriter.hpp index 37f15cc53..f31d20245 100644 --- a/xs/src/libslic3r/GCodeWriter.hpp +++ b/xs/src/libslic3r/GCodeWriter.hpp @@ -54,6 +54,9 @@ public: { return m_extruder == nullptr || m_extruder->id() != extruder_id; } std::string set_extruder(unsigned int extruder_id) { return this->need_toolchange(extruder_id) ? this->toolchange(extruder_id) : ""; } + // Prefix of the toolchange G-code line, to be used by the CoolingBuffer to separate sections of the G-code + // printed with the same extruder. + std::string toolchange_prefix() const; std::string toolchange(unsigned int extruder_id); std::string set_speed(double F, const std::string &comment = std::string(), const std::string &cooling_marker = std::string()) const; std::string travel_to_xy(const Pointf &point, const std::string &comment = std::string());