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.
This commit is contained in:
Martin Šach 2023-11-07 16:12:11 +01:00 committed by SachCZ
parent 09466df1d4
commit 49455cf427
12 changed files with 544 additions and 266 deletions

View File

@ -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));

View File

@ -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;
}
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));
}
}
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();
}
}
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<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
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;
}

View File

@ -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 };

View File

@ -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

View File

@ -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 (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;
}

View File

@ -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);
};

View File

@ -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

View File

@ -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;

View File

@ -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;
}

View File

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

View File

@ -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<Points> {
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<DistancedPoint>& 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<double> get_distances(const std::vector<DistancedPoint>& result) {
std::vector<double> 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<Point> line{
scaled(Vec2f{0, 0}),
scaled(Vec2f{1, 0}),
scaled(Vec2f{2, 1}),
scaled(Vec2f{2, 2})
};
std::vector<double> distances{0, 0.2, 0.5, 1 + std::sqrt(2)/2, 1 + std::sqrt(2) + 0.5, 100.0};
std::vector<DistancedPoint> 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<double>{
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<Point> line{
scaled(Vec2f{0, 0}),
scaled(Vec2f{1, 0}),
scaled(Vec2f{2, 0})
};
std::vector<double> 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<Point> xy_path{
scaled(Vec2f{0, 0}),
scaled(Vec2f{1, 0}),
};
std::vector<double> 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})
});
}

View File

@ -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<unsigned int> 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") {