Browse Source

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.
Martin Šach 1 year ago
parent
commit
49455cf427

+ 0 - 5
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));

+ 313 - 77
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<Extruder> &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<Point> pt = wipe_hide_seam(smooth_path, is_hole, scale_(EXTRUDER_CONFIG(nozzle_diameter))); pt)
+        if (std::optional<Point> 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<const Point> 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<DistancedPoint> slice_xy_path(tcb::span<const Point> xy_path, tcb::span<const double> sorted_distances) {
+    assert(xy_path.size() >= 2);
+    std::vector<DistancedPoint> 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<const Point> xy_path,
+    const std::vector<double>& ensure_points_at_distances,
+    const double initial_elevation,
+    const std::function<double(double)>& elevation
+) {
+    Points3 result{};
+
+    std::vector<DistancedPoint> 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;
+}
 
-    // 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();
+AABBTreeLines::LinesDistancer<Linef> get_expolygons_distancer(const ExPolygons& polygons) {
+    std::vector<Linef> lines;
+    for (const ExPolygon& polygon : polygons) {
+        for (const Line& line : polygon.lines()) {
+            lines.emplace_back(unscaled(line.a), unscaled(line.b));
+        }
+    }
 
-        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();
+    return AABBTreeLines::LinesDistancer{std::move(lines)};
+}
+
+std::optional<double> get_first_crossed_line_distance(
+    tcb::span<const Line> xy_path,
+    const AABBTreeLines::LinesDistancer<Linef>& 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<true>(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();
+            }
         }
-    } else
-        // Reset the wipe path when traveling, so one would not wipe along an old path.
-        m_wipe.reset_path();
+        traversed_distance += (unscaled_line.a - unscaled_line.b).norm();
+    }
 
-    // use G1 because we rely on paths being straight (G0 may make round paths)
-    if (travel.size() >= 2) {
+    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);
 
-        gcode += m_writer.set_travel_acceleration((unsigned int)(m_config.travel_acceleration.value + 0.5));
+    const double slope_deg = config.travel_slope.get_at(extruder_id);
 
-        for (size_t i = 1; i < travel.size(); ++ i)
-            gcode += m_writer.travel_to_xy(this->point_to_gcode(travel.points[i]), comment);
+    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);
+    }
 
-        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));
-        }
+    return elevation_params;
+}
 
-        this->set_last_pos(travel.points.back());
+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<double> 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
+    // use G1 because we rely on paths being straight (G0 may make round paths)
+    gcode += this->m_writer.set_travel_acceleration(acceleration);
+
+    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;
 }
 

+ 62 - 4
src/libslic3r/GCode.hpp

@@ -40,6 +40,7 @@
 #include "GCode/GCodeProcessor.hpp"
 #include "EdgeGrid.hpp"
 #include "GCode/ThumbnailData.hpp"
+#include "tcbspan/span.hpp"
 
 #include <memory>
 #include <map>
@@ -88,6 +89,53 @@ struct LayerResult {
     static LayerResult make_nop_layer_result() { return {"", std::numeric_limits<coord_t>::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<double> distances{0.5, 1.5};
+ * std::vector<Points> xy_path{{0, 0}, {1, 0}};
+ * // produces
+ * {{0, 0}, {0, 0.5}, {1, 0}}
+ * // notice that 1.5 is omitted
+ * @endcode
+ */
+std::vector<DistancedPoint> slice_xy_path(tcb::span<const Point> xy_path, tcb::span<const double> 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<const Point> xy_path,
+    const std::vector<double>& ensure_points_at_distances,
+    const double initial_elevation,
+    const std::function<double(double)>& 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 };

+ 1 - 1
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

+ 20 - 97
src/libslic3r/GCode/GCodeWriter.cpp

@@ -20,6 +20,7 @@
 #include <map>
 #include <assert.h>
 #include <string_view>
+#include <boost/math/special_functions/pow.hpp>
 
 #ifdef __APPLE__
     #include <boost/spirit/include/karma.hpp>
@@ -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 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));
+    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;
+
+        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;
 }
 

+ 4 - 9
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);
 };

+ 6 - 1
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

+ 1 - 0
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;

+ 12 - 4
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<float> 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;
 }

+ 3 - 3
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++;
         }
     });

Some files were not shown because too many files changed in this diff