diff options
author | bubnikv <bubnikv@gmail.com> | 2018-09-19 12:02:24 +0300 |
---|---|---|
committer | bubnikv <bubnikv@gmail.com> | 2018-09-19 12:02:24 +0300 |
commit | 0558b53493a77bae44831cf87bb0f59359828ef5 (patch) | |
tree | c3e8dbdf7d91a051c12d9ebbf7606d41047fea96 /src/libslic3r/GCode | |
parent | 3ddaccb6410478ad02d8c0e02d6d8e6eb1785b9f (diff) |
WIP: Moved sources int src/, separated most of the source code from Perl.mass_rename
The XS was left only for the unit / integration tests, and it links
libslic3r only. No wxWidgets are allowed to be used from Perl starting
from now.
Diffstat (limited to 'src/libslic3r/GCode')
-rw-r--r-- | src/libslic3r/GCode/Analyzer.cpp | 842 | ||||
-rw-r--r-- | src/libslic3r/GCode/Analyzer.hpp | 233 | ||||
-rw-r--r-- | src/libslic3r/GCode/CoolingBuffer.cpp | 749 | ||||
-rw-r--r-- | src/libslic3r/GCode/CoolingBuffer.hpp | 53 | ||||
-rw-r--r-- | src/libslic3r/GCode/PostProcessor.cpp | 60 | ||||
-rw-r--r-- | src/libslic3r/GCode/PostProcessor.hpp | 15 | ||||
-rw-r--r-- | src/libslic3r/GCode/PressureEqualizer.cpp | 621 | ||||
-rw-r--r-- | src/libslic3r/GCode/PressureEqualizer.hpp | 212 | ||||
-rw-r--r-- | src/libslic3r/GCode/PreviewData.cpp | 456 | ||||
-rw-r--r-- | src/libslic3r/GCode/PreviewData.hpp | 208 | ||||
-rw-r--r-- | src/libslic3r/GCode/PrintExtents.cpp | 186 | ||||
-rw-r--r-- | src/libslic3r/GCode/PrintExtents.hpp | 30 | ||||
-rw-r--r-- | src/libslic3r/GCode/SpiralVase.cpp | 87 | ||||
-rw-r--r-- | src/libslic3r/GCode/SpiralVase.hpp | 28 | ||||
-rw-r--r-- | src/libslic3r/GCode/ToolOrdering.cpp | 631 | ||||
-rw-r--r-- | src/libslic3r/GCode/ToolOrdering.hpp | 162 | ||||
-rw-r--r-- | src/libslic3r/GCode/WipeTower.hpp | 167 | ||||
-rw-r--r-- | src/libslic3r/GCode/WipeTowerPrusaMM.cpp | 1258 | ||||
-rw-r--r-- | src/libslic3r/GCode/WipeTowerPrusaMM.hpp | 374 |
19 files changed, 6372 insertions, 0 deletions
diff --git a/src/libslic3r/GCode/Analyzer.cpp b/src/libslic3r/GCode/Analyzer.cpp new file mode 100644 index 000000000..51d5b1a06 --- /dev/null +++ b/src/libslic3r/GCode/Analyzer.cpp @@ -0,0 +1,842 @@ +#include <memory.h> +#include <string.h> +#include <float.h> + +#include "../libslic3r.h" +#include "../PrintConfig.hpp" +#include "Print.hpp" + +#include "Analyzer.hpp" +#include "PreviewData.hpp" + +static const std::string AXIS_STR = "XYZE"; +static const float MMMIN_TO_MMSEC = 1.0f / 60.0f; +static const float INCHES_TO_MM = 25.4f; +static const float DEFAULT_FEEDRATE = 0.0f; +static const unsigned int DEFAULT_EXTRUDER_ID = 0; +static const Slic3r::Vec3d DEFAULT_START_POSITION = Slic3r::Vec3d(0.0f, 0.0f, 0.0f); +static const float DEFAULT_START_EXTRUSION = 0.0f; + +namespace Slic3r { + +const std::string GCodeAnalyzer::Extrusion_Role_Tag = "_ANALYZER_EXTR_ROLE:"; +const std::string GCodeAnalyzer::Mm3_Per_Mm_Tag = "_ANALYZER_MM3_PER_MM:"; +const std::string GCodeAnalyzer::Width_Tag = "_ANALYZER_WIDTH:"; +const std::string GCodeAnalyzer::Height_Tag = "_ANALYZER_HEIGHT:"; + +const double GCodeAnalyzer::Default_mm3_per_mm = 0.0; +const float GCodeAnalyzer::Default_Width = 0.0f; +const float GCodeAnalyzer::Default_Height = 0.0f; + +GCodeAnalyzer::Metadata::Metadata() + : extrusion_role(erNone) + , extruder_id(DEFAULT_EXTRUDER_ID) + , mm3_per_mm(GCodeAnalyzer::Default_mm3_per_mm) + , width(GCodeAnalyzer::Default_Width) + , height(GCodeAnalyzer::Default_Height) + , feedrate(DEFAULT_FEEDRATE) +{ +} + +GCodeAnalyzer::Metadata::Metadata(ExtrusionRole extrusion_role, unsigned int extruder_id, double mm3_per_mm, float width, float height, float feedrate) + : extrusion_role(extrusion_role) + , extruder_id(extruder_id) + , mm3_per_mm(mm3_per_mm) + , width(width) + , height(height) + , feedrate(feedrate) +{ +} + +bool GCodeAnalyzer::Metadata::operator != (const GCodeAnalyzer::Metadata& other) const +{ + if (extrusion_role != other.extrusion_role) + return true; + + if (extruder_id != other.extruder_id) + return true; + + if (mm3_per_mm != other.mm3_per_mm) + return true; + + if (width != other.width) + return true; + + if (height != other.height) + return true; + + if (feedrate != other.feedrate) + return true; + + return false; +} + +GCodeAnalyzer::GCodeMove::GCodeMove(GCodeMove::EType type, ExtrusionRole extrusion_role, unsigned int extruder_id, double mm3_per_mm, float width, float height, float feedrate, const Vec3d& start_position, const Vec3d& end_position, float delta_extruder) + : type(type) + , data(extrusion_role, extruder_id, mm3_per_mm, width, height, feedrate) + , start_position(start_position) + , end_position(end_position) + , delta_extruder(delta_extruder) +{ +} + +GCodeAnalyzer::GCodeMove::GCodeMove(GCodeMove::EType type, const GCodeAnalyzer::Metadata& data, const Vec3d& start_position, const Vec3d& end_position, float delta_extruder) + : type(type) + , data(data) + , start_position(start_position) + , end_position(end_position) + , delta_extruder(delta_extruder) +{ +} + +GCodeAnalyzer::GCodeAnalyzer() +{ + reset(); +} + +void GCodeAnalyzer::reset() +{ + _set_units(Millimeters); + _set_global_positioning_type(Absolute); + _set_e_local_positioning_type(Absolute); + _set_extrusion_role(erNone); + _set_extruder_id(DEFAULT_EXTRUDER_ID); + _set_mm3_per_mm(Default_mm3_per_mm); + _set_width(Default_Width); + _set_height(Default_Height); + _set_feedrate(DEFAULT_FEEDRATE); + _set_start_position(DEFAULT_START_POSITION); + _set_start_extrusion(DEFAULT_START_EXTRUSION); + _reset_axes_position(); + + m_moves_map.clear(); +} + +const std::string& GCodeAnalyzer::process_gcode(const std::string& gcode) +{ + m_process_output = ""; + + m_parser.parse_buffer(gcode, + [this](GCodeReader& reader, const GCodeReader::GCodeLine& line) + { this->_process_gcode_line(reader, line); }); + + return m_process_output; +} + +void GCodeAnalyzer::calc_gcode_preview_data(GCodePreviewData& preview_data) +{ + // resets preview data + preview_data.reset(); + + // calculates extrusion layers + _calc_gcode_preview_extrusion_layers(preview_data); + + // calculates travel + _calc_gcode_preview_travel(preview_data); + + // calculates retractions + _calc_gcode_preview_retractions(preview_data); + + // calculates unretractions + _calc_gcode_preview_unretractions(preview_data); +} + +bool GCodeAnalyzer::is_valid_extrusion_role(ExtrusionRole role) +{ + return ((erPerimeter <= role) && (role < erMixed)); +} + +void GCodeAnalyzer::_process_gcode_line(GCodeReader&, const GCodeReader::GCodeLine& line) +{ + // processes 'special' comments contained in line + if (_process_tags(line)) + { +#if 0 + // DEBUG ONLY: puts the line back into the gcode + m_process_output += line.raw() + "\n"; +#endif + return; + } + + // sets new start position/extrusion + _set_start_position(_get_end_position()); + _set_start_extrusion(_get_axis_position(E)); + + // processes 'normal' gcode lines + std::string cmd = line.cmd(); + if (cmd.length() > 1) + { + switch (::toupper(cmd[0])) + { + case 'G': + { + switch (::atoi(&cmd[1])) + { + case 1: // Move + { + _processG1(line); + break; + } + case 10: // Retract + { + _processG10(line); + break; + } + case 11: // Unretract + { + _processG11(line); + break; + } + case 22: // Firmware controlled Retract + { + _processG22(line); + break; + } + case 23: // Firmware controlled Unretract + { + _processG23(line); + break; + } + case 90: // Set to Absolute Positioning + { + _processG90(line); + break; + } + case 91: // Set to Relative Positioning + { + _processG91(line); + break; + } + case 92: // Set Position + { + _processG92(line); + break; + } + } + + break; + } + case 'M': + { + switch (::atoi(&cmd[1])) + { + case 82: // Set extruder to absolute mode + { + _processM82(line); + break; + } + case 83: // Set extruder to relative mode + { + _processM83(line); + break; + } + } + + break; + } + case 'T': // Select Tools + { + _processT(line); + break; + } + } + } + + // puts the line back into the gcode + m_process_output += line.raw() + "\n"; +} + +// Returns the new absolute position on the given axis in dependence of the given parameters +float axis_absolute_position_from_G1_line(GCodeAnalyzer::EAxis axis, const GCodeReader::GCodeLine& lineG1, GCodeAnalyzer::EUnits units, bool is_relative, float current_absolute_position) +{ + float lengthsScaleFactor = (units == GCodeAnalyzer::Inches) ? INCHES_TO_MM : 1.0f; + if (lineG1.has(Slic3r::Axis(axis))) + { + float ret = lineG1.value(Slic3r::Axis(axis)) * lengthsScaleFactor; + return is_relative ? current_absolute_position + ret : ret; + } + else + return current_absolute_position; +} + +void GCodeAnalyzer::_processG1(const GCodeReader::GCodeLine& line) +{ + // updates axes positions from line + EUnits units = _get_units(); + float new_pos[Num_Axis]; + for (unsigned char a = X; a < Num_Axis; ++a) + { + bool is_relative = (_get_global_positioning_type() == Relative); + if (a == E) + is_relative |= (_get_e_local_positioning_type() == Relative); + + new_pos[a] = axis_absolute_position_from_G1_line((EAxis)a, line, units, is_relative, _get_axis_position((EAxis)a)); + } + + // updates feedrate from line, if present + if (line.has_f()) + _set_feedrate(line.f() * MMMIN_TO_MMSEC); + + // calculates movement deltas + float delta_pos[Num_Axis]; + for (unsigned char a = X; a < Num_Axis; ++a) + { + delta_pos[a] = new_pos[a] - _get_axis_position((EAxis)a); + } + + // Detects move type + GCodeMove::EType type = GCodeMove::Noop; + + if (delta_pos[E] < 0.0f) + { + if ((delta_pos[X] != 0.0f) || (delta_pos[Y] != 0.0f) || (delta_pos[Z] != 0.0f)) + type = GCodeMove::Move; + else + type = GCodeMove::Retract; + } + else if (delta_pos[E] > 0.0f) + { + if ((delta_pos[X] == 0.0f) && (delta_pos[Y] == 0.0f) && (delta_pos[Z] == 0.0f)) + type = GCodeMove::Unretract; + else if ((delta_pos[X] != 0.0f) || (delta_pos[Y] != 0.0f)) + type = GCodeMove::Extrude; + } + else if ((delta_pos[X] != 0.0f) || (delta_pos[Y] != 0.0f) || (delta_pos[Z] != 0.0f)) + type = GCodeMove::Move; + + ExtrusionRole role = _get_extrusion_role(); + if ((type == GCodeMove::Extrude) && ((_get_width() == 0.0f) || (_get_height() == 0.0f) || !is_valid_extrusion_role(role))) + type = GCodeMove::Move; + + // updates axis positions + for (unsigned char a = X; a < Num_Axis; ++a) + { + _set_axis_position((EAxis)a, new_pos[a]); + } + + // stores the move + if (type != GCodeMove::Noop) + _store_move(type); +} + +void GCodeAnalyzer::_processG10(const GCodeReader::GCodeLine& line) +{ + // stores retract move + _store_move(GCodeMove::Retract); +} + +void GCodeAnalyzer::_processG11(const GCodeReader::GCodeLine& line) +{ + // stores unretract move + _store_move(GCodeMove::Unretract); +} + +void GCodeAnalyzer::_processG22(const GCodeReader::GCodeLine& line) +{ + // stores retract move + _store_move(GCodeMove::Retract); +} + +void GCodeAnalyzer::_processG23(const GCodeReader::GCodeLine& line) +{ + // stores unretract move + _store_move(GCodeMove::Unretract); +} + +void GCodeAnalyzer::_processG90(const GCodeReader::GCodeLine& line) +{ + _set_global_positioning_type(Absolute); +} + +void GCodeAnalyzer::_processG91(const GCodeReader::GCodeLine& line) +{ + _set_global_positioning_type(Relative); +} + +void GCodeAnalyzer::_processG92(const GCodeReader::GCodeLine& line) +{ + float lengthsScaleFactor = (_get_units() == Inches) ? INCHES_TO_MM : 1.0f; + bool anyFound = false; + + if (line.has_x()) + { + _set_axis_position(X, line.x() * lengthsScaleFactor); + anyFound = true; + } + + if (line.has_y()) + { + _set_axis_position(Y, line.y() * lengthsScaleFactor); + anyFound = true; + } + + if (line.has_z()) + { + _set_axis_position(Z, line.z() * lengthsScaleFactor); + anyFound = true; + } + + if (line.has_e()) + { + _set_axis_position(E, line.e() * lengthsScaleFactor); + anyFound = true; + } + + if (!anyFound) + { + for (unsigned char a = X; a < Num_Axis; ++a) + { + _set_axis_position((EAxis)a, 0.0f); + } + } +} + +void GCodeAnalyzer::_processM82(const GCodeReader::GCodeLine& line) +{ + _set_e_local_positioning_type(Absolute); +} + +void GCodeAnalyzer::_processM83(const GCodeReader::GCodeLine& line) +{ + _set_e_local_positioning_type(Relative); +} + +void GCodeAnalyzer::_processT(const GCodeReader::GCodeLine& line) +{ + std::string cmd = line.cmd(); + if (cmd.length() > 1) + { + unsigned int id = (unsigned int)::strtol(cmd.substr(1).c_str(), nullptr, 10); + if (_get_extruder_id() != id) + { + _set_extruder_id(id); + + // stores tool change move + _store_move(GCodeMove::Tool_change); + } + } +} + +bool GCodeAnalyzer::_process_tags(const GCodeReader::GCodeLine& line) +{ + std::string comment = line.comment(); + + // extrusion role tag + size_t pos = comment.find(Extrusion_Role_Tag); + if (pos != comment.npos) + { + _process_extrusion_role_tag(comment, pos); + return true; + } + + // mm3 per mm tag + pos = comment.find(Mm3_Per_Mm_Tag); + if (pos != comment.npos) + { + _process_mm3_per_mm_tag(comment, pos); + return true; + } + + // width tag + pos = comment.find(Width_Tag); + if (pos != comment.npos) + { + _process_width_tag(comment, pos); + return true; + } + + // height tag + pos = comment.find(Height_Tag); + if (pos != comment.npos) + { + _process_height_tag(comment, pos); + return true; + } + + return false; +} + +void GCodeAnalyzer::_process_extrusion_role_tag(const std::string& comment, size_t pos) +{ + int role = (int)::strtol(comment.substr(pos + Extrusion_Role_Tag.length()).c_str(), nullptr, 10); + if (_is_valid_extrusion_role(role)) + _set_extrusion_role((ExtrusionRole)role); + else + { + // todo: show some error ? + } +} + +void GCodeAnalyzer::_process_mm3_per_mm_tag(const std::string& comment, size_t pos) +{ + _set_mm3_per_mm(::strtod(comment.substr(pos + Mm3_Per_Mm_Tag.length()).c_str(), nullptr)); +} + +void GCodeAnalyzer::_process_width_tag(const std::string& comment, size_t pos) +{ + _set_width((float)::strtod(comment.substr(pos + Width_Tag.length()).c_str(), nullptr)); +} + +void GCodeAnalyzer::_process_height_tag(const std::string& comment, size_t pos) +{ + _set_height((float)::strtod(comment.substr(pos + Height_Tag.length()).c_str(), nullptr)); +} + +void GCodeAnalyzer::_set_units(GCodeAnalyzer::EUnits units) +{ + m_state.units = units; +} + +GCodeAnalyzer::EUnits GCodeAnalyzer::_get_units() const +{ + return m_state.units; +} + +void GCodeAnalyzer::_set_global_positioning_type(GCodeAnalyzer::EPositioningType type) +{ + m_state.global_positioning_type = type; +} + +GCodeAnalyzer::EPositioningType GCodeAnalyzer::_get_global_positioning_type() const +{ + return m_state.global_positioning_type; +} + +void GCodeAnalyzer::_set_e_local_positioning_type(GCodeAnalyzer::EPositioningType type) +{ + m_state.e_local_positioning_type = type; +} + +GCodeAnalyzer::EPositioningType GCodeAnalyzer::_get_e_local_positioning_type() const +{ + return m_state.e_local_positioning_type; +} + +void GCodeAnalyzer::_set_extrusion_role(ExtrusionRole extrusion_role) +{ + m_state.data.extrusion_role = extrusion_role; +} + +ExtrusionRole GCodeAnalyzer::_get_extrusion_role() const +{ + return m_state.data.extrusion_role; +} + +void GCodeAnalyzer::_set_extruder_id(unsigned int id) +{ + m_state.data.extruder_id = id; +} + +unsigned int GCodeAnalyzer::_get_extruder_id() const +{ + return m_state.data.extruder_id; +} + +void GCodeAnalyzer::_set_mm3_per_mm(double value) +{ + m_state.data.mm3_per_mm = value; +} + +double GCodeAnalyzer::_get_mm3_per_mm() const +{ + return m_state.data.mm3_per_mm; +} + +void GCodeAnalyzer::_set_width(float width) +{ + m_state.data.width = width; +} + +float GCodeAnalyzer::_get_width() const +{ + return m_state.data.width; +} + +void GCodeAnalyzer::_set_height(float height) +{ + m_state.data.height = height; +} + +float GCodeAnalyzer::_get_height() const +{ + return m_state.data.height; +} + +void GCodeAnalyzer::_set_feedrate(float feedrate_mm_sec) +{ + m_state.data.feedrate = feedrate_mm_sec; +} + +float GCodeAnalyzer::_get_feedrate() const +{ + return m_state.data.feedrate; +} + +void GCodeAnalyzer::_set_axis_position(EAxis axis, float position) +{ + m_state.position[axis] = position; +} + +float GCodeAnalyzer::_get_axis_position(EAxis axis) const +{ + return m_state.position[axis]; +} + +void GCodeAnalyzer::_reset_axes_position() +{ + ::memset((void*)m_state.position, 0, Num_Axis * sizeof(float)); +} + +void GCodeAnalyzer::_set_start_position(const Vec3d& position) +{ + m_state.start_position = position; +} + +const Vec3d& GCodeAnalyzer::_get_start_position() const +{ + return m_state.start_position; +} + +void GCodeAnalyzer::_set_start_extrusion(float extrusion) +{ + m_state.start_extrusion = extrusion; +} + +float GCodeAnalyzer::_get_start_extrusion() const +{ + return m_state.start_extrusion; +} + +float GCodeAnalyzer::_get_delta_extrusion() const +{ + return _get_axis_position(E) - m_state.start_extrusion; +} + +Vec3d GCodeAnalyzer::_get_end_position() const +{ + return Vec3d(m_state.position[X], m_state.position[Y], m_state.position[Z]); +} + +void GCodeAnalyzer::_store_move(GCodeAnalyzer::GCodeMove::EType type) +{ + // if type non mapped yet, map it + TypeToMovesMap::iterator it = m_moves_map.find(type); + if (it == m_moves_map.end()) + it = m_moves_map.insert(TypeToMovesMap::value_type(type, GCodeMovesList())).first; + + // store move + it->second.emplace_back(type, _get_extrusion_role(), _get_extruder_id(), _get_mm3_per_mm(), _get_width(), _get_height(), _get_feedrate(), _get_start_position(), _get_end_position(), _get_delta_extrusion()); +} + +bool GCodeAnalyzer::_is_valid_extrusion_role(int value) const +{ + return ((int)erNone <= value) && (value <= (int)erMixed); +} + +void GCodeAnalyzer::_calc_gcode_preview_extrusion_layers(GCodePreviewData& preview_data) +{ + struct Helper + { + static GCodePreviewData::Extrusion::Layer& get_layer_at_z(GCodePreviewData::Extrusion::LayersList& layers, float z) + { + for (GCodePreviewData::Extrusion::Layer& layer : layers) + { + // if layer found, return it + if (layer.z == z) + return layer; + } + + // if layer not found, create and return it + layers.emplace_back(z, ExtrusionPaths()); + return layers.back(); + } + + static void store_polyline(const Polyline& polyline, const Metadata& data, float z, GCodePreviewData& preview_data) + { + // if the polyline is valid, create the extrusion path from it and store it + if (polyline.is_valid()) + { + ExtrusionPath path(data.extrusion_role, data.mm3_per_mm, data.width, data.height); + path.polyline = polyline; + path.feedrate = data.feedrate; + path.extruder_id = data.extruder_id; + + get_layer_at_z(preview_data.extrusion.layers, z).paths.push_back(path); + } + } + }; + + TypeToMovesMap::iterator extrude_moves = m_moves_map.find(GCodeMove::Extrude); + if (extrude_moves == m_moves_map.end()) + return; + + Metadata data; + float z = FLT_MAX; + Polyline polyline; + Vec3d position(FLT_MAX, FLT_MAX, FLT_MAX); + float volumetric_rate = FLT_MAX; + GCodePreviewData::Range height_range; + GCodePreviewData::Range width_range; + GCodePreviewData::Range feedrate_range; + GCodePreviewData::Range volumetric_rate_range; + + // constructs the polylines while traversing the moves + for (const GCodeMove& move : extrude_moves->second) + { + if ((data != move.data) || (z != move.start_position.z()) || (position != move.start_position) || (volumetric_rate != move.data.feedrate * (float)move.data.mm3_per_mm)) + { + // store current polyline + polyline.remove_duplicate_points(); + Helper::store_polyline(polyline, data, z, preview_data); + + // reset current polyline + polyline = Polyline(); + + // add both vertices of the move + polyline.append(Point(scale_(move.start_position.x()), scale_(move.start_position.y()))); + polyline.append(Point(scale_(move.end_position.x()), scale_(move.end_position.y()))); + + // update current values + data = move.data; + z = move.start_position.z(); + volumetric_rate = move.data.feedrate * (float)move.data.mm3_per_mm; + height_range.update_from(move.data.height); + width_range.update_from(move.data.width); + feedrate_range.update_from(move.data.feedrate); + volumetric_rate_range.update_from(volumetric_rate); + } + else + // append end vertex of the move to current polyline + polyline.append(Point(scale_(move.end_position.x()), scale_(move.end_position.y()))); + + // update current values + position = move.end_position; + } + + // store last polyline + polyline.remove_duplicate_points(); + Helper::store_polyline(polyline, data, z, preview_data); + + // updates preview ranges data + preview_data.ranges.height.update_from(height_range); + preview_data.ranges.width.update_from(width_range); + preview_data.ranges.feedrate.update_from(feedrate_range); + preview_data.ranges.volumetric_rate.update_from(volumetric_rate_range); +} + +void GCodeAnalyzer::_calc_gcode_preview_travel(GCodePreviewData& preview_data) +{ + struct Helper + { + static void store_polyline(const Polyline3& polyline, GCodePreviewData::Travel::EType type, GCodePreviewData::Travel::Polyline::EDirection direction, + float feedrate, unsigned int extruder_id, GCodePreviewData& preview_data) + { + // if the polyline is valid, store it + if (polyline.is_valid()) + preview_data.travel.polylines.emplace_back(type, direction, feedrate, extruder_id, polyline); + } + }; + + TypeToMovesMap::iterator travel_moves = m_moves_map.find(GCodeMove::Move); + if (travel_moves == m_moves_map.end()) + return; + + Polyline3 polyline; + Vec3d position(FLT_MAX, FLT_MAX, FLT_MAX); + GCodePreviewData::Travel::EType type = GCodePreviewData::Travel::Num_Types; + GCodePreviewData::Travel::Polyline::EDirection direction = GCodePreviewData::Travel::Polyline::Num_Directions; + float feedrate = FLT_MAX; + unsigned int extruder_id = -1; + + GCodePreviewData::Range height_range; + GCodePreviewData::Range width_range; + GCodePreviewData::Range feedrate_range; + + // constructs the polylines while traversing the moves + for (const GCodeMove& move : travel_moves->second) + { + GCodePreviewData::Travel::EType move_type = (move.delta_extruder < 0.0f) ? GCodePreviewData::Travel::Retract : ((move.delta_extruder > 0.0f) ? GCodePreviewData::Travel::Extrude : GCodePreviewData::Travel::Move); + GCodePreviewData::Travel::Polyline::EDirection move_direction = ((move.start_position.x() != move.end_position.x()) || (move.start_position.y() != move.end_position.y())) ? GCodePreviewData::Travel::Polyline::Generic : GCodePreviewData::Travel::Polyline::Vertical; + + if ((type != move_type) || (direction != move_direction) || (feedrate != move.data.feedrate) || (position != move.start_position) || (extruder_id != move.data.extruder_id)) + { + // store current polyline + polyline.remove_duplicate_points(); + Helper::store_polyline(polyline, type, direction, feedrate, extruder_id, preview_data); + + // reset current polyline + polyline = Polyline3(); + + // add both vertices of the move + polyline.append(Vec3crd(scale_(move.start_position.x()), scale_(move.start_position.y()), scale_(move.start_position.z()))); + polyline.append(Vec3crd(scale_(move.end_position.x()), scale_(move.end_position.y()), scale_(move.end_position.z()))); + } + else + // append end vertex of the move to current polyline + polyline.append(Vec3crd(scale_(move.end_position.x()), scale_(move.end_position.y()), scale_(move.end_position.z()))); + + // update current values + position = move.end_position; + type = move_type; + feedrate = move.data.feedrate; + extruder_id = move.data.extruder_id; + height_range.update_from(move.data.height); + width_range.update_from(move.data.width); + feedrate_range.update_from(move.data.feedrate); + } + + // store last polyline + polyline.remove_duplicate_points(); + Helper::store_polyline(polyline, type, direction, feedrate, extruder_id, preview_data); + + // updates preview ranges data + preview_data.ranges.height.update_from(height_range); + preview_data.ranges.width.update_from(width_range); + preview_data.ranges.feedrate.update_from(feedrate_range); +} + +void GCodeAnalyzer::_calc_gcode_preview_retractions(GCodePreviewData& preview_data) +{ + TypeToMovesMap::iterator retraction_moves = m_moves_map.find(GCodeMove::Retract); + if (retraction_moves == m_moves_map.end()) + return; + + for (const GCodeMove& move : retraction_moves->second) + { + // store position + Vec3crd position(scale_(move.start_position.x()), scale_(move.start_position.y()), scale_(move.start_position.z())); + preview_data.retraction.positions.emplace_back(position, move.data.width, move.data.height); + } +} + +void GCodeAnalyzer::_calc_gcode_preview_unretractions(GCodePreviewData& preview_data) +{ + TypeToMovesMap::iterator unretraction_moves = m_moves_map.find(GCodeMove::Unretract); + if (unretraction_moves == m_moves_map.end()) + return; + + for (const GCodeMove& move : unretraction_moves->second) + { + // store position + Vec3crd position(scale_(move.start_position.x()), scale_(move.start_position.y()), scale_(move.start_position.z())); + preview_data.unretraction.positions.emplace_back(position, move.data.width, move.data.height); + } +} + +GCodePreviewData::Color operator + (const GCodePreviewData::Color& c1, const GCodePreviewData::Color& c2) +{ + return GCodePreviewData::Color(clamp(0.0f, 1.0f, c1.rgba[0] + c2.rgba[0]), + clamp(0.0f, 1.0f, c1.rgba[1] + c2.rgba[1]), + clamp(0.0f, 1.0f, c1.rgba[2] + c2.rgba[2]), + clamp(0.0f, 1.0f, c1.rgba[3] + c2.rgba[3])); +} + +GCodePreviewData::Color operator * (float f, const GCodePreviewData::Color& color) +{ + return GCodePreviewData::Color(clamp(0.0f, 1.0f, f * color.rgba[0]), + clamp(0.0f, 1.0f, f * color.rgba[1]), + clamp(0.0f, 1.0f, f * color.rgba[2]), + clamp(0.0f, 1.0f, f * color.rgba[3])); +} + +} // namespace Slic3r diff --git a/src/libslic3r/GCode/Analyzer.hpp b/src/libslic3r/GCode/Analyzer.hpp new file mode 100644 index 000000000..27a49b869 --- /dev/null +++ b/src/libslic3r/GCode/Analyzer.hpp @@ -0,0 +1,233 @@ +#ifndef slic3r_GCode_Analyzer_hpp_ +#define slic3r_GCode_Analyzer_hpp_ + +#include "../libslic3r.h" +#include "../PrintConfig.hpp" +#include "../ExtrusionEntity.hpp" + +#include "Point.hpp" +#include "GCodeReader.hpp" + +namespace Slic3r { + +class GCodePreviewData; + +class GCodeAnalyzer +{ +public: + static const std::string Extrusion_Role_Tag; + static const std::string Mm3_Per_Mm_Tag; + static const std::string Width_Tag; + static const std::string Height_Tag; + + static const double Default_mm3_per_mm; + static const float Default_Width; + static const float Default_Height; + + enum EUnits : unsigned char + { + Millimeters, + Inches + }; + + enum EAxis : unsigned char + { + X, + Y, + Z, + E, + Num_Axis + }; + + enum EPositioningType : unsigned char + { + Absolute, + Relative + }; + + struct Metadata + { + ExtrusionRole extrusion_role; + unsigned int extruder_id; + double mm3_per_mm; + float width; // mm + float height; // mm + float feedrate; // mm/s + + Metadata(); + Metadata(ExtrusionRole extrusion_role, unsigned int extruder_id, double mm3_per_mm, float width, float height, float feedrate); + + bool operator != (const Metadata& other) const; + }; + + struct GCodeMove + { + enum EType : unsigned char + { + Noop, + Retract, + Unretract, + Tool_change, + Move, + Extrude, + Num_Types + }; + + EType type; + Metadata data; + Vec3d start_position; + Vec3d end_position; + float delta_extruder; + + GCodeMove(EType type, ExtrusionRole extrusion_role, unsigned int extruder_id, double mm3_per_mm, float width, float height, float feedrate, const Vec3d& start_position, const Vec3d& end_position, float delta_extruder); + GCodeMove(EType type, const Metadata& data, const Vec3d& start_position, const Vec3d& end_position, float delta_extruder); + }; + + typedef std::vector<GCodeMove> GCodeMovesList; + typedef std::map<GCodeMove::EType, GCodeMovesList> TypeToMovesMap; + +private: + struct State + { + EUnits units; + EPositioningType global_positioning_type; + EPositioningType e_local_positioning_type; + Metadata data; + Vec3d start_position = Vec3d::Zero(); + float start_extrusion; + float position[Num_Axis]; + }; + +private: + State m_state; + GCodeReader m_parser; + TypeToMovesMap m_moves_map; + + // The output of process_layer() + std::string m_process_output; + +public: + GCodeAnalyzer(); + + // Reinitialize the analyzer + void reset(); + + // Adds the gcode contained in the given string to the analysis and returns it after removing the workcodes + const std::string& process_gcode(const std::string& gcode); + + // Calculates all data needed for gcode visualization + void calc_gcode_preview_data(GCodePreviewData& preview_data); + + static bool is_valid_extrusion_role(ExtrusionRole role); + +private: + // Processes the given gcode line + void _process_gcode_line(GCodeReader& reader, const GCodeReader::GCodeLine& line); + + // Move + void _processG1(const GCodeReader::GCodeLine& line); + + // Retract + void _processG10(const GCodeReader::GCodeLine& line); + + // Unretract + void _processG11(const GCodeReader::GCodeLine& line); + + // Firmware controlled Retract + void _processG22(const GCodeReader::GCodeLine& line); + + // Firmware controlled Unretract + void _processG23(const GCodeReader::GCodeLine& line); + + // Set to Absolute Positioning + void _processG90(const GCodeReader::GCodeLine& line); + + // Set to Relative Positioning + void _processG91(const GCodeReader::GCodeLine& line); + + // Set Position + void _processG92(const GCodeReader::GCodeLine& line); + + // Set extruder to absolute mode + void _processM82(const GCodeReader::GCodeLine& line); + + // Set extruder to relative mode + void _processM83(const GCodeReader::GCodeLine& line); + + // Processes T line (Select Tool) + void _processT(const GCodeReader::GCodeLine& line); + + // Processes the tags + // Returns true if any tag has been processed + bool _process_tags(const GCodeReader::GCodeLine& line); + + // Processes extrusion role tag + void _process_extrusion_role_tag(const std::string& comment, size_t pos); + + // Processes mm3_per_mm tag + void _process_mm3_per_mm_tag(const std::string& comment, size_t pos); + + // Processes width tag + void _process_width_tag(const std::string& comment, size_t pos); + + // Processes height tag + void _process_height_tag(const std::string& comment, size_t pos); + + void _set_units(EUnits units); + EUnits _get_units() const; + + void _set_global_positioning_type(EPositioningType type); + EPositioningType _get_global_positioning_type() const; + + void _set_e_local_positioning_type(EPositioningType type); + EPositioningType _get_e_local_positioning_type() const; + + void _set_extrusion_role(ExtrusionRole extrusion_role); + ExtrusionRole _get_extrusion_role() const; + + void _set_extruder_id(unsigned int id); + unsigned int _get_extruder_id() const; + + void _set_mm3_per_mm(double value); + double _get_mm3_per_mm() const; + + void _set_width(float width); + float _get_width() const; + + void _set_height(float height); + float _get_height() const; + + void _set_feedrate(float feedrate_mm_sec); + float _get_feedrate() const; + + void _set_axis_position(EAxis axis, float position); + float _get_axis_position(EAxis axis) const; + + // Sets axes position to zero + void _reset_axes_position(); + + void _set_start_position(const Vec3d& position); + const Vec3d& _get_start_position() const; + + void _set_start_extrusion(float extrusion); + float _get_start_extrusion() const; + float _get_delta_extrusion() const; + + // Returns current xyz position (from m_state.position[]) + Vec3d _get_end_position() const; + + // Adds a new move with the given data + void _store_move(GCodeMove::EType type); + + // Checks if the given int is a valid extrusion role (contained into enum ExtrusionRole) + bool _is_valid_extrusion_role(int value) const; + + void _calc_gcode_preview_extrusion_layers(GCodePreviewData& preview_data); + void _calc_gcode_preview_travel(GCodePreviewData& preview_data); + void _calc_gcode_preview_retractions(GCodePreviewData& preview_data); + void _calc_gcode_preview_unretractions(GCodePreviewData& preview_data); +}; + +} // namespace Slic3r + +#endif /* slic3r_GCode_Analyzer_hpp_ */ diff --git a/src/libslic3r/GCode/CoolingBuffer.cpp b/src/libslic3r/GCode/CoolingBuffer.cpp new file mode 100644 index 000000000..40ccc7b09 --- /dev/null +++ b/src/libslic3r/GCode/CoolingBuffer.cpp @@ -0,0 +1,749 @@ +#include "../GCode.hpp" +#include "CoolingBuffer.hpp" +#include <boost/algorithm/string/predicate.hpp> +#include <boost/algorithm/string/replace.hpp> +#include <iostream> +#include <float.h> + +#if 0 + #define DEBUG + #define _DEBUG + #undef NDEBUG +#endif + +#include <assert.h> + +namespace Slic3r { + +CoolingBuffer::CoolingBuffer(GCode &gcodegen) : m_gcodegen(gcodegen), m_current_extruder(0) +{ + this->reset(); +} + +void CoolingBuffer::reset() +{ + m_current_pos.assign(5, 0.f); + Vec3d pos = m_gcodegen.writer().get_position(); + m_current_pos[0] = float(pos(0)); + m_current_pos[1] = float(pos(1)); + m_current_pos[2] = float(pos(2)); + m_current_pos[4] = float(m_gcodegen.config().travel_speed.value); +} + +struct CoolingLine +{ + 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, + // The line sets a feedrate. + TYPE_HAS_F = 1 << 8, + TYPE_WIPE = 1 << 9, + TYPE_G4 = 1 << 10, + TYPE_G92 = 1 << 11, + }; + + CoolingLine(unsigned int type, size_t line_start, size_t line_end) : + type(type), line_start(line_start), line_end(line_end), + length(0.f), feedrate(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; + } + + bool adjustable() const { + return (this->type & TYPE_ADJUSTABLE) && 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 feedrate, possibly adjusted. + float feedrate; + // 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; +}; + +// Calculate the required per extruder time stretches. +struct PerExtruderAdjustments +{ + // Calculate the total elapsed time per this extruder, adjusted for the slowdown. + float elapsed_time_total() { + float time_total = 0.f; + for (const CoolingLine &line : lines) + time_total += line.time; + return time_total; + } + // Calculate the total elapsed time when slowing down + // to the minimum extrusion feed rate defined for the current material. + float maximum_time_after_slowdown(bool slowdown_external_perimeters) { + float time_total = 0.f; + for (const CoolingLine &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 adjustable part of the total time. + float adjustable_time(bool slowdown_external_perimeters) { + float time_total = 0.f; + for (const CoolingLine &line : lines) + if (line.adjustable(slowdown_external_perimeters)) + 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 CoolingLine &line : lines) + if (! line.adjustable(slowdown_external_perimeters)) + time_total += line.time; + return time_total; + } + // Slow down the adjustable extrusions to the minimum feedrate allowed for the current extruder material. + // Used by both proportional and non-proportional slow down. + float slowdown_to_minimum_feedrate(bool slowdown_external_perimeters) { + float time_total = 0.f; + for (CoolingLine &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; + line.feedrate = line.length / line.time; + } + time_total += line.time; + } + return time_total; + } + // Slow down each adjustable G-code line proportionally by a factor. + // Used by the proportional slow down. + float slow_down_proportional(float factor, bool slowdown_external_perimeters) { + assert(factor >= 1.f); + float time_total = 0.f; + for (CoolingLine &line : lines) { + if (line.adjustable(slowdown_external_perimeters)) { + line.slowdown = true; + line.time = std::min(line.time_max, line.time * factor); + line.feedrate = line.length / line.time; + } + time_total += line.time; + } + return time_total; + } + + // Sort the lines, adjustable first, higher feedrate first. + // Used by non-proportional slow down. + void sort_lines_by_decreasing_feedrate() { + std::sort(lines.begin(), lines.end(), [](const CoolingLine &l1, const CoolingLine &l2) { + bool adj1 = l1.adjustable(); + bool adj2 = l2.adjustable(); + return (adj1 == adj2) ? l1.feedrate > l2.feedrate : adj1; + }); + for (n_lines_adjustable = 0; + n_lines_adjustable < lines.size() && this->lines[n_lines_adjustable].adjustable(); + ++ n_lines_adjustable); + time_non_adjustable = 0.f; + for (size_t i = n_lines_adjustable; i < lines.size(); ++ i) + time_non_adjustable += lines[i].time; + } + + // Calculate the maximum time stretch when slowing down to min_feedrate. + // Slowdown to min_feedrate shall be allowed for this extruder's material. + // Used by non-proportional slow down. + float time_stretch_when_slowing_down_to_feedrate(float min_feedrate) { + float time_stretch = 0.f; + assert(this->min_print_speed < min_feedrate + EPSILON); + for (size_t i = 0; i < n_lines_adjustable; ++ i) { + const CoolingLine &line = lines[i]; + if (line.feedrate > min_feedrate) + time_stretch += line.time * (line.feedrate / min_feedrate - 1.f); + } + return time_stretch; + } + + // Slow down all adjustable lines down to min_feedrate. + // Slowdown to min_feedrate shall be allowed for this extruder's material. + // Used by non-proportional slow down. + void slow_down_to_feedrate(float min_feedrate) { + assert(this->min_print_speed < min_feedrate + EPSILON); + for (size_t i = 0; i < n_lines_adjustable; ++ i) { + CoolingLine &line = lines[i]; + if (line.feedrate > min_feedrate) { + line.time *= std::max(1.f, line.feedrate / min_feedrate); + line.feedrate = min_feedrate; + line.slowdown = true; + } + } + } + + // Extruder, for which the G-code will be adjusted. + unsigned int extruder_id = 0; + // Is the cooling slow down logic enabled for this extruder's material? + bool cooling_slow_down_enabled = false; + // Slow down the print down to min_print_speed if the total layer time is below slowdown_below_layer_time. + float slowdown_below_layer_time = 0.f; + // Minimum print speed allowed for this extruder. + float min_print_speed = 0.f; + + // Parsed lines. + std::vector<CoolingLine> lines; + // The following two values are set by sort_lines_by_decreasing_feedrate(): + // Number of adjustable lines, at the start of lines. + size_t n_lines_adjustable = 0; + // Non-adjustable time of lines starting with n_lines_adjustable. + float time_non_adjustable = 0; + // Current total time for this extruder. + float time_total = 0; + // Maximum time for this extruder, when the maximum slow down is applied. + float time_maximum = 0; + + // Temporaries for processing the slow down. Both thresholds go from 0 to n_lines_adjustable. + size_t idx_line_begin = 0; + size_t idx_line_end = 0; +}; + +std::string CoolingBuffer::process_layer(const std::string &gcode, size_t layer_id) +{ + std::vector<PerExtruderAdjustments> per_extruder_adjustments = this->parse_layer_gcode(gcode, m_current_pos); + float layer_time_stretched = this->calculate_layer_slowdown(per_extruder_adjustments); + return this->apply_layer_cooldown(gcode, layer_id, layer_time_stretched, per_extruder_adjustments); +} + +// Parse the layer G-code for the moves, which could be adjusted. +// Return the list of parsed lines, bucketed by an extruder. +std::vector<PerExtruderAdjustments> CoolingBuffer::parse_layer_gcode(const std::string &gcode, std::vector<float> ¤t_pos) const +{ + const FullPrintConfig &config = m_gcodegen.config(); + const std::vector<Extruder> &extruders = m_gcodegen.writer().extruders(); + unsigned int num_extruders = 0; + for (const Extruder &ex : extruders) + num_extruders = std::max(ex.id() + 1, num_extruders); + + std::vector<PerExtruderAdjustments> per_extruder_adjustments(extruders.size()); + std::vector<size_t> map_extruder_to_per_extruder_adjustment(num_extruders, 0); + for (size_t i = 0; i < extruders.size(); ++ i) { + PerExtruderAdjustments &adj = per_extruder_adjustments[i]; + unsigned int extruder_id = extruders[i].id(); + adj.extruder_id = extruder_id; + adj.cooling_slow_down_enabled = config.cooling.get_at(extruder_id); + adj.slowdown_below_layer_time = config.slowdown_below_layer_time.get_at(extruder_id); + adj.min_print_speed = config.min_print_speed.get_at(extruder_id); + map_extruder_to_per_extruder_adjustment[extruder_id] = i; + } + + const std::string toolchange_prefix = m_gcodegen.writer().toolchange_prefix(); + unsigned int current_extruder = m_current_extruder; + PerExtruderAdjustments *adjustment = &per_extruder_adjustments[map_extruder_to_per_extruder_adjustment[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 CoolingLine 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; + // sline will not contain the trailing '\n'. + std::string sline(line_start, line_end); + // CoolingLine will contain the trailing '\n'. + if (*line_end == '\n') + ++ line_end; + CoolingLine line(0, line_start - gcode.c_str(), line_end - gcode.c_str()); + if (boost::starts_with(sline, "G0 ")) + line.type = CoolingLine::TYPE_G0; + else if (boost::starts_with(sline, "G1 ")) + line.type = CoolingLine::TYPE_G1; + else if (boost::starts_with(sline, "G92 ")) + line.type = CoolingLine::TYPE_G92; + if (line.type) { + // G0, G1 or G92 + // Parse the G-code line. + std::vector<float> new_pos(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; + if ((line.type & CoolingLine::TYPE_G92) == 0) + // This is G0 or G1 line and it sets the feedrate. This mark is used for reducing the duplicate F calls. + line.type |= CoolingLine::TYPE_HAS_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 |= CoolingLine::TYPE_EXTERNAL_PERIMETER; + if (wipe) + line.type |= CoolingLine::TYPE_WIPE; + if (boost::contains(sline, ";_EXTRUDE_SET_SPEED") && ! wipe) { + line.type |= CoolingLine::TYPE_ADJUSTABLE; + active_speed_modifier = adjustment->lines.size(); + } + if ((line.type & CoolingLine::TYPE_G92) == 0) { + // G0 or G1. Calculate the duration. + if (config.use_relative_e_distances.value) + // Reset extruder accumulator. + current_pos[3] = 0.f; + float dif[4]; + for (size_t i = 0; i < 4; ++ i) + dif[i] = new_pos[i] - 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]); + } + line.feedrate = new_pos[4]; + assert((line.type & CoolingLine::TYPE_ADJUSTABLE) == 0 || line.feedrate > 0.f); + if (line.length > 0) + line.time = line.length / line.feedrate; + line.time_max = line.time; + if ((line.type & CoolingLine::TYPE_ADJUSTABLE) || active_speed_modifier != size_t(-1)) + line.time_max = (adjustment->min_print_speed == 0.f) ? FLT_MAX : std::max(line.time, line.length / adjustment->min_print_speed); + if (active_speed_modifier < adjustment->lines.size() && (line.type & CoolingLine::TYPE_G1)) { + // Inside the ";_EXTRUDE_SET_SPEED" blocks, there must not be a G1 Fxx entry. + assert((line.type & CoolingLine::TYPE_HAS_F) == 0); + CoolingLine &sm = adjustment->lines[active_speed_modifier]; + assert(sm.feedrate > 0.f); + 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; + } + } + current_pos = std::move(new_pos); + } else if (boost::starts_with(sline, ";_EXTRUDE_END")) { + line.type = CoolingLine::TYPE_EXTRUDE_END; + active_speed_modifier = size_t(-1); + } else if (boost::starts_with(sline, toolchange_prefix)) { + // Switch the tool. + line.type = CoolingLine::TYPE_SET_TOOL; + unsigned int new_extruder = (unsigned int)atoi(sline.c_str() + toolchange_prefix.size()); + if (new_extruder != current_extruder) { + current_extruder = new_extruder; + adjustment = &per_extruder_adjustments[map_extruder_to_per_extruder_adjustment[current_extruder]]; + } + } else if (boost::starts_with(sline, ";_BRIDGE_FAN_START")) { + line.type = CoolingLine::TYPE_BRIDGE_FAN_START; + } else if (boost::starts_with(sline, ";_BRIDGE_FAN_END")) { + line.type = CoolingLine::TYPE_BRIDGE_FAN_END; + } else if (boost::starts_with(sline, "G4 ")) { + // Parse the wait time. + line.type = CoolingLine::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)); + } + + return per_extruder_adjustments; +} + +// Slow down an extruder range proportionally down to slowdown_below_layer_time. +// Return the total time for the complete layer. +static inline float extruder_range_slow_down_proportional( + std::vector<PerExtruderAdjustments*>::iterator it_begin, + std::vector<PerExtruderAdjustments*>::iterator it_end, + // Elapsed time for the extruders already processed. + float elapsed_time_total0, + // Initial total elapsed time before slow down. + float elapsed_time_before_slowdown, + // Target time for the complete layer (all extruders applied). + float slowdown_below_layer_time) +{ + // Total layer time after the slow down has been applied. + float total_after_slowdown = elapsed_time_before_slowdown; + // Now decide, whether the external perimeters shall be slowed down as well. + float max_time_nep = elapsed_time_total0; + for (auto it = it_begin; it != it_end; ++ it) + max_time_nep += (*it)->maximum_time_after_slowdown(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 = elapsed_time_total0; + for (auto it = it_begin; it != it_end; ++ it) + non_adjustable_time += (*it)->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_after_slowdown - non_adjustable_time); + assert(factor > 1.f); + total_after_slowdown = elapsed_time_total0; + for (auto it = it_begin; it != it_end; ++ it) + total_after_slowdown += (*it)->slow_down_proportional(factor, false); + if (total_after_slowdown > 0.95f * slowdown_below_layer_time) + break; + } + } else { + // Slow down everything. First slow down the non-external perimeters to maximum. + for (auto it = it_begin; it != it_end; ++ it) + (*it)->slowdown_to_minimum_feedrate(false); + // Slow down the external perimeters proportionally. + float non_adjustable_time = elapsed_time_total0; + for (auto it = it_begin; it != it_end; ++ it) + non_adjustable_time += (*it)->non_adjustable_time(true); + for (size_t iter = 0; iter < 5; ++ iter) { + float factor = (slowdown_below_layer_time - non_adjustable_time) / (total_after_slowdown - non_adjustable_time); + assert(factor > 1.f); + total_after_slowdown = elapsed_time_total0; + for (auto it = it_begin; it != it_end; ++ it) + total_after_slowdown += (*it)->slow_down_proportional(factor, true); + if (total_after_slowdown > 0.95f * slowdown_below_layer_time) + break; + } + } + return total_after_slowdown; +} + +// Slow down an extruder range to slowdown_below_layer_time. +// Return the total time for the complete layer. +static inline void extruder_range_slow_down_non_proportional( + std::vector<PerExtruderAdjustments*>::iterator it_begin, + std::vector<PerExtruderAdjustments*>::iterator it_end, + float time_stretch) +{ + // Slow down. Try to equalize the feedrates. + std::vector<PerExtruderAdjustments*> by_min_print_speed(it_begin, it_end); + // Find the next highest adjustable feedrate among the extruders. + float feedrate = 0; + for (PerExtruderAdjustments *adj : by_min_print_speed) { + adj->idx_line_begin = 0; + adj->idx_line_end = 0; + assert(adj->idx_line_begin < adj->n_lines_adjustable); + if (adj->lines[adj->idx_line_begin].feedrate > feedrate) + feedrate = adj->lines[adj->idx_line_begin].feedrate; + } + assert(feedrate > 0.f); + // Sort by min_print_speed, maximum speed first. + std::sort(by_min_print_speed.begin(), by_min_print_speed.end(), + [](const PerExtruderAdjustments *p1, const PerExtruderAdjustments *p2){ return p1->min_print_speed > p2->min_print_speed; }); + // Slow down, fast moves first. + for (;;) { + // For each extruder, find the span of lines with a feedrate close to feedrate. + for (PerExtruderAdjustments *adj : by_min_print_speed) { + for (adj->idx_line_end = adj->idx_line_begin; + adj->idx_line_end < adj->n_lines_adjustable && adj->lines[adj->idx_line_end].feedrate > feedrate - EPSILON; + ++ adj->idx_line_end) ; + } + // Find the next highest adjustable feedrate among the extruders. + float feedrate_next = 0.f; + for (PerExtruderAdjustments *adj : by_min_print_speed) + if (adj->idx_line_end < adj->n_lines_adjustable && adj->lines[adj->idx_line_end].feedrate > feedrate_next) + feedrate_next = adj->lines[adj->idx_line_end].feedrate; + // Slow down, limited by max(feedrate_next, min_print_speed). + for (auto adj = by_min_print_speed.begin(); adj != by_min_print_speed.end();) { + // Slow down at most by time_stretch. + if ((*adj)->min_print_speed == 0.f) { + // All the adjustable speeds are now lowered to the same speed, + // and the minimum speed is set to zero. + float time_adjustable = 0.f; + for (auto it = adj; it != by_min_print_speed.end(); ++ it) + time_adjustable += (*it)->adjustable_time(true); + float rate = (time_adjustable + time_stretch) / time_adjustable; + for (auto it = adj; it != by_min_print_speed.end(); ++ it) + (*it)->slow_down_proportional(rate, true); + return; + } else { + float feedrate_limit = std::max(feedrate_next, (*adj)->min_print_speed); + bool done = false; + float time_stretch_max = 0.f; + for (auto it = adj; it != by_min_print_speed.end(); ++ it) + time_stretch_max += (*it)->time_stretch_when_slowing_down_to_feedrate(feedrate_limit); + if (time_stretch_max >= time_stretch) { + feedrate_limit = feedrate - (feedrate - feedrate_limit) * time_stretch / time_stretch_max; + done = true; + } else + time_stretch -= time_stretch_max; + for (auto it = adj; it != by_min_print_speed.end(); ++ it) + (*it)->slow_down_to_feedrate(feedrate_limit); + if (done) + return; + } + // Skip the other extruders with nearly the same min_print_speed, as they have been processed already. + auto next = adj; + for (++ next; next != by_min_print_speed.end() && (*next)->min_print_speed > (*adj)->min_print_speed - EPSILON; ++ next); + adj = next; + } + if (feedrate_next == 0.f) + // There are no other extrusions available for slow down. + break; + for (PerExtruderAdjustments *adj : by_min_print_speed) { + adj->idx_line_begin = adj->idx_line_end; + feedrate = feedrate_next; + } + } +} + +// Calculate slow down for all the extruders. +float CoolingBuffer::calculate_layer_slowdown(std::vector<PerExtruderAdjustments> &per_extruder_adjustments) +{ + // Sort the extruders by an increasing slowdown_below_layer_time. + // The layers with a lower slowdown_below_layer_time are slowed down + // together with all the other layers with slowdown_below_layer_time above. + std::vector<PerExtruderAdjustments*> by_slowdown_time; + by_slowdown_time.reserve(per_extruder_adjustments.size()); + // 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_total0 = 0.f; + for (PerExtruderAdjustments &adj : per_extruder_adjustments) { + // Curren total time for this extruder. + adj.time_total = adj.elapsed_time_total(); + // Maximum time for this extruder, when all extrusion moves are slowed down to min_extrusion_speed. + adj.time_maximum = adj.maximum_time_after_slowdown(true); + if (adj.cooling_slow_down_enabled && adj.lines.size() > 0) { + by_slowdown_time.emplace_back(&adj); + if (! m_cooling_logic_proportional) + // sorts the lines, also sets adj.time_non_adjustable + adj.sort_lines_by_decreasing_feedrate(); + } else + elapsed_time_total0 += adj.elapsed_time_total(); + } + std::sort(by_slowdown_time.begin(), by_slowdown_time.end(), + [](const PerExtruderAdjustments *adj1, const PerExtruderAdjustments *adj2) + { return adj1->slowdown_below_layer_time < adj2->slowdown_below_layer_time; }); + + for (auto cur_begin = by_slowdown_time.begin(); cur_begin != by_slowdown_time.end(); ++ cur_begin) { + PerExtruderAdjustments &adj = *(*cur_begin); + // Calculate the current adjusted elapsed_time_total over the non-finalized extruders. + float total = elapsed_time_total0; + for (auto it = cur_begin; it != by_slowdown_time.end(); ++ it) + total += (*it)->time_total; + float slowdown_below_layer_time = adj.slowdown_below_layer_time * 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 = elapsed_time_total0; + for (auto it = cur_begin; it != by_slowdown_time.end(); ++ it) + max_time += (*it)->time_maximum; + if (max_time > slowdown_below_layer_time) { + if (m_cooling_logic_proportional) + extruder_range_slow_down_proportional(cur_begin, by_slowdown_time.end(), elapsed_time_total0, total, slowdown_below_layer_time); + else + extruder_range_slow_down_non_proportional(cur_begin, by_slowdown_time.end(), slowdown_below_layer_time - total); + } else { + // Slow down to maximum possible. + for (auto it = cur_begin; it != by_slowdown_time.end(); ++ it) + (*it)->slowdown_to_minimum_feedrate(true); + } + } + elapsed_time_total0 += adj.elapsed_time_total(); + } + + return elapsed_time_total0; +} + +// Apply slow down over G-code lines stored in per_extruder_adjustments, enable fan if needed. +// Returns the adjusted G-code. +std::string CoolingBuffer::apply_layer_cooldown( + // Source G-code for the current layer. + const std::string &gcode, + // ID of the current layer, used to disable fan for the first n layers. + size_t layer_id, + // Total time of this layer after slow down, used to control the fan. + float layer_time, + // Per extruder list of G-code lines and their cool down attributes. + std::vector<PerExtruderAdjustments> &per_extruder_adjustments) +{ + // First sort the adjustment lines by of multiple extruders by their position in the source G-code. + std::vector<const CoolingLine*> lines; + { + size_t n_lines = 0; + for (const PerExtruderAdjustments &adj : per_extruder_adjustments) + n_lines += adj.lines.size(); + lines.reserve(n_lines); + for (const PerExtruderAdjustments &adj : per_extruder_adjustments) + for (const CoolingLine &line : adj.lines) + lines.emplace_back(&line); + std::sort(lines.begin(), lines.end(), [](const CoolingLine *ln1, const CoolingLine *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, layer_time, &new_gcode, &fan_speed, &bridge_fan_control, &bridge_fan_speed ]() { + const FullPrintConfig &config = m_gcodegen.config(); +#define EXTRUDER_CONFIG(OPT) config.OPT.get_at(m_current_extruder) + 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 (layer_time < slowdown_below_layer_time) { + // Layer time very short. Enable the fan to a full throttle. + fan_speed_new = max_fan_speed; + } else if (layer_time < fan_below_layer_time) { + // Layer time quite short. Enable the fan proportionally according to the current layer time. + assert(layer_time >= slowdown_below_layer_time); + double t = (layer_time - 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); +#undef EXTRUDER_CONFIG + bridge_fan_control = bridge_fan_speed > fan_speed_new; + } else { + bridge_fan_control = false; + bridge_fan_speed = 0; + fan_speed_new = 0; + } + if (fan_speed_new != fan_speed) { + fan_speed = fan_speed_new; + new_gcode += m_gcodegen.writer().set_fan(fan_speed); + } + }; + + const char *pos = gcode.c_str(); + int current_feedrate = 0; + const std::string toolchange_prefix = m_gcodegen.writer().toolchange_prefix(); + change_extruder_set_fan(); + for (const CoolingLine *line : lines) { + const char *line_start = gcode.c_str() + line->line_start; + const char *line_end = gcode.c_str() + line->line_end; + if (line_start > pos) + new_gcode.append(pos, line_start - pos); + if (line->type & CoolingLine::TYPE_SET_TOOL) { + unsigned int new_extruder = (unsigned int)atoi(line_start + toolchange_prefix.size()); + if (new_extruder != m_current_extruder) { + m_current_extruder = new_extruder; + change_extruder_set_fan(); + } + new_gcode.append(line_start, line_end - line_start); + } else if (line->type & CoolingLine::TYPE_BRIDGE_FAN_START) { + if (bridge_fan_control) + new_gcode += m_gcodegen.writer().set_fan(bridge_fan_speed, true); + } else if (line->type & CoolingLine::TYPE_BRIDGE_FAN_END) { + if (bridge_fan_control) + new_gcode += m_gcodegen.writer().set_fan(fan_speed, 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)) { + // Find the start of a comment, or roll to the end of line. + const char *end = line_start; + for (; end < line_end && *end != ';'; ++ end); + // Find the 'F' word. + const char *fpos = strstr(line_start + 2, " F") + 2; + int new_feedrate = current_feedrate; + bool modify = false; + assert(fpos != nullptr); + if (line->slowdown) { + modify = true; + new_feedrate = int(floor(60. * line->feedrate + 0.5)); + } else { + new_feedrate = atoi(fpos); + if (new_feedrate != current_feedrate) { + // Append the line without the comment. + new_gcode.append(line_start, end - line_start); + current_feedrate = new_feedrate; + } else if ((line->type & (CoolingLine::TYPE_ADJUSTABLE | CoolingLine::TYPE_EXTERNAL_PERIMETER | CoolingLine::TYPE_WIPE)) || line->length == 0.) { + // Feedrate does not change and this line does not move the print head. Skip the complete G-code line including the G-code comment. + end = line_end; + } else { + // Remove the feedrate from the G0/G1 line. + modify = true; + } + } + if (modify) { + if (new_feedrate != current_feedrate) { + // Replace the feedrate. + new_gcode.append(line_start, fpos - line_start); + current_feedrate = new_feedrate; + char buf[64]; + sprintf(buf, "%d", int(current_feedrate)); + new_gcode += buf; + } else { + // Remove the feedrate word. + const char *f = fpos; + // Roll the pointer before the 'F' word. + for (f -= 2; f > line_start && (*f == ' ' || *f == '\t'); -- f); + // Append up to the F word, without the trailing whitespace. + new_gcode.append(line_start, f - line_start + 1); + } + // Skip the non-whitespaces of the F parameter up the comment or end of line. + for (; fpos != end && *fpos != ' ' && *fpos != ';' && *fpos != '\n'; ++fpos); + // Append the rest of the line without the comment. + if (fpos < end) + new_gcode.append(fpos, end - fpos); + // There should never be an empty G1 statement emited by the filter. Such lines should be removed completely. + assert(new_gcode.size() < 4 || new_gcode.substr(new_gcode.size() - 4) != "G1 \n"); + } + // Process the rest of the line. + if (end < line_end) { + if (line->type & (CoolingLine::TYPE_ADJUSTABLE | CoolingLine::TYPE_EXTERNAL_PERIMETER | CoolingLine::TYPE_WIPE)) { + // Process comments, remove ";_EXTRUDE_SET_SPEED", ";_EXTERNAL_PERIMETER", ";_WIPE" + std::string comment(end, line_end); + boost::replace_all(comment, ";_EXTRUDE_SET_SPEED", ""); + if (line->type & CoolingLine::TYPE_EXTERNAL_PERIMETER) + boost::replace_all(comment, ";_EXTERNAL_PERIMETER", ""); + if (line->type & CoolingLine::TYPE_WIPE) + boost::replace_all(comment, ";_WIPE", ""); + new_gcode += comment; + } else { + // Just attach the rest of the source line. + new_gcode.append(end, line_end - end); + } + } + } else { + new_gcode.append(line_start, line_end - line_start); + } + pos = line_end; + } + const char *gcode_end = gcode.c_str() + gcode.size(); + if (pos < gcode_end) + new_gcode.append(pos, gcode_end - pos); + + return new_gcode; +} + +} // namespace Slic3r diff --git a/src/libslic3r/GCode/CoolingBuffer.hpp b/src/libslic3r/GCode/CoolingBuffer.hpp new file mode 100644 index 000000000..bf4b082e2 --- /dev/null +++ b/src/libslic3r/GCode/CoolingBuffer.hpp @@ -0,0 +1,53 @@ +#ifndef slic3r_CoolingBuffer_hpp_ +#define slic3r_CoolingBuffer_hpp_ + +#include "libslic3r.h" +#include <map> +#include <string> + +namespace Slic3r { + +class GCode; +class Layer; +class PerExtruderAdjustments; + +// A standalone G-code filter, to control cooling of the print. +// The G-code is processed per layer. Once a layer is collected, fan start / stop commands are edited +// and the print is modified to stretch over a minimum layer time. +// +// The simple it sounds, the actual implementation is significantly more complex. +// Namely, for a multi-extruder print, each material may require a different cooling logic. +// For example, some materials may not like to print too slowly, while with some materials +// we may slow down significantly. +// +class CoolingBuffer { +public: + 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; } + +private: + CoolingBuffer& operator=(const CoolingBuffer&) = delete; + std::vector<PerExtruderAdjustments> parse_layer_gcode(const std::string &gcode, std::vector<float> ¤t_pos) const; + float calculate_layer_slowdown(std::vector<PerExtruderAdjustments> &per_extruder_adjustments); + // Apply slow down over G-code lines stored in per_extruder_adjustments, enable fan if needed. + // Returns the adjusted G-code. + std::string apply_layer_cooldown(const std::string &gcode, size_t layer_id, float layer_time, std::vector<PerExtruderAdjustments> &per_extruder_adjustments); + + GCode& m_gcodegen; + std::string m_gcode; + // Internal data. + // X,Y,Z,E,F + std::vector<char> m_axis; + std::vector<float> m_current_pos; + unsigned int m_current_extruder; + + // Old logic: proportional. + bool m_cooling_logic_proportional = false; +}; + +} + +#endif diff --git a/src/libslic3r/GCode/PostProcessor.cpp b/src/libslic3r/GCode/PostProcessor.cpp new file mode 100644 index 000000000..c04aeae3c --- /dev/null +++ b/src/libslic3r/GCode/PostProcessor.cpp @@ -0,0 +1,60 @@ +#include "PostProcessor.hpp" + +#if 1 +//#ifdef WIN32 + +namespace Slic3r { + +//FIXME Ignore until we include boost::process +void run_post_process_scripts(const std::string &path, const PrintConfig &config) +{ +} + +} // namespace Slic3r + +#else + +#include <boost/process/system.hpp> + +namespace Slic3r { + +void run_post_process_scripts(const std::string &path, const PrintConfig &config) +{ + if (config.post_process.values.empty()) + return; + config.setenv_(); + for (std::string script: config.post_process.values) { + // Ignore empty post processing script lines. + boost::trim(script); + if (script.empty()) + continue; + BOOST_LOG_TRIVIAL(info) << "Executing script " << script << " on file " << path; + if (! boost::filesystem::exists(boost::filesystem::path(path))) + throw std::runtime_exception(std::string("The configured post-processing script does not exist: ") + path); +#ifndef WIN32 + file_status fs = boost::filesystem::status(path); + //FIXME test if executible by the effective UID / GID. + // throw std::runtime_exception(std::string("The configured post-processing script is not executable: check permissions. ") + path)); +#endif + int result = 0; +#ifdef WIN32 + if (boost::iends_with(file, ".gcode")) { + // The current process may be slic3r.exe or slic3r-console.exe. + // Find the path of the process: + wchar_t wpath_exe[_MAX_PATH + 1]; + ::GetModuleFileNameW(nullptr, wpath_exe, _MAX_PATH); + boost::filesystem::path path_exe(wpath_exe); + // Replace it with the current perl interpreter. + result = boost::process::system((path_exe.parent_path() / "perl5.24.0.exe").string(), script, output_file); + } else +#else + result = boost::process::system(script, output_file); +#endif + if (result < 0) + BOOST_LOG_TRIVIAL(error) << "Script " << script << " on file " << path << " failed. Negative error code returned."; + } +} + +} // namespace Slic3r + +#endif diff --git a/src/libslic3r/GCode/PostProcessor.hpp b/src/libslic3r/GCode/PostProcessor.hpp new file mode 100644 index 000000000..ce47374cb --- /dev/null +++ b/src/libslic3r/GCode/PostProcessor.hpp @@ -0,0 +1,15 @@ +#ifndef slic3r_GCode_PostProcessor_hpp_ +#define slic3r_GCode_PostProcessor_hpp_ + +#include <string> + +#include "../libslic3r.h" +#include "../PrintConfig.hpp" + +namespace Slic3r { + +extern void run_post_process_scripts(const std::string &path, const PrintConfig &config); + +} // namespace Slic3r + +#endif /* slic3r_GCode_PostProcessor_hpp_ */ diff --git a/src/libslic3r/GCode/PressureEqualizer.cpp b/src/libslic3r/GCode/PressureEqualizer.cpp new file mode 100644 index 000000000..3b2a58a88 --- /dev/null +++ b/src/libslic3r/GCode/PressureEqualizer.cpp @@ -0,0 +1,621 @@ +#include <memory.h> +#include <string.h> +#include <float.h> + +#include "../libslic3r.h" +#include "../PrintConfig.hpp" + +#include "PressureEqualizer.hpp" + +namespace Slic3r { + +PressureEqualizer::PressureEqualizer(const Slic3r::GCodeConfig *config) : + m_config(config) +{ + reset(); +} + +PressureEqualizer::~PressureEqualizer() +{ +} + +void PressureEqualizer::reset() +{ + circular_buffer_pos = 0; + circular_buffer_size = 100; + circular_buffer_items = 0; + circular_buffer.assign(circular_buffer_size, GCodeLine()); + + // Preallocate some data, so that output_buffer.data() will return an empty string. + output_buffer.assign(32, 0); + output_buffer_length = 0; + + m_current_extruder = 0; + // Zero the position of the XYZE axes + the current feed + memset(m_current_pos, 0, sizeof(float) * 5); + m_current_extrusion_role = erNone; + // Expect the first command to fill the nozzle (deretract). + m_retracted = true; + + // Calculate filamet crossections for the multiple extruders. + m_filament_crossections.clear(); + for (size_t i = 0; i < m_config->filament_diameter.values.size(); ++ i) { + double r = m_config->filament_diameter.values[i]; + double a = 0.25f*M_PI*r*r; + m_filament_crossections.push_back(float(a)); + } + + m_max_segment_length = 20.f; + // Volumetric rate of a 0.45mm x 0.2mm extrusion at 60mm/s XY movement: 0.45*0.2*60*60=5.4*60 = 324 mm^3/min + // Volumetric rate of a 0.45mm x 0.2mm extrusion at 20mm/s XY movement: 0.45*0.2*20*60=1.8*60 = 108 mm^3/min + // Slope of the volumetric rate, changing from 20mm/s to 60mm/s over 2 seconds: (5.4-1.8)*60*60/2=60*60*1.8 = 6480 mm^3/min^2 = 1.8 mm^3/s^2 + m_max_volumetric_extrusion_rate_slope_positive = (m_config == NULL) ? 6480.f : + m_config->max_volumetric_extrusion_rate_slope_positive.value * 60.f * 60.f; + m_max_volumetric_extrusion_rate_slope_negative = (m_config == NULL) ? 6480.f : + m_config->max_volumetric_extrusion_rate_slope_negative.value * 60.f * 60.f; + + for (size_t i = 0; i < numExtrusionRoles; ++ i) { + m_max_volumetric_extrusion_rate_slopes[i].negative = m_max_volumetric_extrusion_rate_slope_negative; + m_max_volumetric_extrusion_rate_slopes[i].positive = m_max_volumetric_extrusion_rate_slope_positive; + } + + // Don't regulate the pressure in infill. + m_max_volumetric_extrusion_rate_slopes[erBridgeInfill].negative = 0; + m_max_volumetric_extrusion_rate_slopes[erBridgeInfill].positive = 0; + // Don't regulate the pressure in gap fill. + m_max_volumetric_extrusion_rate_slopes[erGapFill].negative = 0; + m_max_volumetric_extrusion_rate_slopes[erGapFill].positive = 0; + + m_stat.reset(); + line_idx = 0; +} + +const char* PressureEqualizer::process(const char *szGCode, bool flush) +{ + // Reset length of the output_buffer. + output_buffer_length = 0; + + if (szGCode != 0) { + const char *p = szGCode; + while (*p != 0) { + // Find end of the line. + const char *endl = p; + // Slic3r always generates end of lines in a Unix style. + for (; *endl != 0 && *endl != '\n'; ++ endl) ; + if (circular_buffer_items == circular_buffer_size) + // Buffer is full. Push out the oldest line. + output_gcode_line(circular_buffer[circular_buffer_pos]); + else + ++ circular_buffer_items; + // Process a G-code line, store it into the provided GCodeLine object. + size_t idx_tail = circular_buffer_pos; + circular_buffer_pos = circular_buffer_idx_next(circular_buffer_pos); + if (! process_line(p, endl - p, circular_buffer[idx_tail])) { + // The line has to be forgotten. It contains comment marks, which shall be + // filtered out of the target g-code. + circular_buffer_pos = idx_tail; + -- circular_buffer_items; + } + p = endl; + if (*p == '\n') + ++ p; + } + } + + if (flush) { + // Flush the remaining valid lines of the circular buffer. + for (size_t idx = circular_buffer_idx_head(); circular_buffer_items > 0; -- circular_buffer_items) { + output_gcode_line(circular_buffer[idx]); + if (++ idx == circular_buffer_size) + idx = 0; + } + // Reset the index pointer. + assert(circular_buffer_items == 0); + circular_buffer_pos = 0; + +#if 1 + printf("Statistics: \n"); + printf("Minimum volumetric extrusion rate: %f\n", m_stat.volumetric_extrusion_rate_min); + printf("Maximum volumetric extrusion rate: %f\n", m_stat.volumetric_extrusion_rate_max); + if (m_stat.extrusion_length > 0) + m_stat.volumetric_extrusion_rate_avg /= m_stat.extrusion_length; + printf("Average volumetric extrusion rate: %f\n", m_stat.volumetric_extrusion_rate_avg); + m_stat.reset(); +#endif + } + + return output_buffer.data(); +} + +// Is a white space? +static inline bool is_ws(const char c) { return c == ' ' || c == '\t'; } +// Is it an end of line? Consider a comment to be an end of line as well. +static inline bool is_eol(const char c) { return c == 0 || c == '\r' || c == '\n' || c == ';'; }; +// Is it a white space or end of line? +static inline bool is_ws_or_eol(const char c) { return is_ws(c) || is_eol(c); }; + +// Eat whitespaces. +static void eatws(const char *&line) +{ + while (is_ws(*line)) + ++ line; +} + +// Parse an int starting at the current position of a line. +// If succeeded, the line pointer is advanced. +static inline int parse_int(const char *&line) +{ + char *endptr = NULL; + long result = strtol(line, &endptr, 10); + if (endptr == NULL || !is_ws_or_eol(*endptr)) + throw std::runtime_error("PressureEqualizer: Error parsing an int"); + line = endptr; + return int(result); +}; + +// Parse an int starting at the current position of a line. +// If succeeded, the line pointer is advanced. +static inline float parse_float(const char *&line) +{ + char *endptr = NULL; + float result = strtof(line, &endptr); + if (endptr == NULL || !is_ws_or_eol(*endptr)) + throw std::runtime_error("PressureEqualizer: Error parsing a float"); + line = endptr; + return result; +}; + +#define EXTRUSION_ROLE_TAG ";_EXTRUSION_ROLE:" +bool PressureEqualizer::process_line(const char *line, const size_t len, GCodeLine &buf) +{ + if (strncmp(line, EXTRUSION_ROLE_TAG, strlen(EXTRUSION_ROLE_TAG)) == 0) { + line += strlen(EXTRUSION_ROLE_TAG); + int role = atoi(line); + m_current_extrusion_role = ExtrusionRole(role); + ++ line_idx; + return false; + } + + // Set the type, copy the line to the buffer. + buf.type = GCODELINETYPE_OTHER; + buf.modified = false; + if (buf.raw.size() < len + 1) + buf.raw.assign(line, line + len + 1); + else + memcpy(buf.raw.data(), line, len); + buf.raw[len] = 0; + buf.raw_length = len; + + memcpy(buf.pos_start, m_current_pos, sizeof(float)*5); + memcpy(buf.pos_end, m_current_pos, sizeof(float)*5); + memset(buf.pos_provided, 0, 5); + + buf.volumetric_extrusion_rate = 0.f; + buf.volumetric_extrusion_rate_start = 0.f; + buf.volumetric_extrusion_rate_end = 0.f; + buf.max_volumetric_extrusion_rate_slope_positive = 0.f; + buf.max_volumetric_extrusion_rate_slope_negative = 0.f; + buf.extrusion_role = m_current_extrusion_role; + + // Parse the G-code line, store the result into the buf. + switch (toupper(*line ++)) { + case 'G': { + int gcode = parse_int(line); + eatws(line); + switch (gcode) { + case 0: + case 1: + { + // G0, G1: A FFF 3D printer does not make a difference between the two. + float new_pos[5]; + memcpy(new_pos, m_current_pos, sizeof(float)*5); + bool changed[5] = { false, false, false, false, false }; + while (!is_eol(*line)) { + char axis = toupper(*line++); + int i = -1; + switch (axis) { + case 'X': + case 'Y': + case 'Z': + i = axis - 'X'; + break; + case 'E': + i = 3; + break; + case 'F': + i = 4; + break; + default: + assert(false); + } + if (i == -1) + throw std::runtime_error(std::string("GCode::PressureEqualizer: Invalid axis for G0/G1: ") + axis); + buf.pos_provided[i] = true; + new_pos[i] = parse_float(line); + if (i == 3 && m_config->use_relative_e_distances.value) + new_pos[i] += m_current_pos[i]; + changed[i] = new_pos[i] != m_current_pos[i]; + eatws(line); + } + if (changed[3]) { + // Extrusion, retract or unretract. + float diff = new_pos[3] - m_current_pos[3]; + if (diff < 0) { + buf.type = GCODELINETYPE_RETRACT; + m_retracted = true; + } else if (! changed[0] && ! changed[1] && ! changed[2]) { + // assert(m_retracted); + buf.type = GCODELINETYPE_UNRETRACT; + m_retracted = false; + } else { + assert(changed[0] || changed[1]); + // Moving in XY plane. + buf.type = GCODELINETYPE_EXTRUDE; + // Calculate the volumetric extrusion rate. + float diff[4]; + for (size_t i = 0; i < 4; ++ i) + diff[i] = new_pos[i] - m_current_pos[i]; + // volumetric extrusion rate = A_filament * F_xyz * L_e / L_xyz [mm^3/min] + float len2 = diff[0]*diff[0]+diff[1]*diff[1]+diff[2]*diff[2]; + float rate = m_filament_crossections[m_current_extruder] * new_pos[4] * sqrt((diff[3]*diff[3])/len2); + buf.volumetric_extrusion_rate = rate; + buf.volumetric_extrusion_rate_start = rate; + buf.volumetric_extrusion_rate_end = rate; + m_stat.update(rate, sqrt(len2)); + if (rate < 40.f) { + printf("Extremely low flow rate: %f. Line %d, Length: %f, extrusion: %f Old position: (%f, %f, %f), new position: (%f, %f, %f)\n", + rate, + int(line_idx), + sqrt(len2), sqrt((diff[3]*diff[3])/len2), + m_current_pos[0], m_current_pos[1], m_current_pos[2], + new_pos[0], new_pos[1], new_pos[2]); + } + } + } else if (changed[0] || changed[1] || changed[2]) { + // Moving without extrusion. + buf.type = GCODELINETYPE_MOVE; + } + memcpy(m_current_pos, new_pos, sizeof(float) * 5); + break; + } + case 92: + { + // G92 : Set Position + // Set a logical coordinate position to a new value without actually moving the machine motors. + // Which axes to set? + bool set = false; + while (!is_eol(*line)) { + char axis = toupper(*line++); + switch (axis) { + case 'X': + case 'Y': + case 'Z': + m_current_pos[axis - 'X'] = (!is_ws_or_eol(*line)) ? parse_float(line) : 0.f; + set = true; + break; + case 'E': + m_current_pos[3] = (!is_ws_or_eol(*line)) ? parse_float(line) : 0.f; + set = true; + break; + default: + throw std::runtime_error(std::string("GCode::PressureEqualizer: Incorrect axis in a G92 G-code: ") + axis); + } + eatws(line); + } + assert(set); + break; + } + case 10: + case 22: + // Firmware retract. + buf.type = GCODELINETYPE_RETRACT; + m_retracted = true; + break; + case 11: + case 23: + // Firmware unretract. + buf.type = GCODELINETYPE_UNRETRACT; + m_retracted = false; + break; + default: + // Ignore the rest. + break; + } + break; + } + case 'M': { + int mcode = parse_int(line); + eatws(line); + switch (mcode) { + default: + // Ignore the rest of the M-codes. + break; + } + break; + } + case 'T': + { + // Activate an extruder head. + int new_extruder = parse_int(line); + if (new_extruder != m_current_extruder) { + m_current_extruder = new_extruder; + m_retracted = true; + buf.type = GCODELINETYPE_TOOL_CHANGE; + } else { + buf.type = GCODELINETYPE_NOOP; + } + break; + } + } + + buf.extruder_id = m_current_extruder; + memcpy(buf.pos_end, m_current_pos, sizeof(float)*5); + + adjust_volumetric_rate(); + ++ line_idx; + return true; +} + +void PressureEqualizer::output_gcode_line(GCodeLine &line) +{ + if (! line.modified) { + push_to_output(line.raw.data(), line.raw_length, true); + return; + } + + // The line was modified. + // Find the comment. + const char *comment = line.raw.data(); + while (*comment != ';' && *comment != 0) ++comment; + if (*comment != ';') + comment = NULL; + + // Emit the line with lowered extrusion rates. + float l2 = line.dist_xyz2(); + float l = sqrt(l2); + size_t nSegments = size_t(ceil(l / m_max_segment_length)); + if (nSegments == 1) { + // Just update this segment. + push_line_to_output(line, line.feedrate() * line.volumetric_correction_avg(), comment); + } else { + bool accelerating = line.volumetric_extrusion_rate_start < line.volumetric_extrusion_rate_end; + // Update the initial and final feed rate values. + line.pos_start[4] = line.volumetric_extrusion_rate_start * line.pos_end[4] / line.volumetric_extrusion_rate; + line.pos_end [4] = line.volumetric_extrusion_rate_end * line.pos_end[4] / line.volumetric_extrusion_rate; + float feed_avg = 0.5f * (line.pos_start[4] + line.pos_end[4]); + // Limiting volumetric extrusion rate slope for this segment. + float max_volumetric_extrusion_rate_slope = accelerating ? + line.max_volumetric_extrusion_rate_slope_positive : line.max_volumetric_extrusion_rate_slope_negative; + // Total time for the segment, corrected for the possibly lowered volumetric feed rate, + // if accelerating / decelerating over the complete segment. + float t_total = line.dist_xyz() / feed_avg; + // Time of the acceleration / deceleration part of the segment, if accelerating / decelerating + // with the maximum volumetric extrusion rate slope. + float t_acc = 0.5f * (line.volumetric_extrusion_rate_start + line.volumetric_extrusion_rate_end) / max_volumetric_extrusion_rate_slope; + float l_acc = l; + float l_steady = 0.f; + if (t_acc < t_total) { + // One may achieve higher print speeds if part of the segment is not speed limited. + float l_acc = t_acc * feed_avg; + float l_steady = l - l_acc; + if (l_steady < 0.5f * m_max_segment_length) { + l_acc = l; + l_steady = 0.f; + } else + nSegments = size_t(ceil(l_acc / m_max_segment_length)); + } + float pos_start[5]; + float pos_end [5]; + float pos_end2 [4]; + memcpy(pos_start, line.pos_start, sizeof(float)*5); + memcpy(pos_end , line.pos_end , sizeof(float)*5); + if (l_steady > 0.f) { + // There will be a steady feed segment emitted. + if (accelerating) { + // Prepare the final steady feed rate segment. + memcpy(pos_end2, pos_end, sizeof(float)*4); + float t = l_acc / l; + for (int i = 0; i < 4; ++ i) { + pos_end[i] = pos_start[i] + (pos_end[i] - pos_start[i]) * t; + line.pos_provided[i] = true; + } + } else { + // Emit the steady feed rate segment. + float t = l_steady / l; + for (int i = 0; i < 4; ++ i) { + line.pos_end[i] = pos_start[i] + (pos_end[i] - pos_start[i]) * t; + line.pos_provided[i] = true; + } + push_line_to_output(line, pos_start[4], comment); + comment = NULL; + memcpy(line.pos_start, line.pos_end, sizeof(float)*5); + memcpy(pos_start, line.pos_end, sizeof(float)*5); + } + } + // Split the segment into pieces. + for (size_t i = 1; i < nSegments; ++ i) { + float t = float(i) / float(nSegments); + for (size_t j = 0; j < 4; ++ j) { + line.pos_end[j] = pos_start[j] + (pos_end[j] - pos_start[j]) * t; + line.pos_provided[j] = true; + } + // Interpolate the feed rate at the center of the segment. + push_line_to_output(line, pos_start[4] + (pos_end[4] - pos_start[4]) * (float(i) - 0.5f) / float(nSegments), comment); + comment = NULL; + memcpy(line.pos_start, line.pos_end, sizeof(float)*5); + } + if (l_steady > 0.f && accelerating) { + for (int i = 0; i < 4; ++ i) { + line.pos_end[i] = pos_end2[i]; + line.pos_provided[i] = true; + } + push_line_to_output(line, pos_end[4], comment); + } + } +} + +void PressureEqualizer::adjust_volumetric_rate() +{ + if (circular_buffer_items < 2) + return; + + // Go back from the current circular_buffer_pos and lower the feedtrate to decrease the slope of the extrusion rate changes. + const size_t idx_head = circular_buffer_idx_head(); + const size_t idx_tail = circular_buffer_idx_prev(circular_buffer_idx_tail()); + size_t idx = idx_tail; + if (idx == idx_head || ! circular_buffer[idx].extruding()) + // Nothing to do, the last move is not extruding. + return; + + float feedrate_per_extrusion_role[numExtrusionRoles]; + for (size_t i = 0; i < numExtrusionRoles; ++ i) + feedrate_per_extrusion_role[i] = FLT_MAX; + feedrate_per_extrusion_role[circular_buffer[idx].extrusion_role] = circular_buffer[idx].volumetric_extrusion_rate_start; + + bool modified = true; + while (modified && idx != idx_head) { + size_t idx_prev = circular_buffer_idx_prev(idx); + for (; ! circular_buffer[idx_prev].extruding() && idx_prev != idx_head; idx_prev = circular_buffer_idx_prev(idx_prev)) ; + if (! circular_buffer[idx_prev].extruding()) + break; + // Volumetric extrusion rate at the start of the succeding segment. + float rate_succ = circular_buffer[idx].volumetric_extrusion_rate_start; + // What is the gradient of the extrusion rate between idx_prev and idx? + idx = idx_prev; + GCodeLine &line = circular_buffer[idx]; + for (size_t iRole = 1; iRole < numExtrusionRoles; ++ iRole) { + float rate_slope = m_max_volumetric_extrusion_rate_slopes[iRole].negative; + if (rate_slope == 0) + // The negative rate is unlimited. + continue; + float rate_end = feedrate_per_extrusion_role[iRole]; + if (iRole == line.extrusion_role && rate_succ < rate_end) + // Limit by the succeeding volumetric flow rate. + rate_end = rate_succ; + if (line.volumetric_extrusion_rate_end > rate_end) { + line.volumetric_extrusion_rate_end = rate_end; + line.modified = true; + } else if (iRole == line.extrusion_role) { + rate_end = line.volumetric_extrusion_rate_end; + } else if (rate_end == FLT_MAX) { + // The rate for ExtrusionRole iRole is unlimited. + continue; + } else { + // Use the original, 'floating' extrusion rate as a starting point for the limiter. + } +// modified = false; + float rate_start = rate_end + rate_slope * line.time_corrected(); + if (rate_start < line.volumetric_extrusion_rate_start) { + // Limit the volumetric extrusion rate at the start of this segment due to a segment + // of ExtrusionType iRole, which will be extruded in the future. + line.volumetric_extrusion_rate_start = rate_start; + line.max_volumetric_extrusion_rate_slope_negative = rate_slope; + line.modified = true; +// modified = true; + } + feedrate_per_extrusion_role[iRole] = (iRole == line.extrusion_role) ? line.volumetric_extrusion_rate_start : rate_start; + } + } + + // Go forward and adjust the feedrate to decrease the slope of the extrusion rate changes. + for (size_t i = 0; i < numExtrusionRoles; ++ i) + feedrate_per_extrusion_role[i] = FLT_MAX; + feedrate_per_extrusion_role[circular_buffer[idx].extrusion_role] = circular_buffer[idx].volumetric_extrusion_rate_end; + + assert(circular_buffer[idx].extruding()); + while (idx != idx_tail) { + size_t idx_next = circular_buffer_idx_next(idx); + for (; ! circular_buffer[idx_next].extruding() && idx_next != idx_tail; idx_next = circular_buffer_idx_next(idx_next)) ; + if (! circular_buffer[idx_next].extruding()) + break; + float rate_prec = circular_buffer[idx].volumetric_extrusion_rate_end; + // What is the gradient of the extrusion rate between idx_prev and idx? + idx = idx_next; + GCodeLine &line = circular_buffer[idx]; + for (size_t iRole = 1; iRole < numExtrusionRoles; ++ iRole) { + float rate_slope = m_max_volumetric_extrusion_rate_slopes[iRole].positive; + if (rate_slope == 0) + // The positive rate is unlimited. + continue; + float rate_start = feedrate_per_extrusion_role[iRole]; + if (iRole == line.extrusion_role && rate_prec < rate_start) + rate_start = rate_prec; + if (line.volumetric_extrusion_rate_start > rate_start) { + line.volumetric_extrusion_rate_start = rate_start; + line.modified = true; + } else if (iRole == line.extrusion_role) { + rate_start = line.volumetric_extrusion_rate_start; + } else if (rate_start == FLT_MAX) { + // The rate for ExtrusionRole iRole is unlimited. + continue; + } else { + // Use the original, 'floating' extrusion rate as a starting point for the limiter. + } + float rate_end = (rate_slope == 0) ? FLT_MAX : rate_start + rate_slope * line.time_corrected(); + if (rate_end < line.volumetric_extrusion_rate_end) { + // Limit the volumetric extrusion rate at the start of this segment due to a segment + // of ExtrusionType iRole, which was extruded before. + line.volumetric_extrusion_rate_end = rate_end; + line.max_volumetric_extrusion_rate_slope_positive = rate_slope; + line.modified = true; + } + feedrate_per_extrusion_role[iRole] = (iRole == line.extrusion_role) ? line.volumetric_extrusion_rate_end : rate_end; + } + } +} + +void PressureEqualizer::push_axis_to_output(const char axis, const float value, bool add_eol) +{ + char buf[2048]; + int len = sprintf(buf, + (axis == 'E') ? " %c%.3f" : " %c%.5f", + axis, value); + push_to_output(buf, len, add_eol); +} + +void PressureEqualizer::push_to_output(const char *text, const size_t len, bool add_eol) +{ + // New length of the output buffer content. + size_t len_new = output_buffer_length + len + 1; + if (add_eol) + ++ len_new; + + // Resize the output buffer to a power of 2 higher than the required memory. + if (output_buffer.size() < len_new) { + size_t v = len_new; + // Compute the next highest power of 2 of 32-bit v + // http://graphics.stanford.edu/~seander/bithacks.html + v--; + v |= v >> 1; + v |= v >> 2; + v |= v >> 4; + v |= v >> 8; + v |= v >> 16; + v++; + output_buffer.resize(v); + } + + // Copy the text to the output. + if (len != 0) { + memcpy(output_buffer.data() + output_buffer_length, text, len); + output_buffer_length += len; + } + if (add_eol) + output_buffer[output_buffer_length ++] = '\n'; + output_buffer[output_buffer_length] = 0; +} + +void PressureEqualizer::push_line_to_output(const GCodeLine &line, const float new_feedrate, const char *comment) +{ + push_to_output("G1", 2, false); + for (char i = 0; i < 3; ++ i) + if (line.pos_provided[i]) + push_axis_to_output('X'+i, line.pos_end[i]); + push_axis_to_output('E', m_config->use_relative_e_distances.value ? (line.pos_end[3] - line.pos_start[3]) : line.pos_end[3]); +// if (line.pos_provided[4] || fabs(line.feedrate() - new_feedrate) > 1e-5) + push_axis_to_output('F', new_feedrate); + // output comment and EOL + push_to_output(comment, (comment == NULL) ? 0 : strlen(comment), true); +} + +} // namespace Slic3r diff --git a/src/libslic3r/GCode/PressureEqualizer.hpp b/src/libslic3r/GCode/PressureEqualizer.hpp new file mode 100644 index 000000000..13cdc9418 --- /dev/null +++ b/src/libslic3r/GCode/PressureEqualizer.hpp @@ -0,0 +1,212 @@ +#ifndef slic3r_GCode_PressureEqualizer_hpp_ +#define slic3r_GCode_PressureEqualizer_hpp_ + +#include "../libslic3r.h" +#include "../PrintConfig.hpp" +#include "../ExtrusionEntity.hpp" + +namespace Slic3r { + +// Processes a G-code. Finds changes in the volumetric extrusion speed and adjusts the transitions +// between these paths to limit fast changes in the volumetric extrusion speed. +class PressureEqualizer +{ +public: + PressureEqualizer(const Slic3r::GCodeConfig *config); + ~PressureEqualizer(); + + void reset(); + + // Process a next batch of G-code lines. Flush the internal buffers if asked for. + const char* process(const char *szGCode, bool flush); + + size_t get_output_buffer_length() const { return output_buffer_length; } + +private: + struct Statistics + { + void reset() { + volumetric_extrusion_rate_min = std::numeric_limits<float>::max(); + volumetric_extrusion_rate_max = 0.f; + volumetric_extrusion_rate_avg = 0.f; + extrusion_length = 0.f; + } + void update(float volumetric_extrusion_rate, float length) { + volumetric_extrusion_rate_min = std::min(volumetric_extrusion_rate_min, volumetric_extrusion_rate); + volumetric_extrusion_rate_max = std::max(volumetric_extrusion_rate_max, volumetric_extrusion_rate); + volumetric_extrusion_rate_avg += volumetric_extrusion_rate * length; + extrusion_length += length; + } + float volumetric_extrusion_rate_min; + float volumetric_extrusion_rate_max; + float volumetric_extrusion_rate_avg; + float extrusion_length; + }; + + struct Statistics m_stat; + + // Keeps the reference, does not own the config. + const Slic3r::GCodeConfig *m_config; + + // Private configuration values + // How fast could the volumetric extrusion rate increase / decrase? mm^3/sec^2 + struct ExtrusionRateSlope { + float positive; + float negative; + }; + enum { numExtrusionRoles = erSupportMaterialInterface + 1 }; + ExtrusionRateSlope m_max_volumetric_extrusion_rate_slopes[numExtrusionRoles]; + float m_max_volumetric_extrusion_rate_slope_positive; + float m_max_volumetric_extrusion_rate_slope_negative; + // Maximum segment length to split a long segment, if the initial and the final flow rate differ. + float m_max_segment_length; + + // Configuration extracted from config. + // Area of the crossestion of each filament. Necessary to calculate the volumetric flow rate. + std::vector<float> m_filament_crossections; + + // Internal data. + // X,Y,Z,E,F + float m_current_pos[5]; + size_t m_current_extruder; + ExtrusionRole m_current_extrusion_role; + bool m_retracted; + + enum GCodeLineType + { + GCODELINETYPE_INVALID, + GCODELINETYPE_NOOP, + GCODELINETYPE_OTHER, + GCODELINETYPE_RETRACT, + GCODELINETYPE_UNRETRACT, + GCODELINETYPE_TOOL_CHANGE, + GCODELINETYPE_MOVE, + GCODELINETYPE_EXTRUDE, + }; + + struct GCodeLine + { + GCodeLine() : + type(GCODELINETYPE_INVALID), + raw_length(0), + modified(false), + extruder_id(0), + volumetric_extrusion_rate(0.f), + volumetric_extrusion_rate_start(0.f), + volumetric_extrusion_rate_end(0.f) + {} + + bool moving_xy() const { return fabs(pos_end[0] - pos_start[0]) > 0.f || fabs(pos_end[1] - pos_start[1]) > 0.f; } + bool moving_z () const { return fabs(pos_end[2] - pos_start[2]) > 0.f; } + bool extruding() const { return moving_xy() && pos_end[3] > pos_start[3]; } + bool retracting() const { return pos_end[3] < pos_start[3]; } + bool deretracting() const { return ! moving_xy() && pos_end[3] > pos_start[3]; } + + float dist_xy2() const { return (pos_end[0] - pos_start[0]) * (pos_end[0] - pos_start[0]) + (pos_end[1] - pos_start[1]) * (pos_end[1] - pos_start[1]); } + float dist_xyz2() const { return (pos_end[0] - pos_start[0]) * (pos_end[0] - pos_start[0]) + (pos_end[1] - pos_start[1]) * (pos_end[1] - pos_start[1]) + (pos_end[2] - pos_start[2]) * (pos_end[2] - pos_start[2]); } + float dist_xy() const { return sqrt(dist_xy2()); } + float dist_xyz() const { return sqrt(dist_xyz2()); } + float dist_e() const { return fabs(pos_end[3] - pos_start[3]); } + + float feedrate() const { return pos_end[4]; } + float time() const { return dist_xyz() / feedrate(); } + float time_inv() const { return feedrate() / dist_xyz(); } + float volumetric_correction_avg() const { + float avg_correction = 0.5f * (volumetric_extrusion_rate_start + volumetric_extrusion_rate_end) / volumetric_extrusion_rate; + assert(avg_correction > 0.f); + assert(avg_correction <= 1.00000001f); + return avg_correction; + } + float time_corrected() const { return time() * volumetric_correction_avg(); } + + GCodeLineType type; + + // We try to keep the string buffer once it has been allocated, so it will not be reallocated over and over. + std::vector<char> raw; + size_t raw_length; + // If modified, the raw text has to be adapted by the new extrusion rate, + // or maybe the line needs to be split into multiple lines. + bool modified; + + // float timeStart; + // float timeEnd; + // X,Y,Z,E,F. Storing the state of the currently active extruder only. + float pos_start[5]; + float pos_end[5]; + // Was the axis found on the G-code line? X,Y,Z,F + bool pos_provided[5]; + + // Index of the active extruder. + size_t extruder_id; + // Extrusion role of this segment. + ExtrusionRole extrusion_role; + + // Current volumetric extrusion rate. + float volumetric_extrusion_rate; + // Volumetric extrusion rate at the start of this segment. + float volumetric_extrusion_rate_start; + // Volumetric extrusion rate at the end of this segment. + float volumetric_extrusion_rate_end; + + // Volumetric extrusion rate slope limiting this segment. + // If set to zero, the slope is unlimited. + float max_volumetric_extrusion_rate_slope_positive; + float max_volumetric_extrusion_rate_slope_negative; + }; + + // Circular buffer of GCode lines. The circular buffer size will be limited to circular_buffer_size. + std::vector<GCodeLine> circular_buffer; + // Current position of the circular buffer (index, where to write the next line to, the line has to be pushed out before it is overwritten). + size_t circular_buffer_pos; + // Circular buffer size, configuration value. + size_t circular_buffer_size; + // Number of valid lines in the circular buffer. Lower or equal to circular_buffer_size. + size_t circular_buffer_items; + + // Output buffer will only grow. It will not be reallocated over and over. + std::vector<char> output_buffer; + size_t output_buffer_length; + + // For debugging purposes. Index of the G-code line processed. + size_t line_idx; + + bool process_line(const char *line, const size_t len, GCodeLine &buf); + void output_gcode_line(GCodeLine &buf); + + // Go back from the current circular_buffer_pos and lower the feedtrate to decrease the slope of the extrusion rate changes. + // Then go forward and adjust the feedrate to decrease the slope of the extrusion rate changes. + void adjust_volumetric_rate(); + + // Push the text to the end of the output_buffer. + void push_to_output(const char *text, const size_t len, bool add_eol = true); + // Push an axis assignment to the end of the output buffer. + void push_axis_to_output(const char axis, const float value, bool add_eol = false); + // Push a G-code line to the output, + void push_line_to_output(const GCodeLine &line, const float new_feedrate, const char *comment); + + size_t circular_buffer_idx_head() const { + size_t idx = circular_buffer_pos + circular_buffer_size - circular_buffer_items; + if (idx >= circular_buffer_size) + idx -= circular_buffer_size; + return idx; + } + + size_t circular_buffer_idx_tail() const { return circular_buffer_pos; } + + size_t circular_buffer_idx_prev(size_t idx) const { + idx += circular_buffer_size - 1; + if (idx >= circular_buffer_size) + idx -= circular_buffer_size; + return idx; + } + + size_t circular_buffer_idx_next(size_t idx) const { + if (++ idx >= circular_buffer_size) + idx -= circular_buffer_size; + return idx; + } +}; + +} // namespace Slic3r + +#endif /* slic3r_GCode_PressureEqualizer_hpp_ */ diff --git a/src/libslic3r/GCode/PreviewData.cpp b/src/libslic3r/GCode/PreviewData.cpp new file mode 100644 index 000000000..9cf9716e0 --- /dev/null +++ b/src/libslic3r/GCode/PreviewData.cpp @@ -0,0 +1,456 @@ +#include "Analyzer.hpp" +#include "PreviewData.hpp" +#include <float.h> +#include <wx/intl.h> +#include <I18N.hpp> + +#include <boost/format.hpp> + +//! macro used to mark string used at localization, +#define L(s) (s) + +namespace Slic3r { + +const GCodePreviewData::Color GCodePreviewData::Color::Dummy(0.0f, 0.0f, 0.0f, 0.0f); + +GCodePreviewData::Color::Color() +{ + rgba[0] = 1.0f; + rgba[1] = 1.0f; + rgba[2] = 1.0f; + rgba[3] = 1.0f; +} + +GCodePreviewData::Color::Color(float r, float g, float b, float a) +{ + rgba[0] = r; + rgba[1] = g; + rgba[2] = b; + rgba[3] = a; +} + +std::vector<unsigned char> GCodePreviewData::Color::as_bytes() const +{ + std::vector<unsigned char> ret; + for (unsigned int i = 0; i < 4; ++i) + { + ret.push_back((unsigned char)(255.0f * rgba[i])); + } + return ret; +} + +GCodePreviewData::Extrusion::Layer::Layer(float z, const ExtrusionPaths& paths) + : z(z) + , paths(paths) +{ +} + +GCodePreviewData::Travel::Polyline::Polyline(EType type, EDirection direction, float feedrate, unsigned int extruder_id, const Polyline3& polyline) + : type(type) + , direction(direction) + , feedrate(feedrate) + , extruder_id(extruder_id) + , polyline(polyline) +{ +} + +const GCodePreviewData::Color GCodePreviewData::Range::Default_Colors[Colors_Count] = +{ + Color(0.043f, 0.173f, 0.478f, 1.0f), + Color(0.075f, 0.349f, 0.522f, 1.0f), + Color(0.110f, 0.533f, 0.569f, 1.0f), + Color(0.016f, 0.839f, 0.059f, 1.0f), + Color(0.667f, 0.949f, 0.000f, 1.0f), + Color(0.988f, 0.975f, 0.012f, 1.0f), + Color(0.961f, 0.808f, 0.039f, 1.0f), + Color(0.890f, 0.533f, 0.125f, 1.0f), + Color(0.820f, 0.408f, 0.188f, 1.0f), + Color(0.761f, 0.322f, 0.235f, 1.0f) +}; + +GCodePreviewData::Range::Range() +{ + reset(); +} + +void GCodePreviewData::Range::reset() +{ + min = FLT_MAX; + max = -FLT_MAX; +} + +bool GCodePreviewData::Range::empty() const +{ + return min == max; +} + +void GCodePreviewData::Range::update_from(float value) +{ + min = std::min(min, value); + max = std::max(max, value); +} + +void GCodePreviewData::Range::update_from(const Range& other) +{ + min = std::min(min, other.min); + max = std::max(max, other.max); +} + +void GCodePreviewData::Range::set_from(const Range& other) +{ + min = other.min; + max = other.max; +} + +float GCodePreviewData::Range::step_size() const +{ + return (max - min) / (float)(Colors_Count - 1); +} + +GCodePreviewData::Color GCodePreviewData::Range::get_color_at(float value) const +{ + if (empty()) + return Color::Dummy; + + float global_t = (value - min) / step_size(); + + unsigned int low = (unsigned int)global_t; + unsigned int high = clamp((unsigned int)0, Colors_Count - 1, low + 1); + + Color color_low = colors[low]; + Color color_high = colors[high]; + + float local_t = global_t - (float)low; + + // interpolate in RGB space + Color ret; + for (unsigned int i = 0; i < 4; ++i) + { + ret.rgba[i] = lerp(color_low.rgba[i], color_high.rgba[i], local_t); + } + return ret; +} + +GCodePreviewData::LegendItem::LegendItem(const std::string& text, const GCodePreviewData::Color& color) + : text(text) + , color(color) +{ +} + +const GCodePreviewData::Color GCodePreviewData::Extrusion::Default_Extrusion_Role_Colors[Num_Extrusion_Roles] = +{ + Color(0.0f, 0.0f, 0.0f, 1.0f), // erNone + Color(1.0f, 0.0f, 0.0f, 1.0f), // erPerimeter + Color(0.0f, 1.0f, 0.0f, 1.0f), // erExternalPerimeter + Color(0.0f, 0.0f, 1.0f, 1.0f), // erOverhangPerimeter + Color(1.0f, 1.0f, 0.0f, 1.0f), // erInternalInfill + Color(1.0f, 0.0f, 1.0f, 1.0f), // erSolidInfill + Color(0.0f, 1.0f, 1.0f, 1.0f), // erTopSolidInfill + Color(0.5f, 0.5f, 0.5f, 1.0f), // erBridgeInfill + Color(1.0f, 1.0f, 1.0f, 1.0f), // erGapFill + Color(0.5f, 0.0f, 0.0f, 1.0f), // erSkirt + Color(0.0f, 0.5f, 0.0f, 1.0f), // erSupportMaterial + Color(0.0f, 0.0f, 0.5f, 1.0f), // erSupportMaterialInterface + Color(0.7f, 0.89f, 0.67f, 1.0f), // erWipeTower + Color(1.0f, 1.0f, 0.0f, 1.0f), // erCustom + Color(0.0f, 0.0f, 0.0f, 1.0f) // erMixed +}; + +// todo: merge with Slic3r::ExtrusionRole2String() from GCode.cpp +const std::string GCodePreviewData::Extrusion::Default_Extrusion_Role_Names[Num_Extrusion_Roles] +{ + L("None"), + L("Perimeter"), + L("External perimeter"), + L("Overhang perimeter"), + L("Internal infill"), + L("Solid infill"), + L("Top solid infill"), + L("Bridge infill"), + L("Gap fill"), + L("Skirt"), + L("Support material"), + L("Support material interface"), + L("Wipe tower"), + L("Custom"), + L("Mixed") +}; + +const GCodePreviewData::Extrusion::EViewType GCodePreviewData::Extrusion::Default_View_Type = GCodePreviewData::Extrusion::FeatureType; + +void GCodePreviewData::Extrusion::set_default() +{ + view_type = Default_View_Type; + + ::memcpy((void*)role_colors, (const void*)Default_Extrusion_Role_Colors, Num_Extrusion_Roles * sizeof(Color)); + + for (unsigned int i = 0; i < Num_Extrusion_Roles; ++i) + { + role_names[i] = Default_Extrusion_Role_Names[i]; + } + + role_flags = 0; + for (unsigned int i = 0; i < Num_Extrusion_Roles; ++i) + { + role_flags |= 1 << i; + } +} + +bool GCodePreviewData::Extrusion::is_role_flag_set(ExtrusionRole role) const +{ + return is_role_flag_set(role_flags, role); +} + +bool GCodePreviewData::Extrusion::is_role_flag_set(unsigned int flags, ExtrusionRole role) +{ + return GCodeAnalyzer::is_valid_extrusion_role(role) && (flags & (1 << (role - erPerimeter))) != 0; +} + +const float GCodePreviewData::Travel::Default_Width = 0.075f; +const float GCodePreviewData::Travel::Default_Height = 0.075f; +const GCodePreviewData::Color GCodePreviewData::Travel::Default_Type_Colors[Num_Types] = +{ + Color(0.0f, 0.0f, 0.75f, 1.0f), // Move + Color(0.0f, 0.75f, 0.0f, 1.0f), // Extrude + Color(0.75f, 0.0f, 0.0f, 1.0f), // Retract +}; + +void GCodePreviewData::Travel::set_default() +{ + width = Default_Width; + height = Default_Height; + ::memcpy((void*)type_colors, (const void*)Default_Type_Colors, Num_Types * sizeof(Color)); + + is_visible = false; +} + +const GCodePreviewData::Color GCodePreviewData::Retraction::Default_Color = GCodePreviewData::Color(1.0f, 1.0f, 1.0f, 1.0f); + +GCodePreviewData::Retraction::Position::Position(const Vec3crd& position, float width, float height) + : position(position) + , width(width) + , height(height) +{ +} + +void GCodePreviewData::Retraction::set_default() +{ + color = Default_Color; + is_visible = false; +} + +void GCodePreviewData::Shell::set_default() +{ + is_visible = false; +} + +GCodePreviewData::GCodePreviewData() +{ + set_default(); +} + +void GCodePreviewData::set_default() +{ + ::memcpy((void*)ranges.height.colors, (const void*)Range::Default_Colors, Range::Colors_Count * sizeof(Color)); + ::memcpy((void*)ranges.width.colors, (const void*)Range::Default_Colors, Range::Colors_Count * sizeof(Color)); + ::memcpy((void*)ranges.feedrate.colors, (const void*)Range::Default_Colors, Range::Colors_Count * sizeof(Color)); + ::memcpy((void*)ranges.volumetric_rate.colors, (const void*)Range::Default_Colors, Range::Colors_Count * sizeof(Color)); + + extrusion.set_default(); + travel.set_default(); + retraction.set_default(); + unretraction.set_default(); + shell.set_default(); +} + +void GCodePreviewData::reset() +{ + ranges.width.reset(); + ranges.height.reset(); + ranges.feedrate.reset(); + ranges.volumetric_rate.reset(); + extrusion.layers.clear(); + travel.polylines.clear(); + retraction.positions.clear(); + unretraction.positions.clear(); +} + +bool GCodePreviewData::empty() const +{ + return extrusion.layers.empty() && travel.polylines.empty() && retraction.positions.empty() && unretraction.positions.empty(); +} + +GCodePreviewData::Color GCodePreviewData::get_extrusion_role_color(ExtrusionRole role) const +{ + return extrusion.role_colors[role]; +} + +GCodePreviewData::Color GCodePreviewData::get_height_color(float height) const +{ + return ranges.height.get_color_at(height); +} + +GCodePreviewData::Color GCodePreviewData::get_width_color(float width) const +{ + return ranges.width.get_color_at(width); +} + +GCodePreviewData::Color GCodePreviewData::get_feedrate_color(float feedrate) const +{ + return ranges.feedrate.get_color_at(feedrate); +} + +GCodePreviewData::Color GCodePreviewData::get_volumetric_rate_color(float rate) const +{ + return ranges.volumetric_rate.get_color_at(rate); +} + +void GCodePreviewData::set_extrusion_role_color(const std::string& role_name, float red, float green, float blue, float alpha) +{ + for (unsigned int i = 0; i < Extrusion::Num_Extrusion_Roles; ++i) + { + if (role_name == extrusion.role_names[i]) + { + extrusion.role_colors[i] = Color(red, green, blue, alpha); + break; + } + } +} + +void GCodePreviewData::set_extrusion_paths_colors(const std::vector<std::string>& colors) +{ + unsigned int size = (unsigned int)colors.size(); + + if (size % 2 != 0) + return; + + for (unsigned int i = 0; i < size; i += 2) + { + const std::string& color_str = colors[i + 1]; + + if (color_str.size() == 6) + { + bool valid = true; + for (int c = 0; c < 6; ++c) + { + if (::isxdigit(color_str[c]) == 0) + { + valid = false; + break; + } + } + + if (valid) + { + unsigned int color; + std::stringstream ss; + ss << std::hex << color_str; + ss >> color; + + float den = 1.0f / 255.0f; + + float r = (float)((color & 0xFF0000) >> 16) * den; + float g = (float)((color & 0x00FF00) >> 8) * den; + float b = (float)(color & 0x0000FF) * den; + + this->set_extrusion_role_color(colors[i], r, g, b, 1.0f); + } + } + } +} + +std::string GCodePreviewData::get_legend_title() const +{ + switch (extrusion.view_type) + { + case Extrusion::FeatureType: + return L("Feature type"); + case Extrusion::Height: + return L("Height (mm)"); + case Extrusion::Width: + return L("Width (mm)"); + case Extrusion::Feedrate: + return L("Speed (mm/s)"); + case Extrusion::VolumetricRate: + return L("Volumetric flow rate (mm3/s)"); + case Extrusion::Tool: + return L("Tool"); + } + + return ""; +} + +GCodePreviewData::LegendItemsList GCodePreviewData::get_legend_items(const std::vector<float>& tool_colors) const +{ + struct Helper + { + static void FillListFromRange(LegendItemsList& list, const Range& range, unsigned int decimals, float scale_factor) + { + list.reserve(Range::Colors_Count); + + float step = range.step_size(); + for (int i = Range::Colors_Count - 1; i >= 0; --i) + { + char buf[1024]; + sprintf(buf, "%.*f", decimals, scale_factor * (range.min + (float)i * step)); + list.emplace_back(buf, range.colors[i]); + } + } + }; + + LegendItemsList items; + + switch (extrusion.view_type) + { + case Extrusion::FeatureType: + { + ExtrusionRole first_valid = erPerimeter; + ExtrusionRole last_valid = erCustom; + + items.reserve(last_valid - first_valid + 1); + for (unsigned int i = (unsigned int)first_valid; i <= (unsigned int)last_valid; ++i) + { + items.emplace_back(Slic3r::I18N::translate(extrusion.role_names[i]), extrusion.role_colors[i]); + } + + break; + } + case Extrusion::Height: + { + Helper::FillListFromRange(items, ranges.height, 3, 1.0f); + break; + } + case Extrusion::Width: + { + Helper::FillListFromRange(items, ranges.width, 3, 1.0f); + break; + } + case Extrusion::Feedrate: + { + Helper::FillListFromRange(items, ranges.feedrate, 1, 1.0f); + break; + } + case Extrusion::VolumetricRate: + { + Helper::FillListFromRange(items, ranges.volumetric_rate, 3, 1.0f); + break; + } + case Extrusion::Tool: + { + unsigned int tools_colors_count = tool_colors.size() / 4; + items.reserve(tools_colors_count); + for (unsigned int i = 0; i < tools_colors_count; ++i) + { + GCodePreviewData::Color color; + ::memcpy((void*)color.rgba, (const void*)(tool_colors.data() + i * 4), 4 * sizeof(float)); + items.emplace_back((boost::format(Slic3r::I18N::translate(L("Extruder %d"))) % (i + 1)).str(), color); + } + + break; + } + } + + return items; +} + +} // namespace Slic3r diff --git a/src/libslic3r/GCode/PreviewData.hpp b/src/libslic3r/GCode/PreviewData.hpp new file mode 100644 index 000000000..ab74993f5 --- /dev/null +++ b/src/libslic3r/GCode/PreviewData.hpp @@ -0,0 +1,208 @@ +#ifndef slic3r_GCode_PreviewData_hpp_ +#define slic3r_GCode_PreviewData_hpp_ + +#include "../libslic3r.h" +#include "../ExtrusionEntity.hpp" + +#include "Point.hpp" + +namespace Slic3r { + +class GCodePreviewData +{ +public: + struct Color + { + float rgba[4]; + + Color(); + Color(float r, float g, float b, float a); + + std::vector<unsigned char> as_bytes() const; + + static const Color Dummy; + }; + + struct Range + { + static const unsigned int Colors_Count = 10; + static const Color Default_Colors[Colors_Count]; + + Color colors[Colors_Count]; + float min; + float max; + + Range(); + + void reset(); + bool empty() const; + void update_from(float value); + void update_from(const Range& other); + void set_from(const Range& other); + float step_size() const; + + Color get_color_at(float value) const; + }; + + struct Ranges + { + Range height; + Range width; + Range feedrate; + Range volumetric_rate; + }; + + struct LegendItem + { + std::string text; + Color color; + + LegendItem(const std::string& text, const Color& color); + }; + + typedef std::vector<LegendItem> LegendItemsList; + + struct Extrusion + { + enum EViewType : unsigned char + { + FeatureType, + Height, + Width, + Feedrate, + VolumetricRate, + Tool, + Num_View_Types + }; + + static const unsigned int Num_Extrusion_Roles = (unsigned int)erMixed + 1; + static const Color Default_Extrusion_Role_Colors[Num_Extrusion_Roles]; + static const std::string Default_Extrusion_Role_Names[Num_Extrusion_Roles]; + static const EViewType Default_View_Type; + + struct Layer + { + float z; + ExtrusionPaths paths; + + Layer(float z, const ExtrusionPaths& paths); + }; + + typedef std::vector<Layer> LayersList; + + EViewType view_type; + Color role_colors[Num_Extrusion_Roles]; + std::string role_names[Num_Extrusion_Roles]; + LayersList layers; + unsigned int role_flags; + + void set_default(); + bool is_role_flag_set(ExtrusionRole role) const; + + static bool is_role_flag_set(unsigned int flags, ExtrusionRole role); + }; + + struct Travel + { + enum EType : unsigned char + { + Move, + Extrude, + Retract, + Num_Types + }; + + static const float Default_Width; + static const float Default_Height; + static const Color Default_Type_Colors[Num_Types]; + + struct Polyline + { + enum EDirection + { + Vertical, + Generic, + Num_Directions + }; + + EType type; + EDirection direction; + float feedrate; + unsigned int extruder_id; + Polyline3 polyline; + + Polyline(EType type, EDirection direction, float feedrate, unsigned int extruder_id, const Polyline3& polyline); + }; + + typedef std::vector<Polyline> PolylinesList; + + PolylinesList polylines; + float width; + float height; + Color type_colors[Num_Types]; + bool is_visible; + + void set_default(); + }; + + struct Retraction + { + static const Color Default_Color; + + struct Position + { + Vec3crd position; + float width; + float height; + + Position(const Vec3crd& position, float width, float height); + }; + + typedef std::vector<Position> PositionsList; + + PositionsList positions; + Color color; + bool is_visible; + + void set_default(); + }; + + struct Shell + { + bool is_visible; + + void set_default(); + }; + + Extrusion extrusion; + Travel travel; + Retraction retraction; + Retraction unretraction; + Shell shell; + Ranges ranges; + + GCodePreviewData(); + + void set_default(); + void reset(); + bool empty() const; + + Color get_extrusion_role_color(ExtrusionRole role) const; + Color get_height_color(float height) const; + Color get_width_color(float width) const; + Color get_feedrate_color(float feedrate) const; + Color get_volumetric_rate_color(float rate) const; + + void set_extrusion_role_color(const std::string& role_name, float red, float green, float blue, float alpha); + void set_extrusion_paths_colors(const std::vector<std::string>& colors); + + std::string get_legend_title() const; + LegendItemsList get_legend_items(const std::vector<float>& tool_colors) const; +}; + +GCodePreviewData::Color operator + (const GCodePreviewData::Color& c1, const GCodePreviewData::Color& c2); +GCodePreviewData::Color operator * (float f, const GCodePreviewData::Color& color); + +} // namespace Slic3r + +#endif /* slic3r_GCode_PreviewData_hpp_ */ diff --git a/src/libslic3r/GCode/PrintExtents.cpp b/src/libslic3r/GCode/PrintExtents.cpp new file mode 100644 index 000000000..92a58fdf0 --- /dev/null +++ b/src/libslic3r/GCode/PrintExtents.cpp @@ -0,0 +1,186 @@ +// Calculate extents of the extrusions assigned to Print / PrintObject. +// The extents are used for assessing collisions of the print with the priming towers, +// to decide whether to pause the print after the priming towers are extruded +// to let the operator remove them from the print bed. + +#include "../BoundingBox.hpp" +#include "../ExtrusionEntity.hpp" +#include "../ExtrusionEntityCollection.hpp" +#include "../Print.hpp" + +#include "PrintExtents.hpp" +#include "WipeTower.hpp" + +namespace Slic3r { + +static inline BoundingBox extrusion_polyline_extents(const Polyline &polyline, const coord_t radius) +{ + BoundingBox bbox; + if (! polyline.points.empty()) + bbox.merge(polyline.points.front()); + for (const Point &pt : polyline.points) { + bbox.min(0) = std::min(bbox.min(0), pt(0) - radius); + bbox.min(1) = std::min(bbox.min(1), pt(1) - radius); + bbox.max(0) = std::max(bbox.max(0), pt(0) + radius); + bbox.max(1) = std::max(bbox.max(1), pt(1) + radius); + } + return bbox; +} + +static inline BoundingBoxf extrusionentity_extents(const ExtrusionPath &extrusion_path) +{ + BoundingBox bbox = extrusion_polyline_extents(extrusion_path.polyline, scale_(0.5 * extrusion_path.width)); + BoundingBoxf bboxf; + if (! empty(bbox)) { + bboxf.min = unscale(bbox.min); + bboxf.max = unscale(bbox.max); + bboxf.defined = true; + } + return bboxf; +} + +static inline BoundingBoxf extrusionentity_extents(const ExtrusionLoop &extrusion_loop) +{ + BoundingBox bbox; + for (const ExtrusionPath &extrusion_path : extrusion_loop.paths) + bbox.merge(extrusion_polyline_extents(extrusion_path.polyline, scale_(0.5 * extrusion_path.width))); + BoundingBoxf bboxf; + if (! empty(bbox)) { + bboxf.min = unscale(bbox.min); + bboxf.max = unscale(bbox.max); + bboxf.defined = true; + } + return bboxf; +} + +static inline BoundingBoxf extrusionentity_extents(const ExtrusionMultiPath &extrusion_multi_path) +{ + BoundingBox bbox; + for (const ExtrusionPath &extrusion_path : extrusion_multi_path.paths) + bbox.merge(extrusion_polyline_extents(extrusion_path.polyline, scale_(0.5 * extrusion_path.width))); + BoundingBoxf bboxf; + if (! empty(bbox)) { + bboxf.min = unscale(bbox.min); + bboxf.max = unscale(bbox.max); + bboxf.defined = true; + } + return bboxf; +} + +static BoundingBoxf extrusionentity_extents(const ExtrusionEntity *extrusion_entity); + +static inline BoundingBoxf extrusionentity_extents(const ExtrusionEntityCollection &extrusion_entity_collection) +{ + BoundingBoxf bbox; + for (const ExtrusionEntity *extrusion_entity : extrusion_entity_collection.entities) + bbox.merge(extrusionentity_extents(extrusion_entity)); + return bbox; +} + +static BoundingBoxf extrusionentity_extents(const ExtrusionEntity *extrusion_entity) +{ + if (extrusion_entity == nullptr) + return BoundingBoxf(); + auto *extrusion_path = dynamic_cast<const ExtrusionPath*>(extrusion_entity); + if (extrusion_path != nullptr) + return extrusionentity_extents(*extrusion_path); + auto *extrusion_loop = dynamic_cast<const ExtrusionLoop*>(extrusion_entity); + if (extrusion_loop != nullptr) + return extrusionentity_extents(*extrusion_loop); + auto *extrusion_multi_path = dynamic_cast<const ExtrusionMultiPath*>(extrusion_entity); + if (extrusion_multi_path != nullptr) + return extrusionentity_extents(*extrusion_multi_path); + auto *extrusion_entity_collection = dynamic_cast<const ExtrusionEntityCollection*>(extrusion_entity); + if (extrusion_entity_collection != nullptr) + return extrusionentity_extents(*extrusion_entity_collection); + throw std::runtime_error("Unexpected extrusion_entity type in extrusionentity_extents()"); + return BoundingBoxf(); +} + +BoundingBoxf get_print_extrusions_extents(const Print &print) +{ + BoundingBoxf bbox(extrusionentity_extents(print.brim())); + bbox.merge(extrusionentity_extents(print.skirt())); + return bbox; +} + +BoundingBoxf get_print_object_extrusions_extents(const PrintObject &print_object, const coordf_t max_print_z) +{ + BoundingBoxf bbox; + for (const Layer *layer : print_object.layers()) { + if (layer->print_z > max_print_z) + break; + BoundingBoxf bbox_this; + for (const LayerRegion *layerm : layer->regions()) { + bbox_this.merge(extrusionentity_extents(layerm->perimeters)); + for (const ExtrusionEntity *ee : layerm->fills.entities) + // fill represents infill extrusions of a single island. + bbox_this.merge(extrusionentity_extents(*dynamic_cast<const ExtrusionEntityCollection*>(ee))); + } + const SupportLayer *support_layer = dynamic_cast<const SupportLayer*>(layer); + if (support_layer) + for (const ExtrusionEntity *extrusion_entity : support_layer->support_fills.entities) + bbox_this.merge(extrusionentity_extents(extrusion_entity)); + for (const Point &offset : print_object.copies()) { + BoundingBoxf bbox_translated(bbox_this); + bbox_translated.translate(unscale(offset)); + bbox.merge(bbox_translated); + } + } + return bbox; +} + +// Returns a bounding box of a projection of the wipe tower for the layers <= max_print_z. +// The projection does not contain the priming regions. +BoundingBoxf get_wipe_tower_extrusions_extents(const Print &print, const coordf_t max_print_z) +{ + // Wipe tower extrusions are saved as if the tower was at the origin with no rotation + // We need to get position and angle of the wipe tower to transform them to actual position. + Transform2d trafo = + Eigen::Translation2d(print.config().wipe_tower_x.value, print.config().wipe_tower_y.value) * + Eigen::Rotation2Dd(print.config().wipe_tower_rotation_angle.value); + + BoundingBoxf bbox; + for (const std::vector<WipeTower::ToolChangeResult> &tool_changes : print.wipe_tower_data().tool_changes) { + if (! tool_changes.empty() && tool_changes.front().print_z > max_print_z) + break; + for (const WipeTower::ToolChangeResult &tcr : tool_changes) { + for (size_t i = 1; i < tcr.extrusions.size(); ++ i) { + const WipeTower::Extrusion &e = tcr.extrusions[i]; + if (e.width > 0) { + Vec2d delta = 0.5 * Vec2d(e.width, e.width); + Vec2d p1 = trafo * Vec2d((&e - 1)->pos.x, (&e - 1)->pos.y); + Vec2d p2 = trafo * Vec2d(e.pos.x, e.pos.y); + bbox.merge(p1.cwiseMin(p2) - delta); + bbox.merge(p1.cwiseMax(p2) + delta); + } + } + } + } + return bbox; +} + +// Returns a bounding box of the wipe tower priming extrusions. +BoundingBoxf get_wipe_tower_priming_extrusions_extents(const Print &print) +{ + BoundingBoxf bbox; + if (print.wipe_tower_data().priming != nullptr) { + const WipeTower::ToolChangeResult &tcr = *print.wipe_tower_data().priming; + for (size_t i = 1; i < tcr.extrusions.size(); ++ i) { + const WipeTower::Extrusion &e = tcr.extrusions[i]; + if (e.width > 0) { + Vec2d p1((&e - 1)->pos.x, (&e - 1)->pos.y); + Vec2d p2(e.pos.x, e.pos.y); + bbox.merge(p1); + coordf_t radius = 0.5 * e.width; + bbox.min(0) = std::min(bbox.min(0), std::min(p1(0), p2(0)) - radius); + bbox.min(1) = std::min(bbox.min(1), std::min(p1(1), p2(1)) - radius); + bbox.max(0) = std::max(bbox.max(0), std::max(p1(0), p2(0)) + radius); + bbox.max(1) = std::max(bbox.max(1), std::max(p1(1), p2(1)) + radius); + } + } + } + return bbox; +} + +} diff --git a/src/libslic3r/GCode/PrintExtents.hpp b/src/libslic3r/GCode/PrintExtents.hpp new file mode 100644 index 000000000..db507689d --- /dev/null +++ b/src/libslic3r/GCode/PrintExtents.hpp @@ -0,0 +1,30 @@ +// Measure extents of the planned extrusions. +// To be used for collision reporting. + +#ifndef slic3r_PrintExtents_hpp_ +#define slic3r_PrintExtents_hpp_ + +#include "libslic3r.h" + +namespace Slic3r { + +class Print; +class PrintObject; +class BoundingBoxf; + +// Returns a bounding box of a projection of the brim and skirt. +BoundingBoxf get_print_extrusions_extents(const Print &print); + +// Returns a bounding box of a projection of the object extrusions at z <= max_print_z. +BoundingBoxf get_print_object_extrusions_extents(const PrintObject &print_object, const coordf_t max_print_z); + +// Returns a bounding box of a projection of the wipe tower for the layers <= max_print_z. +// The projection does not contain the priming regions. +BoundingBoxf get_wipe_tower_extrusions_extents(const Print &print, const coordf_t max_print_z); + +// Returns a bounding box of the wipe tower priming extrusions. +BoundingBoxf get_wipe_tower_priming_extrusions_extents(const Print &print); + +}; + +#endif /* slic3r_PrintExtents_hpp_ */ diff --git a/src/libslic3r/GCode/SpiralVase.cpp b/src/libslic3r/GCode/SpiralVase.cpp new file mode 100644 index 000000000..8e8ae3075 --- /dev/null +++ b/src/libslic3r/GCode/SpiralVase.cpp @@ -0,0 +1,87 @@ +#include "SpiralVase.hpp" +#include "GCode.hpp" +#include <sstream> + +namespace Slic3r { + +std::string SpiralVase::process_layer(const std::string &gcode) +{ + /* This post-processor relies on several assumptions: + - all layers are processed through it, including those that are not supposed + to be transformed, in order to update the reader with the XY positions + - each call to this method includes a full layer, with a single Z move + at the beginning + - each layer is composed by suitable geometry (i.e. a single complete loop) + - loops were not clipped before calling this method */ + + // If we're not going to modify G-code, just feed it to the reader + // in order to update positions. + if (!this->enable) { + this->_reader.parse_buffer(gcode); + return gcode; + } + + // Get total XY length for this layer by summing all extrusion moves. + float total_layer_length = 0; + float layer_height = 0; + float z; + bool set_z = false; + + { + //FIXME Performance warning: This copies the GCodeConfig of the reader. + GCodeReader r = this->_reader; // clone + r.parse_buffer(gcode, [&total_layer_length, &layer_height, &z, &set_z] + (GCodeReader &reader, const GCodeReader::GCodeLine &line) { + if (line.cmd_is("G1")) { + if (line.extruding(reader)) { + total_layer_length += line.dist_XY(reader); + } else if (line.has(Z)) { + layer_height += line.dist_Z(reader); + if (!set_z) { + z = line.new_Z(reader); + set_z = true; + } + } + } + }); + } + + //Â Remove layer height from initial Z. + z -= layer_height; + + std::string new_gcode; + this->_reader.parse_buffer(gcode, [&new_gcode, &z, &layer_height, &total_layer_length] + (GCodeReader &reader, GCodeReader::GCodeLine line) { + if (line.cmd_is("G1")) { + if (line.has_z()) { + // If this is the initial Z move of the layer, replace it with a + // (redundant) move to the last Z of previous layer. + line.set(reader, Z, z); + new_gcode += line.raw() + '\n'; + return; + } else { + float dist_XY = line.dist_XY(reader); + if (dist_XY > 0) { + // horizontal move + if (line.extruding(reader)) { + z += dist_XY * layer_height / total_layer_length; + line.set(reader, Z, z); + new_gcode += line.raw() + '\n'; + } + return; + + /* Skip travel moves: the move to first perimeter point will + cause a visible seam when loops are not aligned in XY; by skipping + it we blend the first loop move in the XY plane (although the smoothness + of such blend depend on how long the first segment is; maybe we should + enforce some minimum length?). */ + } + } + } + new_gcode += line.raw() + '\n'; + }); + + return new_gcode; +} + +} diff --git a/src/libslic3r/GCode/SpiralVase.hpp b/src/libslic3r/GCode/SpiralVase.hpp new file mode 100644 index 000000000..60aa668d8 --- /dev/null +++ b/src/libslic3r/GCode/SpiralVase.hpp @@ -0,0 +1,28 @@ +#ifndef slic3r_SpiralVase_hpp_ +#define slic3r_SpiralVase_hpp_ + +#include "libslic3r.h" +#include "GCodeReader.hpp" + +namespace Slic3r { + +class SpiralVase { + public: + bool enable; + + SpiralVase(const PrintConfig &config) + : enable(false), _config(&config) + { + this->_reader.z() = this->_config->z_offset; + this->_reader.apply_config(*this->_config); + }; + std::string process_layer(const std::string &gcode); + + private: + const PrintConfig* _config; + GCodeReader _reader; +}; + +} + +#endif diff --git a/src/libslic3r/GCode/ToolOrdering.cpp b/src/libslic3r/GCode/ToolOrdering.cpp new file mode 100644 index 000000000..175b69447 --- /dev/null +++ b/src/libslic3r/GCode/ToolOrdering.cpp @@ -0,0 +1,631 @@ +#include "Print.hpp" +#include "ToolOrdering.hpp" + +// #define SLIC3R_DEBUG + +// Make assert active if SLIC3R_DEBUG +#ifdef SLIC3R_DEBUG + #define DEBUG + #define _DEBUG + #undef NDEBUG +#endif + +#include <cassert> +#include <limits> + +namespace Slic3r { + + +// Returns true in case that extruder a comes before b (b does not have to be present). False otherwise. +bool LayerTools::is_extruder_order(unsigned int a, unsigned int b) const +{ + if (a==b) + return false; + + for (auto extruder : extruders) { + if (extruder == a) + return true; + if (extruder == b) + return false; + } + + return false; +} + + +// For the use case when each object is printed separately +// (print.config().complete_objects is true). +ToolOrdering::ToolOrdering(const PrintObject &object, unsigned int first_extruder, bool prime_multi_material) +{ + if (object.layers().empty()) + return; + + // Initialize the print layers for just a single object. + { + std::vector<coordf_t> zs; + zs.reserve(zs.size() + object.layers().size() + object.support_layers().size()); + for (auto layer : object.layers()) + zs.emplace_back(layer->print_z); + for (auto layer : object.support_layers()) + zs.emplace_back(layer->print_z); + this->initialize_layers(zs); + } + + // Collect extruders reuqired to print the layers. + this->collect_extruders(object); + + // Reorder the extruders to minimize tool switches. + this->reorder_extruders(first_extruder); + + this->fill_wipe_tower_partitions(object.print()->config(), object.layers().front()->print_z - object.layers().front()->height); + + this->collect_extruder_statistics(prime_multi_material); +} + +// For the use case when all objects are printed at once. +// (print.config().complete_objects is false). +ToolOrdering::ToolOrdering(const Print &print, unsigned int first_extruder, bool prime_multi_material) +{ + m_print_config_ptr = &print.config(); + + PrintObjectPtrs objects = print.get_printable_objects(); + // Initialize the print layers for all objects and all layers. + coordf_t object_bottom_z = 0.; + { + std::vector<coordf_t> zs; + for (auto object : objects) { + zs.reserve(zs.size() + object->layers().size() + object->support_layers().size()); + for (auto layer : object->layers()) + zs.emplace_back(layer->print_z); + for (auto layer : object->support_layers()) + zs.emplace_back(layer->print_z); + if (! object->layers().empty()) + object_bottom_z = object->layers().front()->print_z - object->layers().front()->height; + } + this->initialize_layers(zs); + } + + // Collect extruders reuqired to print the layers. + for (auto object : objects) + this->collect_extruders(*object); + + // Reorder the extruders to minimize tool switches. + this->reorder_extruders(first_extruder); + + this->fill_wipe_tower_partitions(print.config(), object_bottom_z); + + this->collect_extruder_statistics(prime_multi_material); +} + + +LayerTools& ToolOrdering::tools_for_layer(coordf_t print_z) +{ + auto it_layer_tools = std::lower_bound(m_layer_tools.begin(), m_layer_tools.end(), LayerTools(print_z - EPSILON)); + assert(it_layer_tools != m_layer_tools.end()); + coordf_t dist_min = std::abs(it_layer_tools->print_z - print_z); + for (++ it_layer_tools; it_layer_tools != m_layer_tools.end(); ++it_layer_tools) { + coordf_t d = std::abs(it_layer_tools->print_z - print_z); + if (d >= dist_min) + break; + dist_min = d; + } + -- it_layer_tools; + assert(dist_min < EPSILON); + return *it_layer_tools; +} + +void ToolOrdering::initialize_layers(std::vector<coordf_t> &zs) +{ + sort_remove_duplicates(zs); + // Merge numerically very close Z values. + for (size_t i = 0; i < zs.size();) { + // Find the last layer with roughly the same print_z. + size_t j = i + 1; + coordf_t zmax = zs[i] + EPSILON; + for (; j < zs.size() && zs[j] <= zmax; ++ j) ; + // Assign an average print_z to the set of layers with nearly equal print_z. + m_layer_tools.emplace_back(LayerTools(0.5 * (zs[i] + zs[j-1]), m_print_config_ptr)); + i = j; + } +} + +// Collect extruders reuqired to print layers. +void ToolOrdering::collect_extruders(const PrintObject &object) +{ + // Collect the support extruders. + for (auto support_layer : object.support_layers()) { + LayerTools &layer_tools = this->tools_for_layer(support_layer->print_z); + ExtrusionRole role = support_layer->support_fills.role(); + bool has_support = role == erMixed || role == erSupportMaterial; + bool has_interface = role == erMixed || role == erSupportMaterialInterface; + unsigned int extruder_support = object.config().support_material_extruder.value; + unsigned int extruder_interface = object.config().support_material_interface_extruder.value; + if (has_support) + layer_tools.extruders.push_back(extruder_support); + if (has_interface) + layer_tools.extruders.push_back(extruder_interface); + if (has_support || has_interface) + layer_tools.has_support = true; + } + // Collect the object extruders. + for (auto layer : object.layers()) { + LayerTools &layer_tools = this->tools_for_layer(layer->print_z); + // What extruders are required to print this object layer? + for (size_t region_id = 0; region_id < object.print()->regions().size(); ++ region_id) { + const LayerRegion *layerm = (region_id < layer->regions().size()) ? layer->regions()[region_id] : nullptr; + if (layerm == nullptr) + continue; + const PrintRegion ®ion = *object.print()->regions()[region_id]; + + if (! layerm->perimeters.entities.empty()) { + bool something_nonoverriddable = true; + + if (m_print_config_ptr) { // in this case complete_objects is false (see ToolOrdering constructors) + something_nonoverriddable = false; + for (const auto& eec : layerm->perimeters.entities) // let's check if there are nonoverriddable entities + if (!layer_tools.wiping_extrusions().is_overriddable(dynamic_cast<const ExtrusionEntityCollection&>(*eec), *m_print_config_ptr, object, region)) { + something_nonoverriddable = true; + break; + } + } + + if (something_nonoverriddable) + layer_tools.extruders.push_back(region.config().perimeter_extruder.value); + + layer_tools.has_object = true; + } + + + bool has_infill = false; + bool has_solid_infill = false; + bool something_nonoverriddable = false; + for (const ExtrusionEntity *ee : layerm->fills.entities) { + // fill represents infill extrusions of a single island. + const auto *fill = dynamic_cast<const ExtrusionEntityCollection*>(ee); + ExtrusionRole role = fill->entities.empty() ? erNone : fill->entities.front()->role(); + if (is_solid_infill(role)) + has_solid_infill = true; + else if (role != erNone) + has_infill = true; + + if (m_print_config_ptr) { + if (!something_nonoverriddable && !layer_tools.wiping_extrusions().is_overriddable(*fill, *m_print_config_ptr, object, region)) + something_nonoverriddable = true; + } + } + + if (something_nonoverriddable || !m_print_config_ptr) + { + if (has_solid_infill) + layer_tools.extruders.push_back(region.config().solid_infill_extruder); + if (has_infill) + layer_tools.extruders.push_back(region.config().infill_extruder); + } + if (has_solid_infill || has_infill) + layer_tools.has_object = true; + } + } + + for (auto& layer : m_layer_tools) { + // Sort and remove duplicates + sort_remove_duplicates(layer.extruders); + + // make sure that there are some tools for each object layer (e.g. tall wiping object will result in empty extruders vector) + if (layer.extruders.empty() && layer.has_object) + layer.extruders.push_back(0); // 0="dontcare" extruder - it will be taken care of in reorder_extruders + } +} + +// Reorder extruders to minimize layer changes. +void ToolOrdering::reorder_extruders(unsigned int last_extruder_id) +{ + if (m_layer_tools.empty()) + return; + + if (last_extruder_id == (unsigned int)-1) { + // The initial print extruder has not been decided yet. + // Initialize the last_extruder_id with the first non-zero extruder id used for the print. + last_extruder_id = 0; + for (size_t i = 0; i < m_layer_tools.size() && last_extruder_id == 0; ++ i) { + const LayerTools < = m_layer_tools[i]; + for (unsigned int extruder_id : lt.extruders) + if (extruder_id > 0) { + last_extruder_id = extruder_id; + break; + } + } + if (last_extruder_id == 0) + // Nothing to extrude. + return; + } else + // 1 based index + ++ last_extruder_id; + + for (LayerTools < : m_layer_tools) { + if (lt.extruders.empty()) + continue; + if (lt.extruders.size() == 1 && lt.extruders.front() == 0) + lt.extruders.front() = last_extruder_id; + else { + if (lt.extruders.front() == 0) + // Pop the "don't care" extruder, the "don't care" region will be merged with the next one. + lt.extruders.erase(lt.extruders.begin()); + // Reorder the extruders to start with the last one. + for (size_t i = 1; i < lt.extruders.size(); ++ i) + if (lt.extruders[i] == last_extruder_id) { + // Move the last extruder to the front. + memmove(lt.extruders.data() + 1, lt.extruders.data(), i * sizeof(unsigned int)); + lt.extruders.front() = last_extruder_id; + break; + } + } + last_extruder_id = lt.extruders.back(); + } + + // Reindex the extruders, so they are zero based, not 1 based. + for (LayerTools < : m_layer_tools) + for (unsigned int &extruder_id : lt.extruders) { + assert(extruder_id > 0); + -- extruder_id; + } +} + + + +void ToolOrdering::fill_wipe_tower_partitions(const PrintConfig &config, coordf_t object_bottom_z) +{ + if (m_layer_tools.empty()) + return; + + // Count the minimum number of tool changes per layer. + size_t last_extruder = size_t(-1); + for (LayerTools < : m_layer_tools) { + lt.wipe_tower_partitions = lt.extruders.size(); + if (! lt.extruders.empty()) { + if (last_extruder == size_t(-1) || last_extruder == lt.extruders.front()) + // The first extruder on this layer is equal to the current one, no need to do an initial tool change. + -- lt.wipe_tower_partitions; + last_extruder = lt.extruders.back(); + } + } + + // Propagate the wipe tower partitions down to support the upper partitions by the lower partitions. + for (int i = int(m_layer_tools.size()) - 2; i >= 0; -- i) + m_layer_tools[i].wipe_tower_partitions = std::max(m_layer_tools[i + 1].wipe_tower_partitions, m_layer_tools[i].wipe_tower_partitions); + + //FIXME this is a hack to get the ball rolling. + for (LayerTools < : m_layer_tools) + lt.has_wipe_tower = (lt.has_object && lt.wipe_tower_partitions > 0) || lt.print_z < object_bottom_z + EPSILON; + + // Test for a raft, insert additional wipe tower layer to fill in the raft separation gap. + double max_layer_height = std::numeric_limits<double>::max(); + for (size_t i = 0; i < config.nozzle_diameter.values.size(); ++ i) { + double mlh = config.max_layer_height.values[i]; + if (mlh == 0.) + mlh = 0.75 * config.nozzle_diameter.values[i]; + max_layer_height = std::min(max_layer_height, mlh); + } + for (size_t i = 0; i + 1 < m_layer_tools.size(); ++ i) { + const LayerTools < = m_layer_tools[i]; + const LayerTools <_next = m_layer_tools[i + 1]; + if (lt.print_z < object_bottom_z + EPSILON && lt_next.print_z >= object_bottom_z + EPSILON) { + // lt is the last raft layer. Find the 1st object layer. + size_t j = i + 1; + for (; j < m_layer_tools.size() && ! m_layer_tools[j].has_wipe_tower; ++ j); + if (j < m_layer_tools.size()) { + const LayerTools <_object = m_layer_tools[j]; + coordf_t gap = lt_object.print_z - lt.print_z; + assert(gap > 0.f); + if (gap > max_layer_height + EPSILON) { + // Insert one additional wipe tower layer between lh.print_z and lt_object.print_z. + LayerTools lt_new(0.5f * (lt.print_z + lt_object.print_z)); + // Find the 1st layer above lt_new. + for (j = i + 1; j < m_layer_tools.size() && m_layer_tools[j].print_z < lt_new.print_z - EPSILON; ++ j); + if (std::abs(m_layer_tools[j].print_z - lt_new.print_z) < EPSILON) { + m_layer_tools[j].has_wipe_tower = true; + } else { + LayerTools <_extra = *m_layer_tools.insert(m_layer_tools.begin() + j, lt_new); + LayerTools <_prev = m_layer_tools[j - 1]; + LayerTools <_next = m_layer_tools[j + 1]; + assert(! lt_prev.extruders.empty() && ! lt_next.extruders.empty()); + assert(lt_prev.extruders.back() == lt_next.extruders.front()); + lt_extra.has_wipe_tower = true; + lt_extra.extruders.push_back(lt_next.extruders.front()); + lt_extra.wipe_tower_partitions = lt_next.wipe_tower_partitions; + } + } + } + break; + } + } + + // Calculate the wipe_tower_layer_height values. + coordf_t wipe_tower_print_z_last = 0.; + for (LayerTools < : m_layer_tools) + if (lt.has_wipe_tower) { + lt.wipe_tower_layer_height = lt.print_z - wipe_tower_print_z_last; + wipe_tower_print_z_last = lt.print_z; + } +} + +void ToolOrdering::collect_extruder_statistics(bool prime_multi_material) +{ + m_first_printing_extruder = (unsigned int)-1; + for (const auto < : m_layer_tools) + if (! lt.extruders.empty()) { + m_first_printing_extruder = lt.extruders.front(); + break; + } + + m_last_printing_extruder = (unsigned int)-1; + for (auto lt_it = m_layer_tools.rbegin(); lt_it != m_layer_tools.rend(); ++ lt_it) + if (! lt_it->extruders.empty()) { + m_last_printing_extruder = lt_it->extruders.back(); + break; + } + + m_all_printing_extruders.clear(); + for (const auto < : m_layer_tools) { + append(m_all_printing_extruders, lt.extruders); + sort_remove_duplicates(m_all_printing_extruders); + } + + 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. + m_all_printing_extruders.erase( + std::remove_if(m_all_printing_extruders.begin(), m_all_printing_extruders.end(), + [ this ](const unsigned int eid) { return eid == m_first_printing_extruder; }), + m_all_printing_extruders.end()); + m_all_printing_extruders.emplace_back(m_first_printing_extruder); + m_first_printing_extruder = m_all_printing_extruders.front(); + } +} + + + +// This function is called from Print::mark_wiping_extrusions and sets extruder this entity should be printed with (-1 .. as usual) +void WipingExtrusions::set_extruder_override(const ExtrusionEntity* entity, unsigned int copy_id, int extruder, unsigned int num_of_copies) +{ + something_overridden = true; + + auto entity_map_it = (entity_map.insert(std::make_pair(entity, std::vector<int>()))).first; // (add and) return iterator + auto& copies_vector = entity_map_it->second; + if (copies_vector.size() < num_of_copies) + copies_vector.resize(num_of_copies, -1); + + if (copies_vector[copy_id] != -1) + std::cout << "ERROR: Entity extruder overriden multiple times!!!\n"; // A debugging message - this must never happen. + + copies_vector[copy_id] = extruder; +} + + +// Finds first non-soluble extruder on the layer +int WipingExtrusions::first_nonsoluble_extruder_on_layer(const PrintConfig& print_config) const +{ + const LayerTools& lt = *m_layer_tools; + for (auto extruders_it = lt.extruders.begin(); extruders_it != lt.extruders.end(); ++extruders_it) + if (!print_config.filament_soluble.get_at(*extruders_it)) + return (*extruders_it); + + return (-1); +} + +// Finds last non-soluble extruder on the layer +int WipingExtrusions::last_nonsoluble_extruder_on_layer(const PrintConfig& print_config) const +{ + const LayerTools& lt = *m_layer_tools; + for (auto extruders_it = lt.extruders.rbegin(); extruders_it != lt.extruders.rend(); ++extruders_it) + if (!print_config.filament_soluble.get_at(*extruders_it)) + return (*extruders_it); + + return (-1); +} + + +// Decides whether this entity could be overridden +bool WipingExtrusions::is_overriddable(const ExtrusionEntityCollection& eec, const PrintConfig& print_config, const PrintObject& object, const PrintRegion& region) const +{ + if (print_config.filament_soluble.get_at(Print::get_extruder(eec, region))) + return false; + + if (object.config().wipe_into_objects) + return true; + + if (!region.config().wipe_into_infill || eec.role() != erInternalInfill) + return false; + + return true; +} + + +// Following function iterates through all extrusions on the layer, remembers those that could be used for wiping after toolchange +// and returns volume that is left to be wiped on the wipe tower. +float WipingExtrusions::mark_wiping_extrusions(const Print& print, unsigned int old_extruder, unsigned int new_extruder, float volume_to_wipe) +{ + const LayerTools& lt = *m_layer_tools; + const float min_infill_volume = 0.f; // ignore infill with smaller volume than this + + if (print.config().filament_soluble.get_at(old_extruder) || print.config().filament_soluble.get_at(new_extruder)) + return volume_to_wipe; // Soluble filament cannot be wiped in a random infill, neither the filament after it + + // we will sort objects so that dedicated for wiping are at the beginning: + PrintObjectPtrs object_list = print.get_printable_objects(); + std::sort(object_list.begin(), object_list.end(), [](const PrintObject* a, const PrintObject* b) { return a->config().wipe_into_objects; }); + + // We will now iterate through + // - first the dedicated objects to mark perimeters or infills (depending on infill_first) + // - second through the dedicated ones again to mark infills or perimeters (depending on infill_first) + // - then all the others to mark infills (in case that !infill_first, we must also check that the perimeter is finished already + // this is controlled by the following variable: + bool perimeters_done = false; + + for (int i=0 ; i<(int)object_list.size() + (perimeters_done ? 0 : 1); ++i) { + if (!perimeters_done && (i==(int)object_list.size() || !object_list[i]->config().wipe_into_objects)) { // we passed the last dedicated object in list + perimeters_done = true; + i=-1; // let's go from the start again + continue; + } + + const auto& object = object_list[i]; + + // Finds this layer: + auto this_layer_it = std::find_if(object->layers().begin(), object->layers().end(), [<](const Layer* lay) { return std::abs(lt.print_z - lay->print_z)<EPSILON; }); + if (this_layer_it == object->layers().end()) + continue; + const Layer* this_layer = *this_layer_it; + unsigned int num_of_copies = object->copies().size(); + + for (unsigned int copy = 0; copy < num_of_copies; ++copy) { // iterate through copies first, so that we mark neighbouring infills to minimize travel moves + + for (size_t region_id = 0; region_id < object->print()->regions().size(); ++ region_id) { + const auto& region = *object->print()->regions()[region_id]; + + if (!region.config().wipe_into_infill && !object->config().wipe_into_objects) + continue; + + + if ((!print.config().infill_first ? perimeters_done : !perimeters_done) || (!object->config().wipe_into_objects && region.config().wipe_into_infill)) { + for (const ExtrusionEntity* ee : this_layer->regions()[region_id]->fills.entities) { // iterate through all infill Collections + auto* fill = dynamic_cast<const ExtrusionEntityCollection*>(ee); + + if (!is_overriddable(*fill, print.config(), *object, region)) + continue; + + // What extruder would this normally be printed with? + unsigned int correct_extruder = Print::get_extruder(*fill, region); + + if (volume_to_wipe<=0) + continue; + + if (!object->config().wipe_into_objects && !print.config().infill_first && region.config().wipe_into_infill) + // In this case we must check that the original extruder is used on this layer before the one we are overridding + // (and the perimeters will be finished before the infill is printed): + if (!lt.is_extruder_order(region.config().perimeter_extruder - 1, new_extruder)) + continue; + + if ((!is_entity_overridden(fill, copy) && fill->total_volume() > min_infill_volume)) { // this infill will be used to wipe this extruder + set_extruder_override(fill, copy, new_extruder, num_of_copies); + volume_to_wipe -= fill->total_volume(); + } + } + } + + // Now the same for perimeters - see comments above for explanation: + if (object->config().wipe_into_objects && (print.config().infill_first ? perimeters_done : !perimeters_done)) + { + for (const ExtrusionEntity* ee : this_layer->regions()[region_id]->perimeters.entities) { + auto* fill = dynamic_cast<const ExtrusionEntityCollection*>(ee); + if (!is_overriddable(*fill, print.config(), *object, region)) + continue; + + if (volume_to_wipe<=0) + continue; + + if ((!is_entity_overridden(fill, copy) && fill->total_volume() > min_infill_volume)) { + set_extruder_override(fill, copy, new_extruder, num_of_copies); + volume_to_wipe -= fill->total_volume(); + } + } + } + } + } + } + return std::max(0.f, volume_to_wipe); +} + + + +// Called after all toolchanges on a layer were mark_infill_overridden. There might still be overridable entities, +// that were not actually overridden. If they are part of a dedicated object, printing them with the extruder +// they were initially assigned to might mean violating the perimeter-infill order. We will therefore go through +// them again and make sure we override it. +void WipingExtrusions::ensure_perimeters_infills_order(const Print& print) +{ + const LayerTools& lt = *m_layer_tools; + unsigned int first_nonsoluble_extruder = first_nonsoluble_extruder_on_layer(print.config()); + unsigned int last_nonsoluble_extruder = last_nonsoluble_extruder_on_layer(print.config()); + + PrintObjectPtrs printable_objects = print.get_printable_objects(); + for (const PrintObject* object : printable_objects) { + // Finds this layer: + auto this_layer_it = std::find_if(object->layers().begin(), object->layers().end(), [<](const Layer* lay) { return std::abs(lt.print_z - lay->print_z)<EPSILON; }); + if (this_layer_it == object->layers().end()) + continue; + const Layer* this_layer = *this_layer_it; + unsigned int num_of_copies = object->copies().size(); + + for (unsigned int copy = 0; copy < num_of_copies; ++copy) { // iterate through copies first, so that we mark neighbouring infills to minimize travel moves + for (size_t region_id = 0; region_id < object->print()->regions().size(); ++ region_id) { + const auto& region = *object->print()->regions()[region_id]; + + if (!region.config().wipe_into_infill && !object->config().wipe_into_objects) + continue; + + for (const ExtrusionEntity* ee : this_layer->regions()[region_id]->fills.entities) { // iterate through all infill Collections + auto* fill = dynamic_cast<const ExtrusionEntityCollection*>(ee); + + if (!is_overriddable(*fill, print.config(), *object, region) + || is_entity_overridden(fill, copy) ) + continue; + + // This infill could have been overridden but was not - unless we do something, it could be + // printed before its perimeter, or not be printed at all (in case its original extruder has + // not been added to LayerTools + // Either way, we will now force-override it with something suitable: + if (print.config().infill_first + || object->config().wipe_into_objects // in this case the perimeter is overridden, so we can override by the last one safely + || lt.is_extruder_order(region.config().perimeter_extruder - 1, last_nonsoluble_extruder // !infill_first, but perimeter is already printed when last extruder prints + || std::find(lt.extruders.begin(), lt.extruders.end(), region.config().infill_extruder - 1) == lt.extruders.end()) // we have to force override - this could violate infill_first (FIXME) + ) + set_extruder_override(fill, copy, (print.config().infill_first ? first_nonsoluble_extruder : last_nonsoluble_extruder), num_of_copies); + else { + // In this case we can (and should) leave it to be printed normally. + // Force overriding would mean it gets printed before its perimeter. + } + } + + // Now the same for perimeters - see comments above for explanation: + for (const ExtrusionEntity* ee : this_layer->regions()[region_id]->perimeters.entities) { // iterate through all perimeter Collections + auto* fill = dynamic_cast<const ExtrusionEntityCollection*>(ee); + if (!is_overriddable(*fill, print.config(), *object, region) + || is_entity_overridden(fill, copy) ) + continue; + + set_extruder_override(fill, copy, (print.config().infill_first ? last_nonsoluble_extruder : first_nonsoluble_extruder), num_of_copies); + } + } + } + } +} + + + + + + + +// Following function is called from process_layer and returns pointer to vector with information about which extruders should be used for given copy of this entity. +// It first makes sure the pointer is valid (creates the vector if it does not exist) and contains a record for each copy +// It also modifies the vector in place and changes all -1 to correct_extruder_id (at the time the overrides were created, correct extruders were not known, +// so -1 was used as "print as usual". +// The resulting vector has to keep track of which extrusions are the ones that were overridden and which were not. In the extruder is used as overridden, +// its number is saved as it is (zero-based index). Usual extrusions are saved as -number-1 (unfortunately there is no negative zero). +const std::vector<int>* WipingExtrusions::get_extruder_overrides(const ExtrusionEntity* entity, int correct_extruder_id, int num_of_copies) +{ + auto entity_map_it = entity_map.find(entity); + if (entity_map_it == entity_map.end()) + entity_map_it = (entity_map.insert(std::make_pair(entity, std::vector<int>()))).first; + + // Now the entity_map_it should be valid, let's make sure the vector is long enough: + entity_map_it->second.resize(num_of_copies, -1); + + // Each -1 now means "print as usual" - we will replace it with actual extruder id (shifted it so we don't lose that information): + std::replace(entity_map_it->second.begin(), entity_map_it->second.end(), -1, -correct_extruder_id-1); + + return &(entity_map_it->second); +} + + +} // namespace Slic3r diff --git a/src/libslic3r/GCode/ToolOrdering.hpp b/src/libslic3r/GCode/ToolOrdering.hpp new file mode 100644 index 000000000..4dcf6516a --- /dev/null +++ b/src/libslic3r/GCode/ToolOrdering.hpp @@ -0,0 +1,162 @@ +// Ordering of the tools to minimize tool switches. + +#ifndef slic3r_ToolOrdering_hpp_ +#define slic3r_ToolOrdering_hpp_ + +#include "libslic3r.h" + +namespace Slic3r { + +class Print; +class PrintObject; +class LayerTools; + + + +// Object of this class holds information about whether an extrusion is printed immediately +// after a toolchange (as part of infill/perimeter wiping) or not. One extrusion can be a part +// of several copies - this has to be taken into account. +class WipingExtrusions +{ +public: + bool is_anything_overridden() const { // if there are no overrides, all the agenda can be skipped - this function can tell us if that's the case + return something_overridden; + } + + // This is called from GCode::process_layer - see implementation for further comments: + const std::vector<int>* get_extruder_overrides(const ExtrusionEntity* entity, int correct_extruder_id, int num_of_copies); + + // This function goes through all infill entities, decides which ones will be used for wiping and + // marks them by the extruder id. Returns volume that remains to be wiped on the wipe tower: + float mark_wiping_extrusions(const Print& print, unsigned int old_extruder, unsigned int new_extruder, float volume_to_wipe); + + void ensure_perimeters_infills_order(const Print& print); + + bool is_overriddable(const ExtrusionEntityCollection& ee, const PrintConfig& print_config, const PrintObject& object, const PrintRegion& region) const; + + void set_layer_tools_ptr(const LayerTools* lt) { m_layer_tools = lt; } + +private: + int first_nonsoluble_extruder_on_layer(const PrintConfig& print_config) const; + int last_nonsoluble_extruder_on_layer(const PrintConfig& print_config) const; + + // This function is called from mark_wiping_extrusions and sets extruder that it should be printed with (-1 .. as usual) + void set_extruder_override(const ExtrusionEntity* entity, unsigned int copy_id, int extruder, unsigned int num_of_copies); + + // Returns true in case that entity is not printed with its usual extruder for a given copy: + bool is_entity_overridden(const ExtrusionEntity* entity, int copy_id) const { + return (entity_map.find(entity) == entity_map.end() ? false : entity_map.at(entity).at(copy_id) != -1); + } + + std::map<const ExtrusionEntity*, std::vector<int>> entity_map; // to keep track of who prints what + bool something_overridden = false; + const LayerTools* m_layer_tools; // so we know which LayerTools object this belongs to +}; + + + +class LayerTools +{ +public: + LayerTools(const coordf_t z, const PrintConfig* print_config_ptr = nullptr) : + print_z(z), + has_object(false), + has_support(false), + has_wipe_tower(false), + wipe_tower_partitions(0), + wipe_tower_layer_height(0.) {} + + // Changing these operators to epsilon version can make a problem in cases where support and object layers get close to each other. + // In case someone tries to do it, make sure you know what you're doing and test it properly (slice multiple objects at once with supports). + bool operator< (const LayerTools &rhs) const { return print_z < rhs.print_z; } + bool operator==(const LayerTools &rhs) const { return print_z == rhs.print_z; } + + bool is_extruder_order(unsigned int a, unsigned int b) const; + + coordf_t print_z; + bool has_object; + bool has_support; + // Zero based extruder IDs, ordered to minimize tool switches. + std::vector<unsigned int> extruders; + // Will there be anything extruded on this layer for the wipe tower? + // Due to the support layers possibly interleaving the object layers, + // wipe tower will be disabled for some support only layers. + bool has_wipe_tower; + // Number of wipe tower partitions to support the required number of tool switches + // and to support the wipe tower partitions above this one. + size_t wipe_tower_partitions; + coordf_t wipe_tower_layer_height; + + WipingExtrusions& wiping_extrusions() { + m_wiping_extrusions.set_layer_tools_ptr(this); + return m_wiping_extrusions; + } + +private: + // This object holds list of extrusion that will be used for extruder wiping + WipingExtrusions m_wiping_extrusions; +}; + + + +class ToolOrdering +{ +public: + ToolOrdering() {} + + // For the use case when each object is printed separately + // (print.config.complete_objects is true). + ToolOrdering(const PrintObject &object, unsigned int first_extruder = (unsigned int)-1, bool prime_multi_material = false); + + // For the use case when all objects are printed at once. + // (print.config.complete_objects is false). + ToolOrdering(const Print &print, unsigned int first_extruder = (unsigned int)-1, bool prime_multi_material = false); + + void clear() { m_layer_tools.clear(); } + + // Get the first extruder printing, including the extruder priming areas, returns -1 if there is no layer printed. + unsigned int first_extruder() const { return m_first_printing_extruder; } + + // Get the first extruder printing the layer_tools, returns -1 if there is no layer printed. + unsigned int last_extruder() const { return m_last_printing_extruder; } + + // For a multi-material print, the printing extruders are ordered in the order they shall be primed. + const std::vector<unsigned int>& all_extruders() const { return m_all_printing_extruders; } + + // Find LayerTools with the closest print_z. + LayerTools& tools_for_layer(coordf_t print_z); + const LayerTools& tools_for_layer(coordf_t print_z) const + { return *const_cast<const LayerTools*>(&const_cast<const ToolOrdering*>(this)->tools_for_layer(print_z)); } + + const LayerTools& front() const { return m_layer_tools.front(); } + const LayerTools& back() const { return m_layer_tools.back(); } + std::vector<LayerTools>::const_iterator begin() const { return m_layer_tools.begin(); } + std::vector<LayerTools>::const_iterator end() const { return m_layer_tools.end(); } + bool empty() const { return m_layer_tools.empty(); } + std::vector<LayerTools>& layer_tools() { return m_layer_tools; } + bool has_wipe_tower() const { return ! m_layer_tools.empty() && m_first_printing_extruder != (unsigned int)-1 && m_layer_tools.front().wipe_tower_partitions > 0; } + +private: + void initialize_layers(std::vector<coordf_t> &zs); + void collect_extruders(const PrintObject &object); + void reorder_extruders(unsigned int last_extruder_id); + void fill_wipe_tower_partitions(const PrintConfig &config, coordf_t object_bottom_z); + void collect_extruder_statistics(bool prime_multi_material); + + std::vector<LayerTools> m_layer_tools; + // First printing extruder, including the multi-material priming sequence. + unsigned int m_first_printing_extruder = (unsigned int)-1; + // Final printing extruder. + unsigned int m_last_printing_extruder = (unsigned int)-1; + // All extruders, which extrude some material over m_layer_tools. + std::vector<unsigned int> m_all_printing_extruders; + + + const PrintConfig* m_print_config_ptr = nullptr; +}; + + + +} // namespace SLic3r + +#endif /* slic3r_ToolOrdering_hpp_ */ diff --git a/src/libslic3r/GCode/WipeTower.hpp b/src/libslic3r/GCode/WipeTower.hpp new file mode 100644 index 000000000..5cbbc1ca9 --- /dev/null +++ b/src/libslic3r/GCode/WipeTower.hpp @@ -0,0 +1,167 @@ +#ifndef slic3r_WipeTower_hpp_ +#define slic3r_WipeTower_hpp_ + +#include <utility> +#include <string> +#include <vector> + +namespace Slic3r +{ + +// A pure virtual WipeTower definition. +class WipeTower +{ +public: + // Internal point class, to make the wipe tower independent from other slic3r modules. + // This is important for Prusa Research as we want to build the wipe tower post-processor independently from slic3r. + struct xy + { + xy(float x = 0.f, float y = 0.f) : x(x), y(y) {} + xy(const xy& pos,float xp,float yp) : x(pos.x+xp), y(pos.y+yp) {} + xy operator+(const xy &rhs) const { xy out(*this); out.x += rhs.x; out.y += rhs.y; return out; } + xy operator-(const xy &rhs) const { xy out(*this); out.x -= rhs.x; out.y -= rhs.y; return out; } + xy& operator+=(const xy &rhs) { x += rhs.x; y += rhs.y; return *this; } + xy& operator-=(const xy &rhs) { x -= rhs.x; y -= rhs.y; return *this; } + bool operator==(const xy &rhs) const { return x == rhs.x && y == rhs.y; } + bool operator!=(const xy &rhs) const { return x != rhs.x || y != rhs.y; } + + // Rotate the point around center of the wipe tower about given angle (in degrees) + xy rotate(float width, float depth, float angle) const { + xy out(0,0); + float temp_x = x - width / 2.f; + float temp_y = y - depth / 2.f; + angle *= float(M_PI/180.); + out.x += temp_x * cos(angle) - temp_y * sin(angle) + width / 2.f; + out.y += temp_x * sin(angle) + temp_y * cos(angle) + depth / 2.f; + return out; + } + + // Rotate the point around origin about given angle in degrees + void rotate(float angle) { + float temp_x = x * cos(angle) - y * sin(angle); + y = x * sin(angle) + y * cos(angle); + x = temp_x; + } + + void translate(const xy& vect) { + x += vect.x; + y += vect.y; + } + + float x; + float y; + }; + + WipeTower() {} + virtual ~WipeTower() {} + + // Return the wipe tower position. + virtual const xy& position() const = 0; + + // Return the wipe tower width. + virtual float width() const = 0; + + // The wipe tower is finished, there should be no more tool changes or wipe tower prints. + virtual bool finished() const = 0; + + // Switch to a next layer. + virtual void set_layer( + // Print height of this layer. + float print_z, + // Layer height, used to calculate extrusion the rate. + float layer_height, + // Maximum number of tool changes on this layer or the layers below. + size_t max_tool_changes, + // Is this the first layer of the print? In that case print the brim first. + bool is_first_layer, + // Is this the last layer of the wipe tower? + bool is_last_layer) = 0; + + enum Purpose { + PURPOSE_MOVE_TO_TOWER, + PURPOSE_EXTRUDE, + PURPOSE_MOVE_TO_TOWER_AND_EXTRUDE, + }; + + // Extrusion path of the wipe tower, for 3D preview of the generated tool paths. + struct Extrusion + { + Extrusion(const xy &pos, float width, unsigned int tool) : pos(pos), width(width), tool(tool) {} + // End position of this extrusion. + xy pos; + // Width of a squished extrusion, corrected for the roundings of the squished extrusions. + // This is left zero if it is a travel move. + float width; + // Current extruder index. + unsigned int tool; + }; + + struct ToolChangeResult + { + // Print heigh of this tool change. + float print_z; + float layer_height; + // G-code section to be directly included into the output G-code. + std::string gcode; + // For path preview. + std::vector<Extrusion> extrusions; + // Initial position, at which the wipe tower starts its action. + // At this position the extruder is loaded and there is no Z-hop applied. + xy start_pos; + // Last point, at which the normal G-code generator of Slic3r shall continue. + // At this position the extruder is loaded and there is no Z-hop applied. + xy end_pos; + // Time elapsed over this tool change. + // This is useful not only for the print time estimation, but also for the control of layer cooling. + float elapsed_time; + + // Is this a priming extrusion? (If so, the wipe tower rotation & translation will not be applied later) + bool priming; + + // Sum the total length of the extrusion. + float total_extrusion_length_in_plane() { + float e_length = 0.f; + for (size_t i = 1; i < this->extrusions.size(); ++ i) { + const Extrusion &e = this->extrusions[i]; + if (e.width > 0) { + xy v = e.pos - (&e - 1)->pos; + e_length += sqrt(v.x*v.x+v.y*v.y); + } + } + return e_length; + } + }; + + // Returns gcode to prime the nozzles at the front edge of the print bed. + virtual ToolChangeResult prime( + // print_z of the first layer. + float first_layer_height, + // Extruder indices, in the order to be primed. The last extruder will later print the wipe tower brim, print brim and the object. + const std::vector<unsigned int> &tools, + // If true, the last priming are will be the same as the other priming areas, and the rest of the wipe will be performed inside the wipe tower. + // If false, the last priming are will be large enough to wipe the last extruder sufficiently. + bool last_wipe_inside_wipe_tower) = 0; + + // Returns gcode for toolchange and the end position. + // if new_tool == -1, just unload the current filament over the wipe tower. + virtual ToolChangeResult tool_change(unsigned int new_tool, bool last_in_layer) = 0; + + // Close the current wipe tower layer with a perimeter and possibly fill the unfilled space with a zig-zag. + // Call this method only if layer_finished() is false. + virtual ToolChangeResult finish_layer() = 0; + + // Is the current layer finished? A layer is finished if either the wipe tower is finished, or + // the wipe tower has been completely covered by the tool change extrusions, + // or the rest of the tower has been filled by a sparse infill with the finish_layer() method. + virtual bool layer_finished() const = 0; + + // Returns used filament length per extruder: + virtual std::vector<float> get_used_filament() const = 0; + + // Returns total number of toolchanges: + virtual int get_number_of_toolchanges() const = 0; +}; + +}; // namespace Slic3r + +#endif /* slic3r_WipeTower_hpp_ */ diff --git a/src/libslic3r/GCode/WipeTowerPrusaMM.cpp b/src/libslic3r/GCode/WipeTowerPrusaMM.cpp new file mode 100644 index 000000000..54bdfdfd6 --- /dev/null +++ b/src/libslic3r/GCode/WipeTowerPrusaMM.cpp @@ -0,0 +1,1258 @@ +/* + +TODO LIST +--------- + +1. cooling moves - DONE +2. account for perimeter and finish_layer extrusions and subtract it from last wipe - DONE +3. priming extrusions (last wipe must clear the color) - DONE +4. Peter's wipe tower - layer's are not exactly square +5. Peter's wipe tower - variable width for higher levels +6. Peter's wipe tower - make sure it is not too sparse (apply max_bridge_distance and make last wipe longer) +7. Peter's wipe tower - enable enhanced first layer adhesion + +*/ + +#include "WipeTowerPrusaMM.hpp" + +#include <assert.h> +#include <math.h> +#include <iostream> +#include <vector> +#include <numeric> + +#include "Analyzer.hpp" + +#if defined(__linux) || defined(__GNUC__ ) +#include <strings.h> +#endif /* __linux */ + +#ifdef _MSC_VER +#define strcasecmp _stricmp +#endif + + +namespace Slic3r +{ + +namespace PrusaMultiMaterial { + +class Writer +{ +public: + Writer(float layer_height, float line_width) : + m_current_pos(std::numeric_limits<float>::max(), std::numeric_limits<float>::max()), + m_current_z(0.f), + m_current_feedrate(0.f), + m_layer_height(layer_height), + m_extrusion_flow(0.f), + m_preview_suppressed(false), + m_elapsed_time(0.f), + m_default_analyzer_line_width(line_width) + { + // adds tag for analyzer: + char buf[64]; + sprintf(buf, ";%s%f\n", GCodeAnalyzer::Height_Tag.c_str(), m_layer_height); // don't rely on GCodeAnalyzer knowing the layer height - it knows nothing at priming + m_gcode += buf; + sprintf(buf, ";%s%d\n", GCodeAnalyzer::Extrusion_Role_Tag.c_str(), erWipeTower); + m_gcode += buf; + change_analyzer_line_width(line_width); + } + + Writer& change_analyzer_line_width(float line_width) { + // adds tag for analyzer: + char buf[64]; + sprintf(buf, ";%s%f\n", GCodeAnalyzer::Width_Tag.c_str(), line_width); + m_gcode += buf; + return *this; + } + + Writer& set_initial_position(const WipeTower::xy &pos, float width = 0.f, float depth = 0.f, float internal_angle = 0.f) { + m_wipe_tower_width = width; + m_wipe_tower_depth = depth; + m_internal_angle = internal_angle; + m_start_pos = WipeTower::xy(pos,0.f,m_y_shift).rotate(m_wipe_tower_width, m_wipe_tower_depth, m_internal_angle); + m_current_pos = pos; + return *this; + } + + Writer& set_initial_tool(const unsigned int tool) { m_current_tool = tool; return *this; } + + Writer& set_z(float z) + { m_current_z = z; return *this; } + + Writer& set_extrusion_flow(float flow) + { m_extrusion_flow = flow; return *this; } + + Writer& set_y_shift(float shift) { + m_current_pos.y -= shift-m_y_shift; + m_y_shift = shift; + return (*this); + } + + // Suppress / resume G-code preview in Slic3r. Slic3r will have difficulty to differentiate the various + // filament loading and cooling moves from normal extrusion moves. Therefore the writer + // is asked to suppres output of some lines, which look like extrusions. + Writer& suppress_preview() { change_analyzer_line_width(0.f); m_preview_suppressed = true; return *this; } + Writer& resume_preview() { change_analyzer_line_width(m_default_analyzer_line_width); m_preview_suppressed = false; return *this; } + + Writer& feedrate(float f) + { + if (f != m_current_feedrate) + m_gcode += "G1" + set_format_F(f) + "\n"; + return *this; + } + + const std::string& gcode() const { return m_gcode; } + const std::vector<WipeTower::Extrusion>& extrusions() const { return m_extrusions; } + float x() const { return m_current_pos.x; } + float y() const { return m_current_pos.y; } + const WipeTower::xy& pos() const { return m_current_pos; } + const WipeTower::xy start_pos_rotated() const { return m_start_pos; } + const WipeTower::xy pos_rotated() const { return WipeTower::xy(m_current_pos, 0.f, m_y_shift).rotate(m_wipe_tower_width, m_wipe_tower_depth, m_internal_angle); } + float elapsed_time() const { return m_elapsed_time; } + float get_and_reset_used_filament_length() { float temp = m_used_filament_length; m_used_filament_length = 0.f; return temp; } + + // Extrude with an explicitely provided amount of extrusion. + Writer& extrude_explicit(float x, float y, float e, float f = 0.f, bool record_length = false) + { + if (x == m_current_pos.x && y == m_current_pos.y && e == 0.f && (f == 0.f || f == m_current_feedrate)) + // Neither extrusion nor a travel move. + return *this; + + float dx = x - m_current_pos.x; + float dy = y - m_current_pos.y; + double len = sqrt(dx*dx+dy*dy); + if (record_length) + m_used_filament_length += e; + + + // Now do the "internal rotation" with respect to the wipe tower center + WipeTower::xy rotated_current_pos(WipeTower::xy(m_current_pos,0.f,m_y_shift).rotate(m_wipe_tower_width, m_wipe_tower_depth, m_internal_angle)); // this is where we are + WipeTower::xy rot(WipeTower::xy(x,y+m_y_shift).rotate(m_wipe_tower_width, m_wipe_tower_depth, m_internal_angle)); // this is where we want to go + + if (! m_preview_suppressed && e > 0.f && len > 0.) { + // Width of a squished extrusion, corrected for the roundings of the squished extrusions. + // This is left zero if it is a travel move. + float width = float(double(e) * /*Filament_Area*/2.40528 / (len * m_layer_height)); + // Correct for the roundings of a squished extrusion. + width += m_layer_height * float(1. - M_PI / 4.); + if (m_extrusions.empty() || m_extrusions.back().pos != rotated_current_pos) + m_extrusions.emplace_back(WipeTower::Extrusion(rotated_current_pos, 0, m_current_tool)); + m_extrusions.emplace_back(WipeTower::Extrusion(WipeTower::xy(rot.x, rot.y), width, m_current_tool)); + } + + m_gcode += "G1"; + if (std::abs(rot.x - rotated_current_pos.x) > EPSILON) + m_gcode += set_format_X(rot.x); + + if (std::abs(rot.y - rotated_current_pos.y) > EPSILON) + m_gcode += set_format_Y(rot.y); + + + if (e != 0.f) + m_gcode += set_format_E(e); + + if (f != 0.f && f != m_current_feedrate) + m_gcode += set_format_F(f); + + m_current_pos.x = x; + m_current_pos.y = y; + + // Update the elapsed time with a rough estimate. + m_elapsed_time += ((len == 0) ? std::abs(e) : len) / m_current_feedrate * 60.f; + m_gcode += "\n"; + return *this; + } + + Writer& extrude_explicit(const WipeTower::xy &dest, float e, float f = 0.f, bool record_length = false) + { return extrude_explicit(dest.x, dest.y, e, f, record_length); } + + // Travel to a new XY position. f=0 means use the current value. + Writer& travel(float x, float y, float f = 0.f) + { return extrude_explicit(x, y, 0.f, f); } + + Writer& travel(const WipeTower::xy &dest, float f = 0.f) + { return extrude_explicit(dest.x, dest.y, 0.f, f); } + + // Extrude a line from current position to x, y with the extrusion amount given by m_extrusion_flow. + Writer& extrude(float x, float y, float f = 0.f) + { + float dx = x - m_current_pos.x; + float dy = y - m_current_pos.y; + return extrude_explicit(x, y, sqrt(dx*dx+dy*dy) * m_extrusion_flow, f, true); + } + + Writer& extrude(const WipeTower::xy &dest, const float f = 0.f) + { return extrude(dest.x, dest.y, f); } + + Writer& rectangle(const WipeTower::xy& ld,float width,float height,const float f = 0.f) + { + WipeTower::xy corners[4]; + corners[0] = ld; + corners[1] = WipeTower::xy(ld,width,0.f); + corners[2] = WipeTower::xy(ld,width,height); + corners[3] = WipeTower::xy(ld,0.f,height); + int index_of_closest = 0; + if (x()-ld.x > ld.x+width-x()) // closer to the right + index_of_closest = 1; + if (y()-ld.y > ld.y+height-y()) // closer to the top + index_of_closest = (index_of_closest==0 ? 3 : 2); + + travel(corners[index_of_closest].x, y()); // travel to the closest corner + travel(x(),corners[index_of_closest].y); + + int i = index_of_closest; + do { + ++i; + if (i==4) i=0; + extrude(corners[i], f); + } while (i != index_of_closest); + return (*this); + } + + Writer& load(float e, float f = 0.f) + { + if (e == 0.f && (f == 0.f || f == m_current_feedrate)) + return *this; + m_gcode += "G1"; + if (e != 0.f) + m_gcode += set_format_E(e); + if (f != 0.f && f != m_current_feedrate) + m_gcode += set_format_F(f); + m_gcode += "\n"; + return *this; + } + + // Derectract while moving in the X direction. + // If |x| > 0, the feed rate relates to the x distance, + // otherwise the feed rate relates to the e distance. + Writer& load_move_x(float x, float e, float f = 0.f) + { return extrude_explicit(x, m_current_pos.y, e, f); } + + Writer& retract(float e, float f = 0.f) + { return load(-e, f); } + +// Loads filament while also moving towards given points in x-axis (x feedrate is limited by cutting the distance short if necessary) + Writer& load_move_x_advanced(float farthest_x, float loading_dist, float loading_speed, float max_x_speed = 50.f) + { + float time = std::abs(loading_dist / loading_speed); + float x_speed = std::min(max_x_speed, std::abs(farthest_x - x()) / time); + float feedrate = 60.f * std::hypot(x_speed, loading_speed); + + float end_point = x() + (farthest_x > x() ? 1.f : -1.f) * x_speed * time; + return extrude_explicit(end_point, y(), loading_dist, feedrate); + } + + // Elevate the extruder head above the current print_z position. + Writer& z_hop(float hop, float f = 0.f) + { + m_gcode += std::string("G1") + set_format_Z(m_current_z + hop); + if (f != 0 && f != m_current_feedrate) + m_gcode += set_format_F(f); + m_gcode += "\n"; + return *this; + } + + // Lower the extruder head back to the current print_z position. + Writer& z_hop_reset(float f = 0.f) + { return z_hop(0, f); } + + // Move to x1, +y_increment, + // extrude quickly amount e to x2 with feed f. + Writer& ram(float x1, float x2, float dy, float e0, float e, float f) + { + extrude_explicit(x1, m_current_pos.y + dy, e0, f, true); + extrude_explicit(x2, m_current_pos.y, e, 0.f, true); + return *this; + } + + // Let the end of the pulled out filament cool down in the cooling tube + // by moving up and down and moving the print head left / right + // at the current Y position to spread the leaking material. + Writer& cool(float x1, float x2, float e1, float e2, float f) + { + extrude_explicit(x1, m_current_pos.y, e1, f); + extrude_explicit(x2, m_current_pos.y, e2); + return *this; + } + + Writer& set_tool(int tool) + { + char buf[64]; + sprintf(buf, "T%d\n", tool); + m_gcode += buf; + m_current_tool = tool; + return *this; + } + + // Set extruder temperature, don't wait by default. + Writer& set_extruder_temp(int temperature, bool wait = false) + { + char buf[128]; + sprintf(buf, "M%d S%d\n", wait ? 109 : 104, temperature); + m_gcode += buf; + return *this; + }; + + // Wait for a period of time (seconds). + Writer& wait(float time) + { + if (time==0) + return *this; + char buf[128]; + sprintf(buf, "G4 S%.3f\n", time); + m_gcode += buf; + return *this; + }; + + // Set speed factor override percentage. + Writer& speed_override(int speed) + { + char buf[128]; + sprintf(buf, "M220 S%d\n", speed); + m_gcode += buf; + return *this; + }; + + // Set digital trimpot motor + Writer& set_extruder_trimpot(int current) + { + char buf[128]; + sprintf(buf, "M907 E%d\n", current); + m_gcode += buf; + return *this; + }; + + Writer& flush_planner_queue() + { + m_gcode += "G4 S0\n"; + return *this; + } + + // Reset internal extruder counter. + Writer& reset_extruder() + { + m_gcode += "G92 E0\n"; + return *this; + } + + Writer& comment_with_value(const char *comment, int value) + { + char strvalue[64]; + sprintf(strvalue, "%d", value); + m_gcode += std::string(";") + comment + strvalue + "\n"; + return *this; + }; + + + Writer& set_fan(unsigned int speed) + { + if (speed == m_last_fan_speed) + return *this; + + if (speed == 0) + m_gcode += "M107\n"; + else + { + m_gcode += "M106 S"; + char buf[128]; + sprintf(buf,"%u\n",(unsigned int)(255.0 * speed / 100.0)); + m_gcode += buf; + } + m_last_fan_speed = speed; + return *this; + } + + Writer& comment_material(WipeTowerPrusaMM::material_type material) + { + m_gcode += "; material : "; + switch (material) + { + case WipeTowerPrusaMM::PVA: + m_gcode += "#8 (PVA)"; + break; + case WipeTowerPrusaMM::SCAFF: + m_gcode += "#5 (Scaffold)"; + break; + case WipeTowerPrusaMM::FLEX: + m_gcode += "#4 (Flex)"; + break; + default: + m_gcode += "DEFAULT (PLA)"; + break; + } + m_gcode += "\n"; + return *this; + }; + + Writer& append(const char *text) { m_gcode += text; return *this; } + +private: + WipeTower::xy m_start_pos; + WipeTower::xy m_current_pos; + float m_current_z; + float m_current_feedrate; + unsigned int m_current_tool; + float m_layer_height; + float m_extrusion_flow; + bool m_preview_suppressed; + std::string m_gcode; + std::vector<WipeTower::Extrusion> m_extrusions; + float m_elapsed_time; + float m_internal_angle = 0.f; + float m_y_shift = 0.f; + float m_wipe_tower_width = 0.f; + float m_wipe_tower_depth = 0.f; + float m_last_fan_speed = 0.f; + int current_temp = -1; + const float m_default_analyzer_line_width; + float m_used_filament_length = 0.f; + + std::string set_format_X(float x) + { + char buf[64]; + sprintf(buf, " X%.3f", x); + m_current_pos.x = x; + return buf; + } + + std::string set_format_Y(float y) { + char buf[64]; + sprintf(buf, " Y%.3f", y); + m_current_pos.y = y; + return buf; + } + + std::string set_format_Z(float z) { + char buf[64]; + sprintf(buf, " Z%.3f", z); + return buf; + } + + std::string set_format_E(float e) { + char buf[64]; + sprintf(buf, " E%.4f", e); + return buf; + } + + std::string set_format_F(float f) { + char buf[64]; + sprintf(buf, " F%d", int(floor(f + 0.5f))); + m_current_feedrate = f; + return buf; + } + + Writer& operator=(const Writer &rhs); +}; // class Writer + +}; // namespace PrusaMultiMaterial + + + +WipeTowerPrusaMM::material_type WipeTowerPrusaMM::parse_material(const char *name) +{ + if (strcasecmp(name, "PLA") == 0) + return PLA; + if (strcasecmp(name, "ABS") == 0) + return ABS; + if (strcasecmp(name, "PET") == 0) + return PET; + if (strcasecmp(name, "HIPS") == 0) + return HIPS; + if (strcasecmp(name, "FLEX") == 0) + return FLEX; + if (strcasecmp(name, "SCAFF") == 0) + return SCAFF; + if (strcasecmp(name, "EDGE") == 0) + return EDGE; + if (strcasecmp(name, "NGEN") == 0) + return NGEN; + if (strcasecmp(name, "PVA") == 0) + return PVA; + return INVALID; +} + + +// Returns gcode to prime the nozzles at the front edge of the print bed. +WipeTower::ToolChangeResult WipeTowerPrusaMM::prime( + // print_z of the first layer. + float first_layer_height, + // Extruder indices, in the order to be primed. The last extruder will later print the wipe tower brim, print brim and the object. + const std::vector<unsigned int> &tools, + // If true, the last priming are will be the same as the other priming areas, and the rest of the wipe will be performed inside the wipe tower. + // If false, the last priming are will be large enough to wipe the last extruder sufficiently. + bool last_wipe_inside_wipe_tower) +{ + this->set_layer(first_layer_height, first_layer_height, tools.size(), true, false); + this->m_current_tool = tools.front(); + + // The Prusa i3 MK2 has a working space of [0, -2.2] to [250, 210]. + // Due to the XYZ calibration, this working space may shrink slightly from all directions, + // therefore the homing position is shifted inside the bed by 0.2 in the firmware to [0.2, -2.0]. +// box_coordinates cleaning_box(xy(0.5f, - 1.5f), m_wipe_tower_width, wipe_area); + + const float prime_section_width = std::min(240.f / tools.size(), 60.f); + box_coordinates cleaning_box(xy(5.f, 0.01f + m_perimeter_width/2.f), prime_section_width, 100.f); + + PrusaMultiMaterial::Writer writer(m_layer_height, m_perimeter_width); + writer.set_extrusion_flow(m_extrusion_flow) + .set_z(m_z_pos) + .set_initial_tool(m_current_tool) + .append(";--------------------\n" + "; CP PRIMING START\n") + .append(";--------------------\n") + .speed_override(100); + + writer.set_initial_position(xy(0.f, 0.f)) // Always move to the starting position + .travel(cleaning_box.ld, 7200) + .set_extruder_trimpot(750); // Increase the extruder driver current to allow fast ramming. + + for (size_t idx_tool = 0; idx_tool < tools.size(); ++ idx_tool) { + unsigned int tool = tools[idx_tool]; + m_left_to_right = true; + toolchange_Change(writer, tool, m_filpar[tool].material); // Select the tool, set a speed override for soluble and flex materials. + toolchange_Load(writer, cleaning_box); // Prime the tool. + if (idx_tool + 1 == tools.size()) { + // Last tool should not be unloaded, but it should be wiped enough to become of a pure color. + toolchange_Wipe(writer, cleaning_box, wipe_volumes[tools[idx_tool-1]][tool]); + } else { + // Ram the hot material out of the melt zone, retract the filament into the cooling tubes and let it cool. + //writer.travel(writer.x(), writer.y() + m_perimeter_width, 7200); + toolchange_Wipe(writer, cleaning_box , 20.f); + box_coordinates box = cleaning_box; + box.translate(0.f, writer.y() - cleaning_box.ld.y + m_perimeter_width); + toolchange_Unload(writer, box , m_filpar[m_current_tool].material, m_filpar[tools[idx_tool + 1]].first_layer_temperature); + cleaning_box.translate(prime_section_width, 0.f); + writer.travel(cleaning_box.ld, 7200); + } + ++ m_num_tool_changes; + } + + m_old_temperature = -1; // If the priming is turned off in config, the temperature changing commands will not actually appear + // in the output gcode - we should not remember emitting them (we will output them twice in the worst case) + + // Reset the extruder current to a normal value. + writer.set_extruder_trimpot(550) + .feedrate(6000) + .flush_planner_queue() + .reset_extruder() + .append("; CP PRIMING END\n" + ";------------------\n" + "\n\n"); + + // so that tool_change() will know to extrude the wipe tower brim: + m_print_brim = true; + + // Ask our writer about how much material was consumed: + m_used_filament_length[m_current_tool] += writer.get_and_reset_used_filament_length(); + + ToolChangeResult result; + result.priming = true; + result.print_z = this->m_z_pos; + result.layer_height = this->m_layer_height; + result.gcode = writer.gcode(); + result.elapsed_time = writer.elapsed_time(); + result.extrusions = writer.extrusions(); + result.start_pos = writer.start_pos_rotated(); + result.end_pos = writer.pos_rotated(); + return result; +} + +WipeTower::ToolChangeResult WipeTowerPrusaMM::tool_change(unsigned int tool, bool last_in_layer) +{ + if ( m_print_brim ) + return toolchange_Brim(); + + float wipe_area = 0.f; + bool last_change_in_layer = false; + float wipe_volume = 0.f; + + // Finds this toolchange info + if (tool != (unsigned int)(-1)) + { + for (const auto &b : m_layer_info->tool_changes) + if ( b.new_tool == tool ) { + wipe_volume = b.wipe_volume; + if (tool == m_layer_info->tool_changes.back().new_tool) + last_change_in_layer = true; + wipe_area = b.required_depth * m_layer_info->extra_spacing; + break; + } + } + else { + // Otherwise we are going to Unload only. And m_layer_info would be invalid. + } + + box_coordinates cleaning_box( + xy(m_perimeter_width / 2.f, m_perimeter_width / 2.f), + m_wipe_tower_width - m_perimeter_width, + (tool != (unsigned int)(-1) ? /*m_layer_info->depth*/wipe_area+m_depth_traversed-0.5*m_perimeter_width + : m_wipe_tower_depth-m_perimeter_width)); + + PrusaMultiMaterial::Writer writer(m_layer_height, m_perimeter_width); + writer.set_extrusion_flow(m_extrusion_flow) + .set_z(m_z_pos) + .set_initial_tool(m_current_tool) + .set_y_shift(m_y_shift + (tool!=(unsigned int)(-1) && (m_current_shape == SHAPE_REVERSED && !m_peters_wipe_tower) ? m_layer_info->depth - m_layer_info->toolchanges_depth(): 0.f)) + .append(";--------------------\n" + "; CP TOOLCHANGE START\n") + .comment_with_value(" toolchange #", m_num_tool_changes + 1) // the number is zero-based + .comment_material(m_filpar[m_current_tool].material) + .append(";--------------------\n") + .speed_override(100); + + xy initial_position = cleaning_box.ld + WipeTower::xy(0.f,m_depth_traversed); + writer.set_initial_position(initial_position, m_wipe_tower_width, m_wipe_tower_depth, m_internal_rotation); + + // Increase the extruder driver current to allow fast ramming. + writer.set_extruder_trimpot(750); + + // Ram the hot material out of the melt zone, retract the filament into the cooling tubes and let it cool. + if (tool != (unsigned int)-1){ // This is not the last change. + toolchange_Unload(writer, cleaning_box, m_filpar[m_current_tool].material, + m_is_first_layer ? m_filpar[tool].first_layer_temperature : m_filpar[tool].temperature); + toolchange_Change(writer, tool, m_filpar[tool].material); // Change the tool, set a speed override for soluble and flex materials. + toolchange_Load(writer, cleaning_box); + writer.travel(writer.x(),writer.y()-m_perimeter_width); // cooling and loading were done a bit down the road + toolchange_Wipe(writer, cleaning_box, wipe_volume); // Wipe the newly loaded filament until the end of the assigned wipe area. + ++ m_num_tool_changes; + } else + toolchange_Unload(writer, cleaning_box, m_filpar[m_current_tool].material, m_filpar[m_current_tool].temperature); + + m_depth_traversed += wipe_area; + + if (last_change_in_layer) {// draw perimeter line + writer.set_y_shift(m_y_shift); + if (m_peters_wipe_tower) + writer.rectangle(WipeTower::xy(0.f, 0.f),m_layer_info->depth + 3*m_perimeter_width,m_wipe_tower_depth); + else { + writer.rectangle(WipeTower::xy(0.f, 0.f),m_wipe_tower_width, m_layer_info->depth + m_perimeter_width); + if (layer_finished()) { // no finish_layer will be called, we must wipe the nozzle + writer.travel(writer.x()> m_wipe_tower_width / 2.f ? 0.f : m_wipe_tower_width, writer.y()); + } + } + } + + writer.set_extruder_trimpot(550) // Reset the extruder current to a normal value. + .feedrate(6000) + .flush_planner_queue() + .reset_extruder() + .append("; CP TOOLCHANGE END\n" + ";------------------\n" + "\n\n"); + + // Ask our writer about how much material was consumed: + m_used_filament_length[m_current_tool] += writer.get_and_reset_used_filament_length(); + + ToolChangeResult result; + result.priming = false; + result.print_z = this->m_z_pos; + result.layer_height = this->m_layer_height; + result.gcode = writer.gcode(); + result.elapsed_time = writer.elapsed_time(); + result.extrusions = writer.extrusions(); + result.start_pos = writer.start_pos_rotated(); + result.end_pos = writer.pos_rotated(); + return result; +} + +WipeTower::ToolChangeResult WipeTowerPrusaMM::toolchange_Brim(bool sideOnly, float y_offset) +{ + const box_coordinates wipeTower_box( + WipeTower::xy(0.f, 0.f), + m_wipe_tower_width, + m_wipe_tower_depth); + + PrusaMultiMaterial::Writer writer(m_layer_height, m_perimeter_width); + writer.set_extrusion_flow(m_extrusion_flow * 1.1f) + .set_z(m_z_pos) // Let the writer know the current Z position as a base for Z-hop. + .set_initial_tool(m_current_tool) + .append(";-------------------------------------\n" + "; CP WIPE TOWER FIRST LAYER BRIM START\n"); + + xy initial_position = wipeTower_box.lu - xy(m_perimeter_width * 6.f, 0); + writer.set_initial_position(initial_position, m_wipe_tower_width, m_wipe_tower_depth, m_internal_rotation); + + writer.extrude_explicit(wipeTower_box.ld - xy(m_perimeter_width * 6.f, 0), // Prime the extruder left of the wipe tower. + 1.5f * m_extrusion_flow * (wipeTower_box.lu.y - wipeTower_box.ld.y), 2400); + + // The tool is supposed to be active and primed at the time when the wipe tower brim is extruded. + // Extrude 4 rounds of a brim around the future wipe tower. + box_coordinates box(wipeTower_box); + box.expand(m_perimeter_width); + for (size_t i = 0; i < 4; ++ i) { + writer.travel (box.ld, 7000) + .extrude(box.lu, 2100).extrude(box.ru) + .extrude(box.rd ).extrude(box.ld); + box.expand(m_perimeter_width); + } + + writer.travel(wipeTower_box.ld, 7000); // Move to the front left corner. + writer.travel(wipeTower_box.rd) // Always wipe the nozzle with a long wipe to reduce stringing when moving away from the wipe tower. + .travel(wipeTower_box.ld); + writer.append("; CP WIPE TOWER FIRST LAYER BRIM END\n" + ";-----------------------------------\n"); + + m_print_brim = false; // Mark the brim as extruded + + // Ask our writer about how much material was consumed: + m_used_filament_length[m_current_tool] += writer.get_and_reset_used_filament_length(); + + ToolChangeResult result; + result.priming = false; + result.print_z = this->m_z_pos; + result.layer_height = this->m_layer_height; + result.gcode = writer.gcode(); + result.elapsed_time = writer.elapsed_time(); + result.extrusions = writer.extrusions(); + result.start_pos = writer.start_pos_rotated(); + result.end_pos = writer.pos_rotated(); + return result; +} + + + +// Ram the hot material out of the melt zone, retract the filament into the cooling tubes and let it cool. +void WipeTowerPrusaMM::toolchange_Unload( + PrusaMultiMaterial::Writer &writer, + const box_coordinates &cleaning_box, + const material_type current_material, + const int new_temperature) +{ + float xl = cleaning_box.ld.x + 1.f * m_perimeter_width; + float xr = cleaning_box.rd.x - 1.f * m_perimeter_width; + + const float line_width = m_perimeter_width * m_filpar[m_current_tool].ramming_line_width_multiplicator; // desired ramming line thickness + const float y_step = line_width * m_filpar[m_current_tool].ramming_step_multiplicator * m_extra_spacing; // spacing between lines in mm + + writer.append("; CP TOOLCHANGE UNLOAD\n") + .change_analyzer_line_width(line_width); + + unsigned i = 0; // iterates through ramming_speed + m_left_to_right = true; // current direction of ramming + float remaining = xr - xl ; // keeps track of distance to the next turnaround + float e_done = 0; // measures E move done from each segment + + writer.travel(xl, cleaning_box.ld.y + m_depth_traversed + y_step/2.f ); // move to starting position + + // if the ending point of the ram would end up in mid air, align it with the end of the wipe tower: + if (m_layer_info > m_plan.begin() && m_layer_info < m_plan.end() && (m_layer_info-1!=m_plan.begin() || !m_adhesion )) { + + // this is y of the center of previous sparse infill border + float sparse_beginning_y = 0.f; + if (m_current_shape == SHAPE_REVERSED) + sparse_beginning_y += ((m_layer_info-1)->depth - (m_layer_info-1)->toolchanges_depth()) + - ((m_layer_info)->depth-(m_layer_info)->toolchanges_depth()) ; + else + sparse_beginning_y += (m_layer_info-1)->toolchanges_depth() + m_perimeter_width; + + //debugging: + /* float oldx = writer.x(); + float oldy = writer.y(); + writer.travel(xr,sparse_beginning_y); + writer.extrude(xr+5,writer.y()); + writer.travel(oldx,oldy);*/ + + float sum_of_depths = 0.f; + for (const auto& tch : m_layer_info->tool_changes) { // let's find this toolchange + if (tch.old_tool == m_current_tool) { + sum_of_depths += tch.ramming_depth; + float ramming_end_y = sum_of_depths; + ramming_end_y -= (y_step/m_extra_spacing-m_perimeter_width) / 2.f; // center of final ramming line + + // debugging: + /*float oldx = writer.x(); + float oldy = writer.y(); + writer.travel(xl,ramming_end_y); + writer.extrude(xl-15,writer.y()); + writer.travel(oldx,oldy);*/ + + if ( (m_current_shape == SHAPE_REVERSED && ramming_end_y < sparse_beginning_y - 0.5f*m_perimeter_width ) || + (m_current_shape == SHAPE_NORMAL && ramming_end_y > sparse_beginning_y + 0.5f*m_perimeter_width ) ) + { + writer.extrude(xl + tch.first_wipe_line-1.f*m_perimeter_width,writer.y()); + remaining -= tch.first_wipe_line-1.f*m_perimeter_width; + } + break; + } + sum_of_depths += tch.required_depth; + } + } + + // now the ramming itself: + while (i < m_filpar[m_current_tool].ramming_speed.size()) + { + const float x = volume_to_length(m_filpar[m_current_tool].ramming_speed[i] * 0.25f, line_width, m_layer_height); + const float e = m_filpar[m_current_tool].ramming_speed[i] * 0.25f / Filament_Area; // transform volume per sec to E move; + const float dist = std::min(x - e_done, remaining); // distance to travel for either the next 0.25s, or to the next turnaround + const float actual_time = dist/x * 0.25; + writer.ram(writer.x(), writer.x() + (m_left_to_right ? 1.f : -1.f) * dist, 0, 0, e * (dist / x), std::hypot(dist, e * (dist / x)) / (actual_time / 60.)); + remaining -= dist; + + if (remaining < WT_EPSILON) { // we reached a turning point + writer.travel(writer.x(), writer.y() + y_step, 7200); + m_left_to_right = !m_left_to_right; + remaining = xr - xl; + } + e_done += dist; // subtract what was actually done + if (e_done > x - WT_EPSILON) { // current segment finished + ++i; + e_done = 0; + } + } + WipeTower::xy end_of_ramming(writer.x(),writer.y()); + writer.change_analyzer_line_width(m_perimeter_width); // so the next lines are not affected by ramming_line_width_multiplier + + // Retraction: + float old_x = writer.x(); + float turning_point = (!m_left_to_right ? xl : xr ); + float total_retraction_distance = m_cooling_tube_retraction + m_cooling_tube_length/2.f - 15.f; // the 15mm is reserved for the first part after ramming + writer.suppress_preview() + .retract(15.f, m_filpar[m_current_tool].unloading_speed_start * 60.f) // feedrate 5000mm/min = 83mm/s + .retract(0.70f * total_retraction_distance, 1.0f * m_filpar[m_current_tool].unloading_speed * 60.f) + .retract(0.20f * total_retraction_distance, 0.5f * m_filpar[m_current_tool].unloading_speed * 60.f) + .retract(0.10f * total_retraction_distance, 0.3f * m_filpar[m_current_tool].unloading_speed * 60.f) + + /*.load_move_x_advanced(turning_point, -15.f, 83.f, 50.f) // this is done at fixed speed + .load_move_x_advanced(old_x, -0.70f * total_retraction_distance, 1.0f * m_filpar[m_current_tool].unloading_speed) + .load_move_x_advanced(turning_point, -0.20f * total_retraction_distance, 0.5f * m_filpar[m_current_tool].unloading_speed) + .load_move_x_advanced(old_x, -0.10f * total_retraction_distance, 0.3f * m_filpar[m_current_tool].unloading_speed) + .travel(old_x, writer.y()) // in case previous move was shortened to limit feedrate*/ + .resume_preview(); + if (new_temperature != 0 && (new_temperature != m_old_temperature || m_is_first_layer) ) { // Set the extruder temperature, but don't wait. + // If the required temperature is the same as last time, don't emit the M104 again (if user adjusted the value, it would be reset) + // However, always change temperatures on the first layer (this is to avoid issues with priming lines turned off). + writer.set_extruder_temp(new_temperature, false); + m_old_temperature = new_temperature; + } + + // Cooling: + const int& number_of_moves = m_filpar[m_current_tool].cooling_moves; + if (number_of_moves > 0) { + const float& initial_speed = m_filpar[m_current_tool].cooling_initial_speed; + const float& final_speed = m_filpar[m_current_tool].cooling_final_speed; + + float speed_inc = (final_speed - initial_speed) / (2.f * number_of_moves - 1.f); + + writer.suppress_preview() + .travel(writer.x(), writer.y() + y_step); + old_x = writer.x(); + turning_point = xr-old_x > old_x-xl ? xr : xl; + for (int i=0; i<number_of_moves; ++i) { + float speed = initial_speed + speed_inc * 2*i; + writer.load_move_x_advanced(turning_point, m_cooling_tube_length, speed); + speed += speed_inc; + writer.load_move_x_advanced(old_x, -m_cooling_tube_length, speed); + } + } + + // let's wait is necessary: + writer.wait(m_filpar[m_current_tool].delay); + // we should be at the beginning of the cooling tube again - let's move to parking position: + writer.retract(-m_cooling_tube_length/2.f+m_parking_pos_retraction-m_cooling_tube_retraction, 2000); + + // this is to align ramming and future wiping extrusions, so the future y-steps can be uniform from the start: + // the perimeter_width will later be subtracted, it is there to not load while moving over just extruded material + writer.travel(end_of_ramming.x, end_of_ramming.y + (y_step/m_extra_spacing-m_perimeter_width) / 2.f + m_perimeter_width, 2400.f); + + writer.resume_preview() + .flush_planner_queue(); +} + +// Change the tool, set a speed override for soluble and flex materials. +void WipeTowerPrusaMM::toolchange_Change( + PrusaMultiMaterial::Writer &writer, + const unsigned int new_tool, + material_type new_material) +{ + // Ask the writer about how much of the old filament we consumed: + m_used_filament_length[m_current_tool] += writer.get_and_reset_used_filament_length(); + + // Speed override for the material. Go slow for flex and soluble materials. + int speed_override; + switch (new_material) { + case PVA: speed_override = (m_z_pos < 0.80f) ? 60 : 80; break; + case SCAFF: speed_override = 35; break; + case FLEX: speed_override = 35; break; + default: speed_override = 100; + } + writer.set_tool(new_tool) + .speed_override(speed_override) + .flush_planner_queue(); + m_current_tool = new_tool; +} + + + +void WipeTowerPrusaMM::toolchange_Load( + PrusaMultiMaterial::Writer &writer, + const box_coordinates &cleaning_box) +{ + float xl = cleaning_box.ld.x + m_perimeter_width * 0.75f; + float xr = cleaning_box.rd.x - m_perimeter_width * 0.75f; + float oldx = writer.x(); // the nozzle is in place to do the first wiping moves, we will remember the position + + // Load the filament while moving left / right, so the excess material will not create a blob at a single position. + float turning_point = ( oldx-xl < xr-oldx ? xr : xl ); + float edist = m_parking_pos_retraction+m_extra_loading_move; + + writer.append("; CP TOOLCHANGE LOAD\n") + .suppress_preview() + /*.load_move_x_advanced(turning_point, 0.2f * edist, 0.3f * m_filpar[m_current_tool].loading_speed) // Acceleration + .load_move_x_advanced(oldx, 0.5f * edist, m_filpar[m_current_tool].loading_speed) // Fast phase + .load_move_x_advanced(turning_point, 0.2f * edist, 0.3f * m_filpar[m_current_tool].loading_speed) // Slowing down + .load_move_x_advanced(oldx, 0.1f * edist, 0.1f * m_filpar[m_current_tool].loading_speed) // Super slow*/ + + .load(0.2f * edist, 60.f * m_filpar[m_current_tool].loading_speed_start) + .load_move_x_advanced(turning_point, 0.7f * edist, m_filpar[m_current_tool].loading_speed) // Fast phase + .load_move_x_advanced(oldx, 0.1f * edist, 0.1f * m_filpar[m_current_tool].loading_speed) // Super slow*/ + + .travel(oldx, writer.y()) // in case last move was shortened to limit x feedrate + .resume_preview(); + + // Reset the extruder current to the normal value. + writer.set_extruder_trimpot(550); +} + + + + +// Wipe the newly loaded filament until the end of the assigned wipe area. +void WipeTowerPrusaMM::toolchange_Wipe( + PrusaMultiMaterial::Writer &writer, + const box_coordinates &cleaning_box, + float wipe_volume) +{ + // Increase flow on first layer, slow down print. + writer.set_extrusion_flow(m_extrusion_flow * (m_is_first_layer ? 1.18f : 1.f)) + .append("; CP TOOLCHANGE WIPE\n"); + float wipe_coeff = m_is_first_layer ? 0.5f : 1.f; + const float& xl = cleaning_box.ld.x; + const float& xr = cleaning_box.rd.x; + + // Variables x_to_wipe and traversed_x are here to be able to make sure it always wipes at least + // the ordered volume, even if it means violating the box. This can later be removed and simply + // wipe until the end of the assigned area. + + float x_to_wipe = volume_to_length(wipe_volume, m_perimeter_width, m_layer_height); + float dy = m_extra_spacing*m_perimeter_width; + float wipe_speed = 1600.f; + + // if there is less than 2.5*m_perimeter_width to the edge, advance straightaway (there is likely a blob anyway) + if ((m_left_to_right ? xr-writer.x() : writer.x()-xl) < 2.5f*m_perimeter_width) { + writer.travel((m_left_to_right ? xr-m_perimeter_width : xl+m_perimeter_width),writer.y()+dy); + m_left_to_right = !m_left_to_right; + } + + // now the wiping itself: + for (int i = 0; true; ++i) { + if (i!=0) { + if (wipe_speed < 1610.f) wipe_speed = 1800.f; + else if (wipe_speed < 1810.f) wipe_speed = 2200.f; + else if (wipe_speed < 2210.f) wipe_speed = 4200.f; + else wipe_speed = std::min(4800.f, wipe_speed + 50.f); + } + + float traversed_x = writer.x(); + if (m_left_to_right) + writer.extrude(xr - (i % 4 == 0 ? 0 : 1.5*m_perimeter_width), writer.y(), wipe_speed * wipe_coeff); + else + writer.extrude(xl + (i % 4 == 1 ? 0 : 1.5*m_perimeter_width), writer.y(), wipe_speed * wipe_coeff); + + if (writer.y()+EPSILON > cleaning_box.lu.y-0.5f*m_perimeter_width) + break; // in case next line would not fit + + traversed_x -= writer.x(); + x_to_wipe -= fabs(traversed_x); + if (x_to_wipe < WT_EPSILON) { + writer.travel(m_left_to_right ? xl + 1.5*m_perimeter_width : xr - 1.5*m_perimeter_width, writer.y(), 7200); + break; + } + // stepping to the next line: + writer.extrude(writer.x() + (i % 4 == 0 ? -1.f : (i % 4 == 1 ? 1.f : 0.f)) * 1.5*m_perimeter_width, writer.y() + dy); + m_left_to_right = !m_left_to_right; + } + + // this is neither priming nor not the last toolchange on this layer - we are going back to the model - wipe the nozzle + if (m_layer_info != m_plan.end() && m_current_tool != m_layer_info->tool_changes.back().new_tool) { + m_left_to_right = !m_left_to_right; + writer.travel(writer.x(), writer.y() - dy) + .travel(m_left_to_right ? m_wipe_tower_width : 0.f, writer.y()); + } + + writer.set_extrusion_flow(m_extrusion_flow); // Reset the extrusion flow. +} + + + + +WipeTower::ToolChangeResult WipeTowerPrusaMM::finish_layer() +{ + // This should only be called if the layer is not finished yet. + // Otherwise the caller would likely travel to the wipe tower in vain. + assert(! this->layer_finished()); + + PrusaMultiMaterial::Writer writer(m_layer_height, m_perimeter_width); + writer.set_extrusion_flow(m_extrusion_flow) + .set_z(m_z_pos) + .set_initial_tool(m_current_tool) + .set_y_shift(m_y_shift - (m_current_shape == SHAPE_REVERSED && !m_peters_wipe_tower ? m_layer_info->toolchanges_depth() : 0.f)) + .append(";--------------------\n" + "; CP EMPTY GRID START\n") + .comment_with_value(" layer #", m_num_layer_changes + 1); + + // Slow down on the 1st layer. + float speed_factor = m_is_first_layer ? 0.5f : 1.f; + float current_depth = m_layer_info->depth - m_layer_info->toolchanges_depth(); + box_coordinates fill_box(xy(m_perimeter_width, m_depth_traversed + m_perimeter_width), + m_wipe_tower_width - 2 * m_perimeter_width, current_depth-m_perimeter_width); + + + writer.set_initial_position((m_left_to_right ? fill_box.ru : fill_box.lu), // so there is never a diagonal travel + m_wipe_tower_width, m_wipe_tower_depth, m_internal_rotation); + + box_coordinates box = fill_box; + for (int i=0;i<2;++i) { + if (m_layer_info->toolchanges_depth() < WT_EPSILON) { // there were no toolchanges on this layer + if (i==0) box.expand(m_perimeter_width); + else box.expand(-m_perimeter_width); + } + else i=2; // only draw the inner perimeter, outer has been already drawn by tool_change(...) + writer.rectangle(box.ld,box.rd.x-box.ld.x,box.ru.y-box.rd.y,2900*speed_factor); + } + + // we are in one of the corners, travel to ld along the perimeter: + if (writer.x() > fill_box.ld.x+EPSILON) writer.travel(fill_box.ld.x,writer.y()); + if (writer.y() > fill_box.ld.y+EPSILON) writer.travel(writer.x(),fill_box.ld.y); + + if (m_is_first_layer && m_adhesion) { + // Extrude a dense infill at the 1st layer to improve 1st layer adhesion of the wipe tower. + box.expand(-m_perimeter_width/2.f); + int nsteps = int(floor((box.lu.y - box.ld.y) / (2*m_perimeter_width))); + float step = (box.lu.y - box.ld.y) / nsteps; + writer.travel(box.ld-xy(m_perimeter_width/2.f,m_perimeter_width/2.f)); + if (nsteps >= 0) + for (int i = 0; i < nsteps; ++i) { + writer.extrude(box.ld.x+m_perimeter_width/2.f, writer.y() + 0.5f * step); + writer.extrude(box.rd.x - m_perimeter_width / 2.f, writer.y()); + writer.extrude(box.rd.x - m_perimeter_width / 2.f, writer.y() + 0.5f * step); + writer.extrude(box.ld.x + m_perimeter_width / 2.f, writer.y()); + } + writer.travel(box.rd.x-m_perimeter_width/2.f,writer.y()); // wipe the nozzle + } + else { // Extrude a sparse infill to support the material to be printed above. + const float dy = (fill_box.lu.y - fill_box.ld.y - m_perimeter_width); + const float left = fill_box.lu.x+2*m_perimeter_width; + const float right = fill_box.ru.x - 2 * m_perimeter_width; + if (dy > m_perimeter_width) + { + // Extrude an inverse U at the left of the region. + writer.travel(fill_box.ld + xy(m_perimeter_width * 2, 0.f)) + .extrude(fill_box.lu + xy(m_perimeter_width * 2, 0.f), 2900 * speed_factor); + + const int n = 1+(right-left)/(m_bridging); + const float dx = (right-left)/n; + for (int i=1;i<=n;++i) { + float x=left+dx*i; + writer.travel(x,writer.y()); + writer.extrude(x,i%2 ? fill_box.rd.y : fill_box.ru.y); + } + writer.travel(left,writer.y(),7200); // wipes the nozzle before moving away from the wipe tower + } + else + writer.travel(right,writer.y(),7200); // wipes the nozzle before moving away from the wipe tower + } + writer.append("; CP EMPTY GRID END\n" + ";------------------\n\n\n\n\n\n\n"); + + m_depth_traversed = m_wipe_tower_depth-m_perimeter_width; + + // Ask our writer about how much material was consumed: + m_used_filament_length[m_current_tool] += writer.get_and_reset_used_filament_length(); + + ToolChangeResult result; + result.priming = false; + result.print_z = this->m_z_pos; + result.layer_height = this->m_layer_height; + result.gcode = writer.gcode(); + result.elapsed_time = writer.elapsed_time(); + result.extrusions = writer.extrusions(); + result.start_pos = writer.start_pos_rotated(); + result.end_pos = writer.pos_rotated(); + return result; +} + +// Appends a toolchange into m_plan and calculates neccessary depth of the corresponding box +void WipeTowerPrusaMM::plan_toolchange(float z_par, float layer_height_par, unsigned int old_tool, unsigned int new_tool, bool brim, float wipe_volume) +{ + assert(m_plan.back().z <= z_par + WT_EPSILON ); // refuses to add a layer below the last one + + if (m_plan.empty() || m_plan.back().z + WT_EPSILON < z_par) // if we moved to a new layer, we'll add it to m_plan first + m_plan.push_back(WipeTowerInfo(z_par, layer_height_par)); + + if (brim) { // this toolchange prints brim - we must add it to m_plan, but not to count its depth + m_plan.back().tool_changes.push_back(WipeTowerInfo::ToolChange(old_tool, new_tool)); + return; + } + + if (old_tool==new_tool) // new layer without toolchanges - we are done + return; + + // this is an actual toolchange - let's calculate depth to reserve on the wipe tower + float depth = 0.f; + float width = m_wipe_tower_width - 3*m_perimeter_width; + float length_to_extrude = volume_to_length(0.25f * std::accumulate(m_filpar[old_tool].ramming_speed.begin(), m_filpar[old_tool].ramming_speed.end(), 0.f), + m_perimeter_width * m_filpar[old_tool].ramming_line_width_multiplicator, + layer_height_par); + depth = (int(length_to_extrude / width) + 1) * (m_perimeter_width * m_filpar[old_tool].ramming_line_width_multiplicator * m_filpar[old_tool].ramming_step_multiplicator); + float ramming_depth = depth; + length_to_extrude = width*((length_to_extrude / width)-int(length_to_extrude / width)) - width; + float first_wipe_line = -length_to_extrude; + length_to_extrude += volume_to_length(wipe_volume, m_perimeter_width, layer_height_par); + length_to_extrude = std::max(length_to_extrude,0.f); + + depth += (int(length_to_extrude / width) + 1) * m_perimeter_width; + depth *= m_extra_spacing; + + m_plan.back().tool_changes.push_back(WipeTowerInfo::ToolChange(old_tool, new_tool, depth, ramming_depth, first_wipe_line, wipe_volume)); +} + + + +void WipeTowerPrusaMM::plan_tower() +{ + // Calculate m_wipe_tower_depth (maximum depth for all the layers) and propagate depths downwards + m_wipe_tower_depth = 0.f; + for (auto& layer : m_plan) + layer.depth = 0.f; + + for (int layer_index = m_plan.size() - 1; layer_index >= 0; --layer_index) + { + float this_layer_depth = std::max(m_plan[layer_index].depth, m_plan[layer_index].toolchanges_depth()); + m_plan[layer_index].depth = this_layer_depth; + + if (this_layer_depth > m_wipe_tower_depth - m_perimeter_width) + m_wipe_tower_depth = this_layer_depth + m_perimeter_width; + + for (int i = layer_index - 1; i >= 0 ; i--) + { + if (m_plan[i].depth - this_layer_depth < 2*m_perimeter_width ) + m_plan[i].depth = this_layer_depth; + } + } +} + +void WipeTowerPrusaMM::save_on_last_wipe() +{ + for (m_layer_info=m_plan.begin();m_layer_info<m_plan.end();++m_layer_info) { + set_layer(m_layer_info->z, m_layer_info->height, 0, m_layer_info->z == m_plan.front().z, m_layer_info->z == m_plan.back().z); + if (m_layer_info->tool_changes.size()==0) // we have no way to save anything on an empty layer + continue; + + for (const auto &toolchange : m_layer_info->tool_changes) + tool_change(toolchange.new_tool, false); + + float width = m_wipe_tower_width - 3*m_perimeter_width; // width we draw into + float length_to_save = 2*(m_wipe_tower_width+m_wipe_tower_depth) + (!layer_finished() ? finish_layer().total_extrusion_length_in_plane() : 0.f); + float length_to_wipe = volume_to_length(m_layer_info->tool_changes.back().wipe_volume, + m_perimeter_width,m_layer_info->height) - m_layer_info->tool_changes.back().first_wipe_line - length_to_save; + + length_to_wipe = std::max(length_to_wipe,0.f); + float depth_to_wipe = m_perimeter_width * (std::floor(length_to_wipe/width) + ( length_to_wipe > 0.f ? 1.f : 0.f ) ) * m_extra_spacing; + + //depth += (int(length_to_extrude / width) + 1) * m_perimeter_width; + m_layer_info->tool_changes.back().required_depth = m_layer_info->tool_changes.back().ramming_depth + depth_to_wipe; + } +} + +// Processes vector m_plan and calls respective functions to generate G-code for the wipe tower +// Resulting ToolChangeResults are appended into vector "result" +void WipeTowerPrusaMM::generate(std::vector<std::vector<WipeTower::ToolChangeResult>> &result) +{ + if (m_plan.empty()) + + return; + + m_extra_spacing = 1.f; + + plan_tower(); + for (int i=0;i<5;++i) { + save_on_last_wipe(); + plan_tower(); + } + + if (m_peters_wipe_tower) + make_wipe_tower_square(); + + m_layer_info = m_plan.begin(); + m_current_tool = (unsigned int)(-2); // we don't know which extruder to start with - we'll set it according to the first toolchange + for (auto& used : m_used_filament_length) // reset used filament stats + used = 0.f; + + std::vector<WipeTower::ToolChangeResult> layer_result; + for (auto layer : m_plan) + { + set_layer(layer.z,layer.height,0,layer.z == m_plan.front().z,layer.z == m_plan.back().z); + if (m_peters_wipe_tower) + m_internal_rotation += 90.f; + else + m_internal_rotation += 180.f; + + if (!m_peters_wipe_tower && m_layer_info->depth < m_wipe_tower_depth - m_perimeter_width) + m_y_shift = (m_wipe_tower_depth-m_layer_info->depth-m_perimeter_width)/2.f; + + for (const auto &toolchange : layer.tool_changes) { + if (m_current_tool == (unsigned int)(-2)) + m_current_tool = toolchange.old_tool; + layer_result.emplace_back(tool_change(toolchange.new_tool, false)); + } + + if (! layer_finished()) { + auto finish_layer_toolchange = finish_layer(); + if ( ! layer.tool_changes.empty() ) { // we will merge it to the last toolchange + auto& last_toolchange = layer_result.back(); + if (last_toolchange.end_pos != finish_layer_toolchange.start_pos) { + char buf[2048]; // Add a travel move from tc1.end_pos to tc2.start_pos. + sprintf(buf, "G1 X%.3f Y%.3f F7200\n", finish_layer_toolchange.start_pos.x, finish_layer_toolchange.start_pos.y); + last_toolchange.gcode += buf; + } + last_toolchange.gcode += finish_layer_toolchange.gcode; + last_toolchange.extrusions.insert(last_toolchange.extrusions.end(), finish_layer_toolchange.extrusions.begin(), finish_layer_toolchange.extrusions.end()); + last_toolchange.end_pos = finish_layer_toolchange.end_pos; + } + else + layer_result.emplace_back(std::move(finish_layer_toolchange)); + } + + result.emplace_back(std::move(layer_result)); + m_is_first_layer = false; + } +} + +void WipeTowerPrusaMM::make_wipe_tower_square() +{ + const float width = m_wipe_tower_width - 3 * m_perimeter_width; + const float depth = m_wipe_tower_depth - m_perimeter_width; + // area that we actually print into is width*depth + float side = sqrt(depth * width); + + m_wipe_tower_width = side + 3 * m_perimeter_width; + m_wipe_tower_depth = side + 2 * m_perimeter_width; + // For all layers, find how depth changed and update all toolchange depths + for (auto &lay : m_plan) + { + side = sqrt(lay.depth * width); + float width_ratio = width / side; + + //lay.extra_spacing = width_ratio; + for (auto &tch : lay.tool_changes) + tch.required_depth *= width_ratio; + } + + plan_tower(); // propagates depth downwards again (width has changed) + for (auto& lay : m_plan) // depths set, now the spacing + lay.extra_spacing = lay.depth / lay.toolchanges_depth(); +} + +}; // namespace Slic3r diff --git a/src/libslic3r/GCode/WipeTowerPrusaMM.hpp b/src/libslic3r/GCode/WipeTowerPrusaMM.hpp new file mode 100644 index 000000000..06625d189 --- /dev/null +++ b/src/libslic3r/GCode/WipeTowerPrusaMM.hpp @@ -0,0 +1,374 @@ +#ifndef WipeTowerPrusaMM_hpp_ +#define WipeTowerPrusaMM_hpp_ + +#include <cmath> +#include <string> +#include <sstream> +#include <utility> +#include <algorithm> + +#include "WipeTower.hpp" + + +namespace Slic3r +{ + +namespace PrusaMultiMaterial { + class Writer; +}; + + + +class WipeTowerPrusaMM : public WipeTower +{ +public: + enum material_type + { + INVALID = -1, + PLA = 0, // E:210C B:55C + ABS = 1, // E:255C B:100C + PET = 2, // E:240C B:90C + HIPS = 3, // E:220C B:100C + FLEX = 4, // E:245C B:80C + SCAFF = 5, // E:215C B:55C + EDGE = 6, // E:240C B:80C + NGEN = 7, // E:230C B:80C + PVA = 8 // E:210C B:80C + }; + + // Parse material name into material_type. + static material_type parse_material(const char *name); + + // x -- x coordinates of wipe tower in mm ( left bottom corner ) + // y -- y coordinates of wipe tower in mm ( left bottom corner ) + // width -- width of wipe tower in mm ( default 60 mm - leave as it is ) + // wipe_area -- space available for one toolchange in mm + WipeTowerPrusaMM(float x, float y, float width, float rotation_angle, float cooling_tube_retraction, + float cooling_tube_length, float parking_pos_retraction, float extra_loading_move, float bridging, + const std::vector<std::vector<float>>& wiping_matrix, unsigned int initial_tool) : + m_wipe_tower_pos(x, y), + m_wipe_tower_width(width), + m_wipe_tower_rotation_angle(rotation_angle), + m_y_shift(0.f), + m_z_pos(0.f), + m_is_first_layer(false), + m_cooling_tube_retraction(cooling_tube_retraction), + m_cooling_tube_length(cooling_tube_length), + m_parking_pos_retraction(parking_pos_retraction), + m_extra_loading_move(extra_loading_move), + m_bridging(bridging), + m_current_tool(initial_tool), + wipe_volumes(wiping_matrix) + {} + + virtual ~WipeTowerPrusaMM() {} + + + // Set the extruder properties. + void set_extruder(size_t idx, material_type material, int temp, int first_layer_temp, float loading_speed, float loading_speed_start, + float unloading_speed, float unloading_speed_start, float delay, int cooling_moves, + float cooling_initial_speed, float cooling_final_speed, std::string ramming_parameters, float nozzle_diameter) + { + //while (m_filpar.size() < idx+1) // makes sure the required element is in the vector + m_filpar.push_back(FilamentParameters()); + + m_filpar[idx].material = material; + m_filpar[idx].temperature = temp; + m_filpar[idx].first_layer_temperature = first_layer_temp; + m_filpar[idx].loading_speed = loading_speed; + m_filpar[idx].loading_speed_start = loading_speed_start; + m_filpar[idx].unloading_speed = unloading_speed; + m_filpar[idx].unloading_speed_start = unloading_speed_start; + m_filpar[idx].delay = delay; + m_filpar[idx].cooling_moves = cooling_moves; + m_filpar[idx].cooling_initial_speed = cooling_initial_speed; + m_filpar[idx].cooling_final_speed = cooling_final_speed; + m_filpar[idx].nozzle_diameter = nozzle_diameter; // to be used in future with (non-single) multiextruder MM + + m_perimeter_width = nozzle_diameter * Width_To_Nozzle_Ratio; // all extruders are now assumed to have the same diameter + + std::stringstream stream{ramming_parameters}; + float speed = 0.f; + stream >> m_filpar[idx].ramming_line_width_multiplicator >> m_filpar[idx].ramming_step_multiplicator; + m_filpar[idx].ramming_line_width_multiplicator /= 100; + m_filpar[idx].ramming_step_multiplicator /= 100; + while (stream >> speed) + m_filpar[idx].ramming_speed.push_back(speed); + + m_used_filament_length.resize(std::max(m_used_filament_length.size(), idx + 1)); // makes sure that the vector is big enough so we don't have to check later + } + + + // Appends into internal structure m_plan containing info about the future wipe tower + // to be used before building begins. The entries must be added ordered in z. + void plan_toolchange(float z_par, float layer_height_par, unsigned int old_tool, unsigned int new_tool, bool brim, float wipe_volume = 0.f); + + // Iterates through prepared m_plan, generates ToolChangeResults and appends them to "result" + void generate(std::vector<std::vector<WipeTower::ToolChangeResult>> &result); + + float get_depth() const { return m_wipe_tower_depth; } + + + + // Switch to a next layer. + virtual void set_layer( + // Print height of this layer. + float print_z, + // Layer height, used to calculate extrusion the rate. + float layer_height, + // Maximum number of tool changes on this layer or the layers below. + size_t max_tool_changes, + // Is this the first layer of the print? In that case print the brim first. + bool is_first_layer, + // Is this the last layer of the waste tower? + bool is_last_layer) + { + m_z_pos = print_z; + m_layer_height = layer_height; + m_is_first_layer = is_first_layer; + m_print_brim = is_first_layer; + m_depth_traversed = 0.f; + m_current_shape = (! is_first_layer && m_current_shape == SHAPE_NORMAL) ? SHAPE_REVERSED : SHAPE_NORMAL; + if (is_first_layer) { + this->m_num_layer_changes = 0; + this->m_num_tool_changes = 0; + } + else + ++ m_num_layer_changes; + + // Calculate extrusion flow from desired line width, nozzle diameter, filament diameter and layer_height: + m_extrusion_flow = extrusion_flow(layer_height); + + // Advance m_layer_info iterator, making sure we got it right + while (!m_plan.empty() && m_layer_info->z < print_z - WT_EPSILON && m_layer_info+1 != m_plan.end()) + ++m_layer_info; + } + + // Return the wipe tower position. + virtual const xy& position() const { return m_wipe_tower_pos; } + // Return the wipe tower width. + virtual float width() const { return m_wipe_tower_width; } + // The wipe tower is finished, there should be no more tool changes or wipe tower prints. + virtual bool finished() const { return m_max_color_changes == 0; } + + // Returns gcode to prime the nozzles at the front edge of the print bed. + virtual ToolChangeResult prime( + // print_z of the first layer. + float first_layer_height, + // Extruder indices, in the order to be primed. The last extruder will later print the wipe tower brim, print brim and the object. + const std::vector<unsigned int> &tools, + // If true, the last priming are will be the same as the other priming areas, and the rest of the wipe will be performed inside the wipe tower. + // If false, the last priming are will be large enough to wipe the last extruder sufficiently. + bool last_wipe_inside_wipe_tower); + + // Returns gcode for a toolchange and a final print head position. + // On the first layer, extrude a brim around the future wipe tower first. + virtual ToolChangeResult tool_change(unsigned int new_tool, bool last_in_layer); + + // Fill the unfilled space with a sparse infill. + // Call this method only if layer_finished() is false. + virtual ToolChangeResult finish_layer(); + + // Is the current layer finished? + virtual bool layer_finished() const { + return ( (m_is_first_layer ? m_wipe_tower_depth - m_perimeter_width : m_layer_info->depth) - WT_EPSILON < m_depth_traversed); + } + + virtual std::vector<float> get_used_filament() const override { return m_used_filament_length; } + virtual int get_number_of_toolchanges() const override { return m_num_tool_changes; } + + +private: + WipeTowerPrusaMM(); + + enum wipe_shape // A fill-in direction + { + SHAPE_NORMAL = 1, + SHAPE_REVERSED = -1 + }; + + + const bool m_peters_wipe_tower = false; // sparse wipe tower inspired by Peter's post processor - not finished yet + const float Filament_Area = M_PI * 1.75f * 1.75f / 4.f; // filament area in mm^2 + const float Width_To_Nozzle_Ratio = 1.25f; // desired line width (oval) in multiples of nozzle diameter - may not be actually neccessary to adjust + const float WT_EPSILON = 1e-3f; + + + xy m_wipe_tower_pos; // Left front corner of the wipe tower in mm. + float m_wipe_tower_width; // Width of the wipe tower. + float m_wipe_tower_depth = 0.f; // Depth of the wipe tower + float m_wipe_tower_rotation_angle = 0.f; // Wipe tower rotation angle in degrees (with respect to x axis) + float m_internal_rotation = 0.f; + float m_y_shift = 0.f; // y shift passed to writer + float m_z_pos = 0.f; // Current Z position. + float m_layer_height = 0.f; // Current layer height. + size_t m_max_color_changes = 0; // Maximum number of color changes per layer. + bool m_is_first_layer = false;// Is this the 1st layer of the print? If so, print the brim around the waste tower. + int m_old_temperature = -1; // To keep track of what was the last temp that we set (so we don't issue the command when not neccessary) + + // G-code generator parameters. + float m_cooling_tube_retraction = 0.f; + float m_cooling_tube_length = 0.f; + float m_parking_pos_retraction = 0.f; + float m_extra_loading_move = 0.f; + float m_bridging = 0.f; + bool m_adhesion = true; + + float m_perimeter_width = 0.4 * Width_To_Nozzle_Ratio; // Width of an extrusion line, also a perimeter spacing for 100% infill. + float m_extrusion_flow = 0.038; //0.029f;// Extrusion flow is derived from m_perimeter_width, layer height and filament diameter. + + + struct FilamentParameters { + material_type material = PLA; + int temperature = 0; + int first_layer_temperature = 0; + float loading_speed = 0.f; + float loading_speed_start = 0.f; + float unloading_speed = 0.f; + float unloading_speed_start = 0.f; + float delay = 0.f ; + int cooling_moves = 0; + float cooling_initial_speed = 0.f; + float cooling_final_speed = 0.f; + float ramming_line_width_multiplicator = 0.f; + float ramming_step_multiplicator = 0.f; + std::vector<float> ramming_speed; + float nozzle_diameter; + }; + + // Extruder specific parameters. + std::vector<FilamentParameters> m_filpar; + + + // State of the wipe tower generator. + unsigned int m_num_layer_changes = 0; // Layer change counter for the output statistics. + unsigned int m_num_tool_changes = 0; // Tool change change counter for the output statistics. + ///unsigned int m_idx_tool_change_in_layer = 0; // Layer change counter in this layer. Counting up to m_max_color_changes. + bool m_print_brim = true; + // A fill-in direction (positive Y, negative Y) alternates with each layer. + wipe_shape m_current_shape = SHAPE_NORMAL; + unsigned int m_current_tool = 0; + const std::vector<std::vector<float>> wipe_volumes; + + float m_depth_traversed = 0.f; // Current y position at the wipe tower. + bool m_left_to_right = true; + float m_extra_spacing = 1.f; + + // Calculates extrusion flow needed to produce required line width for given layer height + float extrusion_flow(float layer_height = -1.f) const // negative layer_height - return current m_extrusion_flow + { + if ( layer_height < 0 ) + return m_extrusion_flow; + return layer_height * ( m_perimeter_width - layer_height * (1-M_PI/4.f)) / Filament_Area; + } + + // Calculates length of extrusion line to extrude given volume + float volume_to_length(float volume, float line_width, float layer_height) const { + return std::max(0., volume / (layer_height * (line_width - layer_height * (1. - M_PI / 4.)))); + } + + // Calculates depth for all layers and propagates them downwards + void plan_tower(); + + // Goes through m_plan and recalculates depths and width of the WT to make it exactly square - experimental + void make_wipe_tower_square(); + + // Goes through m_plan, calculates border and finish_layer extrusions and subtracts them from last wipe + void save_on_last_wipe(); + + + struct box_coordinates + { + box_coordinates(float left, float bottom, float width, float height) : + ld(left , bottom ), + lu(left , bottom + height), + rd(left + width, bottom ), + ru(left + width, bottom + height) {} + box_coordinates(const xy &pos, float width, float height) : box_coordinates(pos.x, pos.y, width, height) {} + void translate(const xy &shift) { + ld += shift; lu += shift; + rd += shift; ru += shift; + } + void translate(const float dx, const float dy) { translate(xy(dx, dy)); } + void expand(const float offset) { + ld += xy(- offset, - offset); + lu += xy(- offset, offset); + rd += xy( offset, - offset); + ru += xy( offset, offset); + } + void expand(const float offset_x, const float offset_y) { + ld += xy(- offset_x, - offset_y); + lu += xy(- offset_x, offset_y); + rd += xy( offset_x, - offset_y); + ru += xy( offset_x, offset_y); + } + xy ld; // left down + xy lu; // left upper + xy rd; // right lower + xy ru; // right upper + }; + + + // to store information about tool changes for a given layer + struct WipeTowerInfo{ + struct ToolChange { + unsigned int old_tool; + unsigned int new_tool; + float required_depth; + float ramming_depth; + float first_wipe_line; + float wipe_volume; + ToolChange(unsigned int old, unsigned int newtool, float depth=0.f, float ramming_depth=0.f, float fwl=0.f, float wv=0.f) + : old_tool{old}, new_tool{newtool}, required_depth{depth}, ramming_depth{ramming_depth}, first_wipe_line{fwl}, wipe_volume{wv} {} + }; + float z; // z position of the layer + float height; // layer height + float depth; // depth of the layer based on all layers above + float extra_spacing; + float toolchanges_depth() const { float sum = 0.f; for (const auto &a : tool_changes) sum += a.required_depth; return sum; } + + std::vector<ToolChange> tool_changes; + + WipeTowerInfo(float z_par, float layer_height_par) + : z{z_par}, height{layer_height_par}, depth{0}, extra_spacing{1.f} {} + }; + + std::vector<WipeTowerInfo> m_plan; // Stores information about all layers and toolchanges for the future wipe tower (filled by plan_toolchange(...)) + std::vector<WipeTowerInfo>::iterator m_layer_info = m_plan.end(); + + // Stores information about used filament length per extruder: + std::vector<float> m_used_filament_length; + + + // Returns gcode for wipe tower brim + // sideOnly -- set to false -- experimental, draw brim on sides of wipe tower + // offset -- set to 0 -- experimental, offset to replace brim in front / rear of wipe tower + ToolChangeResult toolchange_Brim(bool sideOnly = false, float y_offset = 0.f); + + void toolchange_Unload( + PrusaMultiMaterial::Writer &writer, + const box_coordinates &cleaning_box, + const material_type current_material, + const int new_temperature); + + void toolchange_Change( + PrusaMultiMaterial::Writer &writer, + const unsigned int new_tool, + material_type new_material); + + void toolchange_Load( + PrusaMultiMaterial::Writer &writer, + const box_coordinates &cleaning_box); + + void toolchange_Wipe( + PrusaMultiMaterial::Writer &writer, + const box_coordinates &cleaning_box, + float wipe_volume); +}; + + + + +}; // namespace Slic3r + +#endif /* WipeTowerPrusaMM_hpp_ */ |