From 49455cf4276ea1aab799a619ab93e2421e6ed969 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20=C5=A0ach?= Date: Tue, 7 Nov 2023 16:12:11 +0100 Subject: [PATCH] Replace GCode.cpp travel_to with more general z-hop strategy. The new travel has an initial flat part, sloped part and once the travel height reaches maxima a flat part again. Also, the notion of extruder lift is removed. It is used no more. Consequently the retract_lift parameter lost its original meaning. --- src/libslic3r/Extruder.cpp | 5 - src/libslic3r/GCode.cpp | 398 ++++++++++++++---- src/libslic3r/GCode.hpp | 66 ++- .../GCode/AvoidCrossingPerimeters.cpp | 2 +- src/libslic3r/GCode/GCodeWriter.cpp | 115 +---- src/libslic3r/GCode/GCodeWriter.hpp | 13 +- src/libslic3r/GCode/WipeTower.cpp | 7 +- src/libslic3r/GCode/WipeTower.hpp | 1 + src/libslic3r/GCode/WipeTowerIntegration.cpp | 16 +- t/retraction.t | 6 +- tests/fff_print/test_gcode.cpp | 119 ++++++ tests/fff_print/test_gcodewriter.cpp | 62 --- 12 files changed, 544 insertions(+), 266 deletions(-) diff --git a/src/libslic3r/Extruder.cpp b/src/libslic3r/Extruder.cpp index 18b385b401..22b9161d09 100644 --- a/src/libslic3r/Extruder.cpp +++ b/src/libslic3r/Extruder.cpp @@ -141,11 +141,6 @@ double Extruder::retract_length() const return m_config->retract_length.get_at(m_id); } -double Extruder::retract_lift() const -{ - return m_config->retract_lift.get_at(m_id); -} - int Extruder::retract_speed() const { return int(floor(m_config->retract_speed.get_at(m_id)+0.5)); diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 256b14695f..bafb79d0fa 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -182,7 +182,7 @@ void GCodeGenerator::PlaceholderParserIntegration::reset() this->failed_templates.clear(); this->output_config.clear(); this->opt_position = nullptr; - this->opt_zhop = nullptr; + this->opt_zhop = nullptr; this->opt_e_position = nullptr; this->opt_e_retracted = nullptr; this->opt_e_restart_extra = nullptr; @@ -228,6 +228,7 @@ void GCodeGenerator::PlaceholderParserIntegration::init(const GCodeWriter &write this->position.assign(3, 0); this->opt_position = new ConfigOptionFloats(this->position); this->output_config.set_key_value("position", this->opt_position); + // Store zhop variable into the parser itself, it is a read-only variable to the script. this->opt_zhop = new ConfigOptionFloat(writer.get_zhop()); this->parser.set("zhop", this->opt_zhop); @@ -237,7 +238,6 @@ void GCodeGenerator::PlaceholderParserIntegration::update_from_gcodewriter(const { memcpy(this->position.data(), writer.get_position().data(), sizeof(double) * 3); this->opt_position->values = this->position; - this->opt_zhop->value = writer.get_zhop(); if (this->num_extruders > 0) { const std::vector &extruders = writer.extruders(); @@ -1257,7 +1257,7 @@ void GCodeGenerator::_do_export(Print& print, GCodeOutputStream &file, Thumbnail // This happens before Z goes down to layer 0 again, so that no collision happens hopefully. m_enable_cooling_markers = false; // we're not filtering these moves through CoolingBuffer m_avoid_crossing_perimeters.use_external_mp_once(); - file.write(this->retract()); + file.write(this->retract_and_wipe()); file.write(this->travel_to(Point(0, 0), ExtrusionRole::None, "move to origin position for next object")); m_enable_cooling_markers = true; // Disable motion planner when traveling to first object point. @@ -1308,7 +1308,7 @@ void GCodeGenerator::_do_export(Print& print, GCodeOutputStream &file, Thumbnail bool overlap = bbox_prime.overlap(bbox_print); if (print.config().gcode_flavor == gcfMarlinLegacy || print.config().gcode_flavor == gcfMarlinFirmware) { - file.write(this->retract()); + file.write(this->retract_and_wipe()); file.write("M300 S800 P500\n"); // Beep for 500ms, tone 800Hz. if (overlap) { // Wait for the user to remove the priming extrusions. @@ -1344,7 +1344,7 @@ void GCodeGenerator::_do_export(Print& print, GCodeOutputStream &file, Thumbnail } // Write end commands to file. - file.write(this->retract()); + file.write(this->retract_and_wipe()); file.write(m_writer.set_fan(0)); // adds tag for processor @@ -2616,8 +2616,8 @@ std::string GCodeGenerator::change_layer(coordf_t print_z) // Increment a progress bar indicator. gcode += m_writer.update_progress(++ m_layer_index, m_layer_count); coordf_t z = print_z + m_config.z_offset.value; // in unscaled coordinates - if (EXTRUDER_CONFIG(retract_layer_change) && m_writer.will_move_z(z)) - gcode += this->retract(); + if (EXTRUDER_CONFIG(retract_layer_change)) + gcode += this->retract_and_wipe(); { std::ostringstream comment; @@ -2688,9 +2688,11 @@ std::string GCodeGenerator::extrude_loop(const ExtrusionLoop &loop_src, const GC } else if (loop_src.paths.back().role().is_external_perimeter() && m_layer != nullptr && m_config.perimeters.value > 1) { // Only wipe inside if the wipe along the perimeter is disabled. // Make a little move inwards before leaving loop. - if (std::optional pt = wipe_hide_seam(smooth_path, is_hole, scale_(EXTRUDER_CONFIG(nozzle_diameter))); pt) + if (std::optional pt = wipe_hide_seam(smooth_path, is_hole, scale_(EXTRUDER_CONFIG(nozzle_diameter))); pt) { // Generate the seam hiding travel move. gcode += m_writer.travel_to_xy(this->point_to_gcode(*pt), "move inwards before travel"); + this->set_last_pos(*pt); + } } return gcode; @@ -2891,7 +2893,13 @@ std::string GCodeGenerator::_extrude( const std::string_view description_bridge = path_attr.role.is_bridge() ? " (bridge)"sv : ""sv; // go to first point of extrusion path - if (!m_last_pos_defined || m_last_pos != path.front().point) { + if (!m_last_pos_defined) { + const double z = this->m_last_layer_z + this->m_config.z_offset.value; + const std::string comment{"move to print after unknown position"}; + gcode += this->retract_and_wipe(); + gcode += this->m_writer.travel_to_xy(this->point_to_gcode(path.front().point), comment); + gcode += this->m_writer.get_travel_to_z_gcode(z, comment); + } else if ( m_last_pos != path.front().point) { std::string comment = "move to first "; comment += description; comment += description_bridge; @@ -3127,86 +3135,217 @@ std::string GCodeGenerator::_extrude( return gcode; } -// This method accepts &point in print coordinates. -std::string GCodeGenerator::travel_to(const Point &point, ExtrusionRole role, std::string comment) -{ - /* Define the travel move as a line between current position and the taget point. - This is expressed in print coordinates, so it will need to be translated by - this->origin in order to get G-code coordinates. */ - Polyline travel { this->last_pos(), point }; +Points3 generate_flat_travel(tcb::span xy_path, const float elevation) { + Points3 result; + result.reserve(xy_path.size() - 1); + for (const Point& point : xy_path.subspan(1)) { + result.emplace_back(point.x(), point.y(), scaled(elevation)); + } + return result; +} - if (this->config().avoid_crossing_curled_overhangs) { - if (m_config.avoid_crossing_perimeters) { - BOOST_LOG_TRIVIAL(warning) - << "Option >avoid crossing curled overhangs< is not compatible with avoid crossing perimeters and it will be ignored!"; +Vec2d place_at_segment(const Vec2d& current_point, const Vec2d& previous_point, const double distance) { + Vec2d direction = (current_point - previous_point).normalized(); + return previous_point + direction * distance; +} + +namespace GCode::Impl { +std::vector slice_xy_path(tcb::span xy_path, tcb::span sorted_distances) { + assert(xy_path.size() >= 2); + std::vector result; + result.reserve(xy_path.size() + sorted_distances.size()); + double total_distance{0}; + result.emplace_back(DistancedPoint{xy_path.front(), 0}); + Point previous_point = result.front().point; + std::size_t offset{0}; + for (const Point& point : xy_path.subspan(1)) { + Vec2d unscaled_point{unscaled(point)}; + Vec2d unscaled_previous_point{unscaled(previous_point)}; + const double current_segment_length = (unscaled_point - unscaled_previous_point).norm(); + for (const double distance_to_add : sorted_distances.subspan(offset)) { + if (distance_to_add <= total_distance + current_segment_length) { + Point to_place = scaled(place_at_segment( + unscaled_point, + unscaled_previous_point, + distance_to_add - total_distance + )); + if (to_place != previous_point && to_place != point) { + result.emplace_back(DistancedPoint{to_place, distance_to_add}); + } + ++offset; + } else { + break; + } + } + total_distance += current_segment_length; + result.emplace_back(DistancedPoint{point, total_distance}); + previous_point = point; + } + return result; +} + +struct ElevatedTravelParams { + double lift_height{}; + double slope_end{}; +}; + +struct ElevatedTravelFormula { + double operator()(double distance_from_start) const { + if (distance_from_start < this->params.slope_end) { + const double lift_percent = distance_from_start / this->params.slope_end; + return lift_percent * this->params.lift_height; } else { - Point scaled_origin = Point(scaled(this->origin())); - travel = m_avoid_crossing_curled_overhangs.find_path(this->last_pos() + scaled_origin, point + scaled_origin); - travel.translate(-scaled_origin); + return this->params.lift_height; } } - // check whether a straight travel move would need retraction - bool needs_retraction = this->needs_retraction(travel, role); - // check whether wipe could be disabled without causing visible stringing - bool could_be_wipe_disabled = false; - // Save state of use_external_mp_once for the case that will be needed to call twice m_avoid_crossing_perimeters.travel_to. - const bool used_external_mp_once = m_avoid_crossing_perimeters.used_external_mp_once(); + ElevatedTravelParams params{}; +}; - // if a retraction would be needed, try to use avoid_crossing_perimeters to plan a - // multi-hop travel path inside the configuration space - if (needs_retraction - && m_config.avoid_crossing_perimeters - && ! m_avoid_crossing_perimeters.disabled_once()) { - travel = m_avoid_crossing_perimeters.travel_to(*this, point, &could_be_wipe_disabled); - // check again whether the new travel path still needs a retraction - needs_retraction = this->needs_retraction(travel, role); - //if (needs_retraction && m_layer_index > 1) exit(0); +Points3 generate_elevated_travel( + const tcb::span xy_path, + const std::vector& ensure_points_at_distances, + const double initial_elevation, + const std::function& elevation +) { + Points3 result{}; + + std::vector extended_xy_path = slice_xy_path(xy_path, ensure_points_at_distances); + result.reserve(extended_xy_path.size()); + + for (const DistancedPoint& point : extended_xy_path) { + result.emplace_back(point.point.x(), point.point.y(), scaled(initial_elevation + elevation(point.distance_from_start))); } - // Re-allow avoid_crossing_perimeters for the next travel moves - m_avoid_crossing_perimeters.reset_once_modifiers(); + return result; +} + +AABBTreeLines::LinesDistancer get_expolygons_distancer(const ExPolygons& polygons) { + std::vector lines; + for (const ExPolygon& polygon : polygons) { + for (const Line& line : polygon.lines()) { + lines.emplace_back(unscaled(line.a), unscaled(line.b)); + } + } + + return AABBTreeLines::LinesDistancer{std::move(lines)}; +} + +std::optional get_first_crossed_line_distance( + tcb::span xy_path, + const AABBTreeLines::LinesDistancer& distancer +) { + assert(!xy_path.empty()); + if (xy_path.empty()) { + return {}; + } + + double traversed_distance = 0; + for (const Line& line : xy_path) { + const Linef unscaled_line = {unscaled(line.a), unscaled(line.b)}; + auto intersections = distancer.intersections_with_line(unscaled_line); + if (!intersections.empty()) { + const Vec2d intersection = intersections.front().first; + const double distance = traversed_distance + (unscaled_line.a - intersection).norm(); + if (distance > EPSILON) { + return distance; + } else if (intersections.size() >= 2) { // Edge case + const Vec2d second_intersection = intersections[1].first; + return traversed_distance + (unscaled_line.a - second_intersection).norm(); + } + } + traversed_distance += (unscaled_line.a - unscaled_line.b).norm(); + } + + return {}; +} + +ElevatedTravelParams get_elevated_traval_params( + const FullPrintConfig& config, + const unsigned extruder_id +) +{ + ElevatedTravelParams elevation_params{}; + if (!config.travel_ramping_lift.get_at(extruder_id)) { + elevation_params.slope_end = 0; + elevation_params.lift_height = config.retract_lift.get_at(extruder_id); + return elevation_params; + } + elevation_params.lift_height = config.travel_max_lift.get_at(extruder_id); + + const double slope_deg = config.travel_slope.get_at(extruder_id); + + if (slope_deg >= 90 || slope_deg <= 0) { + elevation_params.slope_end = 0; + } else { + const double slope_rad = slope_deg * (M_PI / 180); // rad + elevation_params.slope_end = elevation_params.lift_height / std::tan(slope_rad); + } + + return elevation_params; +} + +Points3 generate_travel_to_extrusion( + const Polyline& xy_path, + const FullPrintConfig& config, + const unsigned extruder_id, + const double initial_elevation +) { + const double upper_limit = config.retract_lift_below.get_at(extruder_id); + const double lower_limit = config.retract_lift_above.get_at(extruder_id); + if ( + (lower_limit > 0 && initial_elevation < lower_limit) + || (upper_limit > 0 && initial_elevation > upper_limit) + ) { + return generate_flat_travel(xy_path.points, initial_elevation); + } + + ElevatedTravelParams elevation_params{get_elevated_traval_params( + config, + extruder_id + )}; + + const std::vector ensure_points_at_distances{elevation_params.slope_end}; + + Points3 result{generate_elevated_travel( + xy_path.points, + ensure_points_at_distances, + initial_elevation, + ElevatedTravelFormula{elevation_params} + )}; + + result.emplace_back(xy_path.back().x(), xy_path.back().y(), scaled(initial_elevation)); + return result; +} +} + +std::string GCodeGenerator::generate_travel_gcode( + const Points3& travel, + const std::string& comment +) { + std::string gcode; + + const unsigned acceleration =(unsigned)(m_config.travel_acceleration.value + 0.5); + + if (travel.empty()) { + return ""; + } // generate G-code for the travel move - std::string gcode; - if (needs_retraction) { - if (m_config.avoid_crossing_perimeters && could_be_wipe_disabled) - m_wipe.reset_path(); - - Point last_post_before_retract = this->last_pos(); - gcode += this->retract(); - // When "Wipe while retracting" is enabled, then extruder moves to another position, and travel from this position can cross perimeters. - // Because of it, it is necessary to call avoid crossing perimeters again with new starting point after calling retraction() - // FIXME Lukas H.: Try to predict if this second calling of avoid crossing perimeters will be needed or not. It could save computations. - if (last_post_before_retract != this->last_pos() && m_config.avoid_crossing_perimeters) { - // If in the previous call of m_avoid_crossing_perimeters.travel_to was use_external_mp_once set to true restore this value for next call. - if (used_external_mp_once) - m_avoid_crossing_perimeters.use_external_mp_once(); - travel = m_avoid_crossing_perimeters.travel_to(*this, point); - // If state of use_external_mp_once was changed reset it to right value. - if (used_external_mp_once) - m_avoid_crossing_perimeters.reset_once_modifiers(); - } - } else - // Reset the wipe path when traveling, so one would not wipe along an old path. - m_wipe.reset_path(); - // use G1 because we rely on paths being straight (G0 may make round paths) - if (travel.size() >= 2) { + gcode += this->m_writer.set_travel_acceleration(acceleration); - gcode += m_writer.set_travel_acceleration((unsigned int)(m_config.travel_acceleration.value + 0.5)); - - for (size_t i = 1; i < travel.size(); ++ i) - gcode += m_writer.travel_to_xy(this->point_to_gcode(travel.points[i]), comment); - - if (! GCodeWriter::supports_separate_travel_acceleration(config().gcode_flavor)) { - // In case that this flavor does not support separate print and travel acceleration, - // reset acceleration to default. - gcode += m_writer.set_travel_acceleration((unsigned int)(m_config.travel_acceleration.value + 0.5)); - } - - this->set_last_pos(travel.points.back()); + for (const Vec3crd& point : travel) { + gcode += this->m_writer.travel_to_xyz(to_3d(this->point_to_gcode(point.head<2>()), unscaled(point.z())), comment); + this->set_last_pos(point.head<2>()); } + + if (! GCodeWriter::supports_separate_travel_acceleration(config().gcode_flavor)) { + // In case that this flavor does not support separate print and travel acceleration, + // reset acceleration to default. + gcode += this->m_writer.set_travel_acceleration(acceleration); + } + return gcode; } @@ -3249,7 +3388,104 @@ bool GCodeGenerator::needs_retraction(const Polyline &travel, ExtrusionRole role return true; } -std::string GCodeGenerator::retract(bool toolchange) +Polyline GCodeGenerator::generate_travel_xy_path( + const Point& start_point, + const Point& end_point, + const bool needs_retraction, + bool& could_be_wipe_disabled +) { + + const Point scaled_origin{scaled(this->origin())}; + const bool avoid_crossing_perimeters = ( + this->m_config.avoid_crossing_perimeters + && !this->m_avoid_crossing_perimeters.disabled_once() + ); + + Polyline xy_path{start_point, end_point}; + if (m_config.avoid_crossing_curled_overhangs) { + if (avoid_crossing_perimeters) { + BOOST_LOG_TRIVIAL(warning) + << "Option >avoid crossing curled overhangs< is not compatible with avoid crossing perimeters and it will be ignored!"; + } else { + xy_path = this->m_avoid_crossing_curled_overhangs.find_path( + start_point + scaled_origin, + end_point + scaled_origin + ); + xy_path.translate(-scaled_origin); + } + } + + + // if a retraction would be needed, try to use avoid_crossing_perimeters to plan a + // multi-hop travel path inside the configuration space + if ( + needs_retraction + && avoid_crossing_perimeters + ) { + xy_path = this->m_avoid_crossing_perimeters.travel_to(*this, end_point, &could_be_wipe_disabled); + } + + return xy_path; +} + +// This method accepts &point in print coordinates. +std::string GCodeGenerator::travel_to(const Point &point, ExtrusionRole role, std::string comment) +{ + + const Point start_point = this->last_pos(); + + using namespace GCode::Impl; + + // check whether a straight travel move would need retraction + + bool could_be_wipe_disabled {false}; + bool needs_retraction = this->needs_retraction(Polyline{start_point, point}, role); + + Polyline xy_path{generate_travel_xy_path( + start_point, point, needs_retraction, could_be_wipe_disabled + )}; + + needs_retraction = this->needs_retraction(xy_path, role); + + std::string wipe_retract_gcode{}; + if (needs_retraction) { + if (could_be_wipe_disabled) { + m_wipe.reset_path(); + } + + Point position_before_wipe{this->last_pos()}; + wipe_retract_gcode = this->retract_and_wipe(); + + if (this->last_pos() != position_before_wipe) { + xy_path = generate_travel_xy_path( + this->last_pos(), point, needs_retraction, could_be_wipe_disabled + ); + } + } else { + m_wipe.reset_path(); + } + + this->m_avoid_crossing_perimeters.reset_once_modifiers(); + + const unsigned extruder_id = this->m_writer.extruder()->id(); + const double retract_length = this->m_config.retract_length.get_at(extruder_id); + bool can_be_flat{!needs_retraction || retract_length == 0}; + const double initial_elevation = this->m_last_layer_z + this->m_config.z_offset.value; + const Points3 travel = ( + can_be_flat ? + generate_flat_travel(xy_path.points, initial_elevation) : + GCode::Impl::generate_travel_to_extrusion( + xy_path, + this->m_config, + extruder_id, + initial_elevation + ) + ); + + return wipe_retract_gcode + generate_travel_gcode(travel, comment); +} + +std::string GCodeGenerator::retract_and_wipe(bool toolchange) { std::string gcode; @@ -3267,10 +3503,7 @@ std::string GCodeGenerator::retract(bool toolchange) methods even if we performed wipe, since this will ensure the entire retraction length is honored in case wipe path was too short. */ gcode += toolchange ? m_writer.retract_for_toolchange() : m_writer.retract(); - gcode += m_writer.reset_e(); - if (m_writer.extruder()->retract_length() > 0 || m_config.use_firmware_retraction) - gcode += m_writer.lift(); return gcode; } @@ -3302,7 +3535,7 @@ std::string GCodeGenerator::set_extruder(unsigned int extruder_id, double print_ } // prepend retraction on the current extruder - std::string gcode = this->retract(true); + std::string gcode = this->retract_and_wipe(true); // Always reset the extrusion path, even if the tool change retract is set to zero. m_wipe.reset_path(); @@ -3378,6 +3611,9 @@ std::string GCodeGenerator::set_extruder(unsigned int extruder_id, double print_ if (m_ooze_prevention.enable) gcode += m_ooze_prevention.post_toolchange(*this); + // The position is now known after the tool change. + this->m_last_pos_defined = false; + return gcode; } diff --git a/src/libslic3r/GCode.hpp b/src/libslic3r/GCode.hpp index d25113b0d2..efa75fcd67 100644 --- a/src/libslic3r/GCode.hpp +++ b/src/libslic3r/GCode.hpp @@ -40,6 +40,7 @@ #include "GCode/GCodeProcessor.hpp" #include "EdgeGrid.hpp" #include "GCode/ThumbnailData.hpp" +#include "tcbspan/span.hpp" #include #include @@ -88,6 +89,53 @@ struct LayerResult { static LayerResult make_nop_layer_result() { return {"", std::numeric_limits::max(), false, false, true}; } }; +namespace GCode::Impl { +struct DistancedPoint { + Point point; + double distance_from_start; +}; + +/** + * @brief Takes a path described as a list of points and adds points to it. + * + * @param xy_path A list of points describing a path in xy. + * @param sorted_distances A sorted list of distances along the path. + * @return Sliced path. + * + * The algorithm travels along the path segments and adds points to + * the segments in such a way that the points have specified distances + * from the xy_path start. **Any distances over the xy_path end will + * be simply ignored.** + * + * Example usage - simplified for clarity: + * @code + * std::vector distances{0.5, 1.5}; + * std::vector xy_path{{0, 0}, {1, 0}}; + * // produces + * {{0, 0}, {0, 0.5}, {1, 0}} + * // notice that 1.5 is omitted + * @endcode + */ +std::vector slice_xy_path(tcb::span xy_path, tcb::span sorted_distances); + +/** + * @brief Take xy_path and genrate a travel acording to elevation. + * + * @param xy_path A list of points describing a path in xy. + * @param ensure_points_at_distances See slice_xy_path sorted_distances. + * @param elevation A function taking current distance in mm as input and returning elevation in mm as output. + * + * **Be aweare** that the elevation function operates in mm, while xy_path and returned travel are in + * scaled coordinates. + */ +Points3 generate_elevated_travel( + const tcb::span xy_path, + const std::vector& ensure_points_at_distances, + const double initial_elevation, + const std::function& elevation +); + +} class GCodeGenerator { public: @@ -303,11 +351,21 @@ private: const bool print_wipe_extrusions); std::string extrude_support(const ExtrusionEntityReferences &support_fills, const GCode::SmoothPathCache &smooth_path_cache); - + std::string generate_travel_gcode( + const Points3& travel, + const std::string& comment + ); + Polyline generate_travel_xy_path( + const Point& start, + const Point& end, + const bool needs_retraction, + bool& could_be_wipe_disabled + ); std::string travel_to(const Point &point, ExtrusionRole role, std::string comment); bool needs_retraction(const Polyline &travel, ExtrusionRole role = ExtrusionRole::None); - std::string retract(bool toolchange = false); - std::string unretract() { return m_writer.unlift() + m_writer.unretract(); } + + std::string retract_and_wipe(bool toolchange = false); + std::string unretract() { return m_writer.unretract(); } std::string set_extruder(unsigned int extruder_id, double print_z); // Cache for custom seam enforcers/blockers for each layer. @@ -336,8 +394,8 @@ private: // Input/output from/to custom G-code block, for returning position, retraction etc. DynamicConfig output_config; ConfigOptionFloats *opt_position { nullptr }; - ConfigOptionFloat *opt_zhop { nullptr }; ConfigOptionFloats *opt_e_position { nullptr }; + ConfigOptionFloat *opt_zhop { nullptr }; ConfigOptionFloats *opt_e_retracted { nullptr }; ConfigOptionFloats *opt_e_restart_extra { nullptr }; ConfigOptionFloats *opt_extruded_volume { nullptr }; diff --git a/src/libslic3r/GCode/AvoidCrossingPerimeters.cpp b/src/libslic3r/GCode/AvoidCrossingPerimeters.cpp index 04d3aa36c6..782ebc306f 100644 --- a/src/libslic3r/GCode/AvoidCrossingPerimeters.cpp +++ b/src/libslic3r/GCode/AvoidCrossingPerimeters.cpp @@ -742,7 +742,7 @@ static bool need_wipe(const GCodeGenerator &gcodegen, const Polyline &result_travel, const size_t intersection_count) { - bool z_lift_enabled = gcodegen.config().retract_lift.get_at(gcodegen.writer().extruder()->id()) > 0.; + bool z_lift_enabled = gcodegen.config().travel_max_lift.get_at(gcodegen.writer().extruder()->id()) > 0.; bool wipe_needed = false; // If the original unmodified path doesn't have any intersection with boundary, then it is entirely inside the object otherwise is entirely diff --git a/src/libslic3r/GCode/GCodeWriter.cpp b/src/libslic3r/GCode/GCodeWriter.cpp index 8af0c1344b..5231f97cd8 100644 --- a/src/libslic3r/GCode/GCodeWriter.cpp +++ b/src/libslic3r/GCode/GCodeWriter.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #ifdef __APPLE__ #include @@ -277,7 +278,7 @@ std::string GCodeWriter::set_speed(double F, const std::string_view comment, con std::string GCodeWriter::travel_to_xy(const Vec2d &point, const std::string_view comment) { m_pos.head<2>() = point.head<2>(); - + GCodeG1Formatter w; w.emit_xy(point); w.emit_f(this->config.travel_speed.value * 60.0); @@ -304,64 +305,37 @@ std::string GCodeWriter::travel_to_xy_G2G3IJ(const Vec2d &point, const Vec2d &ij std::string GCodeWriter::travel_to_xyz(const Vec3d &point, const std::string_view comment) { - // FIXME: This function was not being used when travel_speed_z was separated (bd6badf). - // Calculation of feedrate was not updated accordingly. If you want to use - // this function, fix it first. - std::terminate(); + if (std::abs(point.x() - m_pos.x()) < EPSILON && std::abs(point.y() - m_pos.y()) < EPSILON) { + return this->travel_to_z(point.z(), comment); + } else if (std::abs(point.z() - m_pos.z()) < EPSILON) { + return this->travel_to_xy(point.head<2>(), comment); + } else { + m_pos = point; - /* If target Z is lower than current Z but higher than nominal Z we - don't perform the Z move but we only move in the XY plane and - adjust the nominal Z by reducing the lift amount that will be - used for unlift. */ - if (!this->will_move_z(point.z())) { - double nominal_z = m_pos.z() - m_lifted; - m_lifted -= (point.z() - nominal_z); - // In case that retract_lift == layer_height we could end up with almost zero in_m_lifted - // and a retract could be skipped (https://github.com/prusa3d/PrusaSlicer/issues/2154 - if (std::abs(m_lifted) < EPSILON) - m_lifted = 0.; - return this->travel_to_xy(to_2d(point)); + GCodeG1Formatter w; + w.emit_xyz(point); + + Vec2f speed {this->config.travel_speed_z.value, this->config.travel_speed.value}; + w.emit_f(speed.norm() * 60.0); + w.emit_comment(this->config.gcode_comments, comment); + return w.string(); } - - /* In all the other cases, we perform an actual XYZ move and cancel - the lift. */ - m_lifted = 0; - m_pos = point; - - GCodeG1Formatter w; - w.emit_xyz(point); - w.emit_f(this->config.travel_speed.value * 60.0); - w.emit_comment(this->config.gcode_comments, comment); - return w.string(); } + std::string GCodeWriter::travel_to_z(double z, const std::string_view comment) { - /* If target Z is lower than current Z but higher than nominal Z - we don't perform the move but we only adjust the nominal Z by - reducing the lift amount that will be used for unlift. */ - if (!this->will_move_z(z)) { - double nominal_z = m_pos.z() - m_lifted; - m_lifted -= (z - nominal_z); - if (std::abs(m_lifted) < EPSILON) - m_lifted = 0.; - return {}; - } - - /* In all the other cases, we perform an actual Z move and cancel - the lift. */ - m_lifted = 0; - return this->_travel_to_z(z, comment); + return std::abs(m_pos.z() - z) < EPSILON ? "" : this->get_travel_to_z_gcode(z, comment); } -std::string GCodeWriter::_travel_to_z(double z, const std::string_view comment) +std::string GCodeWriter::get_travel_to_z_gcode(double z, const std::string_view comment) { m_pos.z() = z; double speed = this->config.travel_speed_z.value; if (speed == 0.) speed = this->config.travel_speed.value; - + GCodeG1Formatter w; w.emit_z(z); w.emit_f(speed * 60.0); @@ -369,18 +343,6 @@ std::string GCodeWriter::_travel_to_z(double z, const std::string_view comment) return w.string(); } -bool GCodeWriter::will_move_z(double z) const -{ - /* If target Z is lower than current Z but higher than nominal Z - we don't perform an actual Z move. */ - if (m_lifted > 0) { - double nominal_z = m_pos.z() - m_lifted; - if (z >= nominal_z && z <= m_pos.z()) - return false; - } - return true; -} - std::string GCodeWriter::extrude_to_xy(const Vec2d &point, double dE, const std::string_view comment) { assert(dE != 0); @@ -514,47 +476,8 @@ std::string GCodeWriter::unretract() return gcode; } -/* If this method is called more than once before calling unlift(), - it will not perform subsequent lifts, even if Z was raised manually - (i.e. with travel_to_z()) and thus _lifted was reduced. */ -std::string GCodeWriter::lift() -{ - // check whether the above/below conditions are met - double target_lift = 0; - { - double above = this->config.retract_lift_above.get_at(m_extruder->id()); - double below = this->config.retract_lift_below.get_at(m_extruder->id()); - if (m_pos.z() >= above && (below == 0 || m_pos.z() <= below)) - target_lift = this->config.retract_lift.get_at(m_extruder->id()); - } - if (m_lifted == 0 && target_lift > 0) { - m_lifted = target_lift; - return this->_travel_to_z(m_pos.z() + target_lift, "lift Z"); - } - return {}; -} - -std::string GCodeWriter::unlift() -{ - std::string gcode; - if (m_lifted > 0) { - gcode += this->_travel_to_z(m_pos.z() - m_lifted, "restore layer Z"); - m_lifted = 0; - } - return gcode; -} - void GCodeWriter::update_position(const Vec3d &new_pos) { - assert(this->m_lifted >= 0); - const double nominal_z = m_pos.z() - m_lifted; - m_lifted = new_pos.z() - nominal_z; - if (m_lifted < - EPSILON) - throw Slic3r::RuntimeError("Custom G-code reports negative Z-hop. Final Z position is below the print_z height."); - // In case that retract_lift == layer_height we could end up with almost zero in_m_lifted - // and a retract could be skipped (https://github.com/prusa3d/PrusaSlicer/issues/2154 - if (m_lifted < EPSILON) - m_lifted = 0.; m_pos = new_pos; } diff --git a/src/libslic3r/GCode/GCodeWriter.hpp b/src/libslic3r/GCode/GCodeWriter.hpp index 72fcfe357a..b51b6269c5 100644 --- a/src/libslic3r/GCode/GCodeWriter.hpp +++ b/src/libslic3r/GCode/GCodeWriter.hpp @@ -30,8 +30,7 @@ public: multiple_extruders(false), m_extrusion_axis("E"), m_extruder(nullptr), m_single_extruder_multi_material(false), m_last_acceleration(0), m_max_acceleration(0), - m_last_bed_temperature(0), m_last_bed_temperature_reached(true), - m_lifted(0) + m_last_bed_temperature(0), m_last_bed_temperature_reached(true) {} Extruder* extruder() { return m_extruder; } const Extruder* extruder() const { return m_extruder; } @@ -70,23 +69,21 @@ public: std::string travel_to_xy(const Vec2d &point, const std::string_view comment = {}); std::string travel_to_xy_G2G3IJ(const Vec2d &point, const Vec2d &ij, const bool ccw, const std::string_view comment = {}); std::string travel_to_xyz(const Vec3d &point, const std::string_view comment = {}); + std::string get_travel_to_z_gcode(double z, const std::string_view comment); std::string travel_to_z(double z, const std::string_view comment = {}); - bool will_move_z(double z) const; std::string extrude_to_xy(const Vec2d &point, double dE, const std::string_view comment = {}); std::string extrude_to_xy_G2G3IJ(const Vec2d &point, const Vec2d &ij, const bool ccw, double dE, const std::string_view comment); // std::string extrude_to_xyz(const Vec3d &point, double dE, const std::string_view comment = {}); std::string retract(bool before_wipe = false); std::string retract_for_toolchange(bool before_wipe = false); std::string unretract(); - std::string lift(); - std::string unlift(); // Current position of the printer, in G-code coordinates. // Z coordinate of current position contains zhop. If zhop is applied (this->zhop() > 0), // then the print_z = this->get_position().z() - this->zhop(). Vec3d get_position() const { return m_pos; } - // Current Z hop value. - double get_zhop() const { return m_lifted; } + // Zhop value is obsolete. This is for backwards compability. + double get_zhop() const { return 0; } // Update position of the print head based on the final position returned by a custom G-code block. // The new position Z coordinate contains the Z-hop. // GCodeWriter expects the custom script to NOT change print_z, only Z-hop, thus the print_z is maintained @@ -117,7 +114,6 @@ private: unsigned int m_last_bed_temperature; bool m_last_bed_temperature_reached; - double m_lifted; Vec3d m_pos = Vec3d::Zero(); enum class Acceleration { @@ -125,7 +121,6 @@ private: Print }; - std::string _travel_to_z(double z, const std::string_view comment); std::string _retract(double length, double restart_extra, const std::string_view comment); std::string set_acceleration_internal(Acceleration type, unsigned int acceleration); }; diff --git a/src/libslic3r/GCode/WipeTower.cpp b/src/libslic3r/GCode/WipeTower.cpp index a899f2f2be..447dbbd0e4 100644 --- a/src/libslic3r/GCode/WipeTower.cpp +++ b/src/libslic3r/GCode/WipeTower.cpp @@ -537,6 +537,7 @@ WipeTower::WipeTower(const PrintConfig& config, const PrintRegionConfig& default m_no_sparse_layers(config.wipe_tower_no_sparse_layers), m_gcode_flavor(config.gcode_flavor), m_travel_speed(config.travel_speed), + m_travel_speed_z(config.travel_speed_z), m_infill_speed(default_region_config.infill_speed), m_perimeter_speed(default_region_config.perimeter_speed), m_current_tool(initial_tool), @@ -1018,7 +1019,11 @@ void WipeTower::toolchange_Change( writer.feedrate(m_travel_speed * 60.f) // see https://github.com/prusa3d/PrusaSlicer/issues/5483 .append(std::string("G1 X") + Slic3r::float_to_string_decimal_point(current_pos.x()) + " Y" + Slic3r::float_to_string_decimal_point(current_pos.y()) - + never_skip_tag() + "\n"); + + never_skip_tag() + "\n" + ); + writer.feedrate(m_travel_speed_z * 60.f) + .append("G1 Z" + Slic3r::float_to_string_decimal_point(this->m_z_pos) + "\n"); + writer.append("[deretraction_from_wipe_tower_generator]"); // The toolchange Tn command will be inserted later, only in case that the user does diff --git a/src/libslic3r/GCode/WipeTower.hpp b/src/libslic3r/GCode/WipeTower.hpp index ff2fabdf0f..06919cd4ce 100644 --- a/src/libslic3r/GCode/WipeTower.hpp +++ b/src/libslic3r/GCode/WipeTower.hpp @@ -279,6 +279,7 @@ private: size_t m_max_color_changes = 0; // Maximum number of color changes per layer. 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) float m_travel_speed = 0.f; + float m_travel_speed_z = 0.f; float m_infill_speed = 0.f; float m_perimeter_speed = 0.f; float m_first_layer_speed = 0.f; diff --git a/src/libslic3r/GCode/WipeTowerIntegration.cpp b/src/libslic3r/GCode/WipeTowerIntegration.cpp index 7ad3ee518b..5beca23ba2 100644 --- a/src/libslic3r/GCode/WipeTowerIntegration.cpp +++ b/src/libslic3r/GCode/WipeTowerIntegration.cpp @@ -41,9 +41,9 @@ std::string WipeTowerIntegration::append_tcr(GCodeGenerator &gcodegen, const Wip std::string tcr_rotated_gcode = post_process_wipe_tower_moves(tcr, wipe_tower_offset, wipe_tower_rotation); - gcode += gcodegen.writer().unlift(); // Make sure there is no z-hop (in most cases, there isn't). - double current_z = gcodegen.writer().get_position().z(); + gcode += gcodegen.writer().travel_to_z(current_z); + if (z == -1.) // in case no specific z was provided, print at current_z pos z = current_z; @@ -57,7 +57,7 @@ std::string WipeTowerIntegration::append_tcr(GCodeGenerator &gcodegen, const Wip || is_ramming || will_go_down); // don't dig into the print if (should_travel_to_tower) { - gcode += gcodegen.retract(); + gcode += gcodegen.retract_and_wipe(); gcodegen.m_avoid_crossing_perimeters.use_external_mp_once(); gcode += gcodegen.travel_to( wipe_tower_point_to_object_point(gcodegen, start_pos), @@ -150,12 +150,15 @@ std::string WipeTowerIntegration::post_process_wipe_tower_moves(const WipeTower: } std::ostringstream line_out; std::istringstream line_str(line); + std::optional z{}; line_str >> std::noskipws; // don't skip whitespace char ch = 0; line_str >> ch >> ch; // read the "G1" while (line_str >> ch) { if (ch == 'X' || ch == 'Y') line_str >> (ch == 'X' ? pos.x() : pos.y()); + else if (ch == 'Z') + line_str >> *z; else line_out << ch; } @@ -171,6 +174,8 @@ std::string WipeTowerIntegration::post_process_wipe_tower_moves(const WipeTower: oss << " X" << transformed_pos.x() - extruder_offset.x(); if (transformed_pos.y() != old_pos.y() || never_skip) oss << " Y" << transformed_pos.y() - extruder_offset.y(); + if (z) + oss << " Z" << *z; if (! line.empty()) oss << " "; line = oss.str() + line; @@ -243,7 +248,10 @@ std::string WipeTowerIntegration::finalize(GCodeGenerator &gcodegen) { std::string gcode; if (std::abs(gcodegen.writer().get_position().z() - m_final_purge.print_z) > EPSILON) - gcode += gcodegen.change_layer(m_final_purge.print_z); + gcode += gcodegen.generate_travel_gcode( + {{gcodegen.last_pos().x(), gcodegen.last_pos().y(), scaled(m_final_purge.print_z)}}, + "move to safe place for purging" + ); gcode += append_tcr(gcodegen, m_final_purge, -1); return gcode; } diff --git a/t/retraction.t b/t/retraction.t index 6e6a130ca3..f773c3f080 100644 --- a/t/retraction.t +++ b/t/retraction.t @@ -24,7 +24,7 @@ use Slic3r::Test qw(_eq); my $tool = 0; my @toolchange_count = (); # track first usages so that we don't expect retract_length_toolchange when extruders are used for the first time - my @retracted = (1); # ignore the first travel move from home to first point + my @retracted = (0); my @retracted_length = (0); my $lifted = 0; my $lift_dist = 0; # track lifted distance for toolchanges and extruders with different retract_lift values @@ -91,7 +91,7 @@ use Slic3r::Test qw(_eq); fail 'retracted before long travel move' if !$retracted[$tool]; } }); - + 1; }; @@ -155,7 +155,7 @@ use Slic3r::Test qw(_eq); if ($info->{dist_Z} && $retracted) { $layer_changes_with_retraction++; } - if ($info->{dist_Z} && $args->{Z} < $self->Z) { + if ($info->{dist_Z} && $args->{Z} < $self->{Z}) { $z_restores++; } }); diff --git a/tests/fff_print/test_gcode.cpp b/tests/fff_print/test_gcode.cpp index 3ec1758b4e..bf68a6d424 100644 --- a/tests/fff_print/test_gcode.cpp +++ b/tests/fff_print/test_gcode.cpp @@ -5,6 +5,7 @@ #include "libslic3r/GCode.hpp" using namespace Slic3r; +using namespace Slic3r::GCode::Impl; SCENARIO("Origin manipulation", "[GCode]") { Slic3r::GCodeGenerator gcodegen; @@ -20,3 +21,121 @@ SCENARIO("Origin manipulation", "[GCode]") { } } } + +struct ApproxEqualsPoints : public Catch::MatcherBase { + ApproxEqualsPoints(const Points& expected, unsigned tolerance): expected(expected), tolerance(tolerance) {} + bool match(const Points& points) const override { + if (points.size() != expected.size()) { + return false; + } + for (auto i = 0u; i < points.size(); ++i) { + const Point& point = points[i]; + const Point& expected_point = this->expected[i]; + if ( + std::abs(point.x() - expected_point.x()) > this->tolerance + || std::abs(point.y() - expected_point.y()) > this->tolerance + ) { + return false; + } + } + return true; + } + std::string describe() const override { + std::stringstream ss; + ss << std::endl; + for (const Point& point : expected) { + ss << "(" << point.x() << ", " << point.y() << ")" << std::endl; + } + ss << "With tolerance: " << this->tolerance; + + return "Equals " + ss.str(); + } + +private: + Points expected; + unsigned tolerance; +}; + +Points get_points(const std::vector& result) { + Points result_points; + std::transform( + result.begin(), + result.end(), + std::back_inserter(result_points), + [](const DistancedPoint& point){ + return point.point; + } + ); + return result_points; +} + +std::vector get_distances(const std::vector& result) { + std::vector result_distances; + std::transform( + result.begin(), + result.end(), + std::back_inserter(result_distances), + [](const DistancedPoint& point){ + return point.distance_from_start; + } + ); + return result_distances; +} + +TEST_CASE("Place points at distances - expected use", "[GCode]") { + std::vector line{ + scaled(Vec2f{0, 0}), + scaled(Vec2f{1, 0}), + scaled(Vec2f{2, 1}), + scaled(Vec2f{2, 2}) + }; + std::vector distances{0, 0.2, 0.5, 1 + std::sqrt(2)/2, 1 + std::sqrt(2) + 0.5, 100.0}; + std::vector result = slice_xy_path(line, distances); + + REQUIRE_THAT(get_points(result), ApproxEqualsPoints(Points{ + scaled(Vec2f{0, 0}), + scaled(Vec2f{0.2, 0}), + scaled(Vec2f{0.5, 0}), + scaled(Vec2f{1, 0}), + scaled(Vec2f{1.5, 0.5}), + scaled(Vec2f{2, 1}), + scaled(Vec2f{2, 1.5}), + scaled(Vec2f{2, 2}) + }, 5)); + + REQUIRE_THAT(get_distances(result), Catch::Matchers::Approx(std::vector{ + distances[0], distances[1], distances[2], 1, distances[3], 1 + std::sqrt(2), distances[4], 2 + std::sqrt(2) + })); +} + +TEST_CASE("Place points at distances - edge case", "[GCode]") { + std::vector line{ + scaled(Vec2f{0, 0}), + scaled(Vec2f{1, 0}), + scaled(Vec2f{2, 0}) + }; + std::vector distances{0, 1, 1.5, 2}; + Points result{get_points(slice_xy_path(line, distances))}; + CHECK(result == Points{ + scaled(Vec2f{0, 0}), + scaled(Vec2f{1, 0}), + scaled(Vec2f{1.5, 0}), + scaled(Vec2f{2, 0}) + }); +} + +TEST_CASE("Generate elevated travel", "[GCode]") { + std::vector xy_path{ + scaled(Vec2f{0, 0}), + scaled(Vec2f{1, 0}), + }; + std::vector ensure_points_at_distances{0.2, 0.5}; + Points3 result{generate_elevated_travel(xy_path, ensure_points_at_distances, 2.0, [](double x){return 1 + x;})}; + + CHECK(result == Points3{ + scaled(Vec3f{0, 0, 3.0}), + scaled(Vec3f{0.2, 0, 3.2}), + scaled(Vec3f{0.5, 0, 3.5}), + scaled(Vec3f{1, 0, 4.0}) + }); +} diff --git a/tests/fff_print/test_gcodewriter.cpp b/tests/fff_print/test_gcodewriter.cpp index 35c70ad0c8..93888a988f 100644 --- a/tests/fff_print/test_gcodewriter.cpp +++ b/tests/fff_print/test_gcodewriter.cpp @@ -6,68 +6,6 @@ using namespace Slic3r; -SCENARIO("lift() is not ignored after unlift() at normal values of Z", "[GCodeWriter]") { - GIVEN("A config from a file and a single extruder.") { - GCodeWriter writer; - GCodeConfig &config = writer.config; - config.load(std::string(TEST_DATA_DIR) + "/fff_print_tests/test_gcodewriter/config_lift_unlift.ini", ForwardCompatibilitySubstitutionRule::Disable); - - std::vector extruder_ids {0}; - writer.set_extruders(extruder_ids); - writer.set_extruder(0); - - WHEN("Z is set to 203") { - double trouble_Z = 203; - writer.travel_to_z(trouble_Z); - AND_WHEN("GcodeWriter::Lift() is called") { - REQUIRE(writer.lift().size() > 0); - AND_WHEN("Z is moved post-lift to the same delta as the config Z lift") { - REQUIRE(writer.travel_to_z(trouble_Z + config.retract_lift.values[0]).size() == 0); - AND_WHEN("GCodeWriter::Unlift() is called") { - REQUIRE(writer.unlift().size() == 0); // we're the same height so no additional move happens. - THEN("GCodeWriter::Lift() emits gcode.") { - REQUIRE(writer.lift().size() > 0); - } - } - } - } - } - WHEN("Z is set to 500003") { - double trouble_Z = 500003; - writer.travel_to_z(trouble_Z); - AND_WHEN("GcodeWriter::Lift() is called") { - REQUIRE(writer.lift().size() > 0); - AND_WHEN("Z is moved post-lift to the same delta as the config Z lift") { - REQUIRE(writer.travel_to_z(trouble_Z + config.retract_lift.values[0]).size() == 0); - AND_WHEN("GCodeWriter::Unlift() is called") { - REQUIRE(writer.unlift().size() == 0); // we're the same height so no additional move happens. - THEN("GCodeWriter::Lift() emits gcode.") { - REQUIRE(writer.lift().size() > 0); - } - } - } - } - } - WHEN("Z is set to 10.3") { - double trouble_Z = 10.3; - writer.travel_to_z(trouble_Z); - AND_WHEN("GcodeWriter::Lift() is called") { - REQUIRE(writer.lift().size() > 0); - AND_WHEN("Z is moved post-lift to the same delta as the config Z lift") { - REQUIRE(writer.travel_to_z(trouble_Z + config.retract_lift.values[0]).size() == 0); - AND_WHEN("GCodeWriter::Unlift() is called") { - REQUIRE(writer.unlift().size() == 0); // we're the same height so no additional move happens. - THEN("GCodeWriter::Lift() emits gcode.") { - REQUIRE(writer.lift().size() > 0); - } - } - } - } - } - // The test above will fail for trouble_Z == 9007199254740992, where trouble_Z + 1.5 will be rounded to trouble_Z + 2.0 due to double mantisa overflow. - } -} - SCENARIO("set_speed emits values with fixed-point output.", "[GCodeWriter]") { GIVEN("GCodeWriter instance") {