Prusa 2.7.2

This commit is contained in:
sunsets
2024-03-27 14:38:03 +08:00
parent 63daf0c087
commit 2387bc9cdb
203 changed files with 6053 additions and 15634 deletions

View File

@@ -13,14 +13,17 @@ add_executable(${_TEST_NAME}_tests
test_flow.cpp
test_gaps.cpp
test_gcode.cpp
test_gcode_travels.cpp
test_gcodefindreplace.cpp
test_gcodewriter.cpp
test_layers.cpp
test_model.cpp
test_multi.cpp
test_perimeters.cpp
test_print.cpp
test_printgcode.cpp
test_printobject.cpp
test_retraction.cpp
test_shells.cpp
test_skirt_brim.cpp
test_support_material.cpp

View File

@@ -250,10 +250,10 @@ SCENARIO("Cooling integration tests", "[Cooling]") {
if (l == 0)
l = line.dist_Z(self);
if (l > 0.) {
if (layer_times.empty())
layer_times.emplace_back(0.);
if (!layer_times.empty()) { // Ignore anything before first z move.
layer_times.back() += 60. * std::abs(l) / line.new_F(self);
}
}
if (line.has('F') && line.f() == external_perimeter_speed)
++ layer_external[scaled<coord_t>(self.z())];
}

View File

@@ -45,13 +45,21 @@ SCENARIO("Custom G-code", "[CustomGCode]")
});
GCodeReader parser;
bool last_move_was_z_change = false;
bool first_z_move = true; // First z move is not a layer change.
int num_layer_changes_not_applied = 0;
parser.parse_buffer(Slic3r::Test::slice({ Test::TestMesh::cube_2x20x10 }, config),
[&last_move_was_z_change, &num_layer_changes_not_applied](Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line)
[&](Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line)
{
if (last_move_was_z_change != line.cmd_is("_MY_CUSTOM_LAYER_GCODE_"))
if (last_move_was_z_change != line.cmd_is("_MY_CUSTOM_LAYER_GCODE_")) {
++ num_layer_changes_not_applied;
last_move_was_z_change = line.dist_Z(self) > 0;
}
if (line.dist_Z(self) > 0 && first_z_move) {
first_z_move = false;
} else if (line.dist_Z(self) > 0){
last_move_was_z_change = true;
} else {
last_move_was_z_change = false;
}
});
THEN("custom layer G-code is applied after Z move and before other moves") {
REQUIRE(num_layer_changes_not_applied == 0);

View File

@@ -236,7 +236,7 @@ static bool verbose_gcode()
return s == "1" || s == "on" || s == "yes";
}
void init_print(std::vector<TriangleMesh> &&meshes, Slic3r::Print &print, Slic3r::Model &model, const DynamicPrintConfig &config_in, bool comments)
void init_print(std::vector<TriangleMesh> &&meshes, Slic3r::Print &print, Slic3r::Model &model, const DynamicPrintConfig &config_in, bool comments, unsigned duplicate_count)
{
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
config.apply(config_in);
@@ -247,10 +247,18 @@ void init_print(std::vector<TriangleMesh> &&meshes, Slic3r::Print &print, Slic3r
for (const TriangleMesh &t : meshes) {
ModelObject *object = model.add_object();
object->name += "object.stl";
object->add_volume(std::move(t));
object->add_volume(t);
object->add_instance();
}
arrange_objects(model, arr2::to_arrange_bed(get_bed_shape(config)), arr2::ArrangeSettings{}.set_distance_from_objects(min_object_distance(config)));
double distance = min_object_distance(config);
arr2::ArrangeSettings arrange_settings{};
arrange_settings.set_distance_from_objects(distance);
arr2::ArrangeBed bed{arr2::to_arrange_bed(get_bed_shape(config))};
if (duplicate_count > 1) {
duplicate(model, duplicate_count, bed, arrange_settings);
}
arrange_objects(model, bed, arrange_settings);
model.center_instances_around_point({100, 100});
for (ModelObject *mo : model.objects) {
mo->ensure_on_bed();
@@ -262,36 +270,36 @@ void init_print(std::vector<TriangleMesh> &&meshes, Slic3r::Print &print, Slic3r
print.set_status_silent();
}
void init_print(std::initializer_list<TestMesh> test_meshes, Slic3r::Print &print, Slic3r::Model &model, const Slic3r::DynamicPrintConfig &config_in, bool comments)
void init_print(std::initializer_list<TestMesh> test_meshes, Slic3r::Print &print, Slic3r::Model &model, const Slic3r::DynamicPrintConfig &config_in, bool comments, unsigned duplicate_count)
{
std::vector<TriangleMesh> triangle_meshes;
triangle_meshes.reserve(test_meshes.size());
for (const TestMesh test_mesh : test_meshes)
triangle_meshes.emplace_back(mesh(test_mesh));
init_print(std::move(triangle_meshes), print, model, config_in, comments);
init_print(std::move(triangle_meshes), print, model, config_in, comments, duplicate_count);
}
void init_print(std::initializer_list<TriangleMesh> input_meshes, Slic3r::Print &print, Slic3r::Model &model, const DynamicPrintConfig &config_in, bool comments)
void init_print(std::initializer_list<TriangleMesh> input_meshes, Slic3r::Print &print, Slic3r::Model &model, const DynamicPrintConfig &config_in, bool comments, unsigned duplicate_count)
{
std::vector<TriangleMesh> triangle_meshes;
triangle_meshes.reserve(input_meshes.size());
for (const TriangleMesh &input_mesh : input_meshes)
triangle_meshes.emplace_back(input_mesh);
init_print(std::move(triangle_meshes), print, model, config_in, comments);
init_print(std::move(triangle_meshes), print, model, config_in, comments, duplicate_count);
}
void init_print(std::initializer_list<TestMesh> meshes, Slic3r::Print &print, Slic3r::Model &model, std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> config_items, bool comments)
void init_print(std::initializer_list<TestMesh> meshes, Slic3r::Print &print, Slic3r::Model &model, std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> config_items, bool comments, unsigned duplicate_count)
{
Slic3r::DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config();
config.set_deserialize_strict(config_items);
init_print(meshes, print, model, config, comments);
init_print(meshes, print, model, config, comments, duplicate_count);
}
void init_print(std::initializer_list<TriangleMesh> meshes, Slic3r::Print &print, Slic3r::Model &model, std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> config_items, bool comments)
void init_print(std::initializer_list<TriangleMesh> meshes, Slic3r::Print &print, Slic3r::Model &model, std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> config_items, bool comments, unsigned duplicate_count)
{
Slic3r::DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config();
config.set_deserialize_strict(config_items);
init_print(meshes, print, model, config, comments);
init_print(meshes, print, model, config, comments, duplicate_count);
}
void init_and_process_print(std::initializer_list<TestMesh> meshes, Slic3r::Print &print, const DynamicPrintConfig &config, bool comments)

View File

@@ -63,11 +63,11 @@ template <typename T>
bool _equiv(const T& a, const T& b, double epsilon) { return abs(a - b) < epsilon; }
Slic3r::Model model(const std::string& model_name, TriangleMesh&& _mesh);
void init_print(std::vector<TriangleMesh> &&meshes, Slic3r::Print &print, Slic3r::Model& model, const DynamicPrintConfig &config_in, bool comments = false);
void init_print(std::initializer_list<TestMesh> meshes, Slic3r::Print &print, Slic3r::Model& model, const Slic3r::DynamicPrintConfig &config_in = Slic3r::DynamicPrintConfig::full_print_config(), bool comments = false);
void init_print(std::initializer_list<TriangleMesh> meshes, Slic3r::Print &print, Slic3r::Model& model, const Slic3r::DynamicPrintConfig &config_in = Slic3r::DynamicPrintConfig::full_print_config(), bool comments = false);
void init_print(std::initializer_list<TestMesh> meshes, Slic3r::Print &print, Slic3r::Model& model, std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> config_items, bool comments = false);
void init_print(std::initializer_list<TriangleMesh> meshes, Slic3r::Print &print, Slic3r::Model& model, std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> config_items, bool comments = false);
void init_print(std::vector<TriangleMesh> &&meshes, Slic3r::Print &print, Slic3r::Model& model, const DynamicPrintConfig &config_in, bool comments = false, unsigned duplicate_count = 1);
void init_print(std::initializer_list<TestMesh> meshes, Slic3r::Print &print, Slic3r::Model& model, const Slic3r::DynamicPrintConfig &config_in = Slic3r::DynamicPrintConfig::full_print_config(), bool comments = false, unsigned duplicate_count = 1);
void init_print(std::initializer_list<TriangleMesh> meshes, Slic3r::Print &print, Slic3r::Model& model, const Slic3r::DynamicPrintConfig &config_in = Slic3r::DynamicPrintConfig::full_print_config(), bool comments = false, unsigned duplicate = 1);
void init_print(std::initializer_list<TestMesh> meshes, Slic3r::Print &print, Slic3r::Model& model, std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> config_items, bool comments = false, unsigned duplicate = 1);
void init_print(std::initializer_list<TriangleMesh> meshes, Slic3r::Print &print, Slic3r::Model& model, std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> config_items, bool comments = false, unsigned duplicate = 1);
void init_and_process_print(std::initializer_list<TestMesh> meshes, Slic3r::Print &print, const DynamicPrintConfig& config, bool comments = false);
void init_and_process_print(std::initializer_list<TriangleMesh> meshes, Slic3r::Print &print, const DynamicPrintConfig& config, bool comments = false);

View File

@@ -6,7 +6,6 @@
#include "libslic3r/libslic3r.h"
#include "libslic3r/ClipperUtils.hpp"
#include "libslic3r/Fill/Fill.hpp"
#include "libslic3r/Flow.hpp"
#include "libslic3r/Layer.hpp"
#include "libslic3r/Geometry.hpp"

View File

@@ -1,11 +1,18 @@
#include <catch2/catch.hpp>
#include <memory>
#include <regex>
#include <fstream>
#include "libslic3r/GCode.hpp"
#include "libslic3r/Geometry/ConvexHull.hpp"
#include "libslic3r/ModelArrange.hpp"
#include "test_data.hpp"
using namespace Slic3r;
using namespace Slic3r::GCode::Impl;
using namespace Test;
constexpr bool debug_files = false;
SCENARIO("Origin manipulation", "[GCode]") {
Slic3r::GCodeGenerator gcodegen;
@@ -22,219 +29,290 @@ 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;
TEST_CASE("Wiping speeds", "[GCode]") {
DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config();
config.set_deserialize_strict({
{ "wipe", "1" },
{ "retract_layer_change", "0" },
});
bool have_wipe = false;
std::vector<double> retract_speeds;
bool extruded_on_this_layer = false;
bool wiping_on_new_layer = false;
GCodeReader parser;
std::string gcode = Slic3r::Test::slice({TestMesh::cube_20x20x20}, config);
parser.parse_buffer(gcode, [&] (Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line) {
if (line.travel() && line.dist_Z(self) != 0) {
extruded_on_this_layer = false;
} else if (line.extruding(self) && line.dist_XY(self) > 0) {
extruded_on_this_layer = true;
} else if (line.retracting(self) && line.dist_XY(self) > 0) {
have_wipe = true;
wiping_on_new_layer = !extruded_on_this_layer;
const double f = line.has_f() ? line.f() : self.f();
double move_time = line.dist_XY(self) / f;
retract_speeds.emplace_back(std::abs(line.dist_E(self)) / move_time);
}
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;
});
CHECK(have_wipe);
double expected_retract_speed = config.option<ConfigOptionFloats>("retract_speed")->get_at(0) * 60;
for (const double retract_speed : retract_speeds) {
INFO("Wipe moves don\'t retract faster than configured speed");
CHECK(retract_speed < expected_retract_speed);
}
INFO("No wiping after layer change")
CHECK(!wiping_on_new_layer);
}
return true;
bool has_moves_below_z_offset(const DynamicPrintConfig& config) {
GCodeReader parser;
std::string gcode = Slic3r::Test::slice({TestMesh::cube_20x20x20}, config);
unsigned moves_below_z_offset{};
double configured_offset = config.opt_float("z_offset");
parser.parse_buffer(gcode, [&] (Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line) {
if (line.travel() && line.has_z() && line.z() < configured_offset) {
moves_below_z_offset++;
}
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})
});
}
TEST_CASE("Get first crossed line distance", "[GCode]") {
// A 2x2 square at 0, 0, with 1x1 square hole in its center.
ExPolygon square_with_hole{
{
scaled(Vec2f{-1, -1}),
scaled(Vec2f{1, -1}),
scaled(Vec2f{1, 1}),
scaled(Vec2f{-1, 1})
},
{
scaled(Vec2f{-0.5, -0.5}),
scaled(Vec2f{0.5, -0.5}),
scaled(Vec2f{0.5, 0.5}),
scaled(Vec2f{-0.5, 0.5})
return moves_below_z_offset > 0;
}
};
// A 2x2 square above the previous square at (0, 3).
ExPolygon square_above{
{
scaled(Vec2f{-1, 2}),
scaled(Vec2f{1, 2}),
scaled(Vec2f{1, 4}),
scaled(Vec2f{-1, 4})
}
};
// Bottom-up travel intersecting the squares.
Lines travel{Polyline{
scaled(Vec2f{0, -2}),
scaled(Vec2f{0, -0.7}),
scaled(Vec2f{0, 0}),
scaled(Vec2f{0, 1}),
scaled(Vec2f{0, 1.3}),
scaled(Vec2f{0, 2.4}),
scaled(Vec2f{0, 4.5}),
scaled(Vec2f{0, 5}),
}.lines()};
// Try different cases by skipping lines in the travel.
AABBTreeLines::LinesDistancer<Linef> distancer = get_expolygons_distancer({square_with_hole, square_above});
CHECK(*get_first_crossed_line_distance(travel, distancer) == Approx(1));
CHECK(*get_first_crossed_line_distance(tcb::span{travel}.subspan(1), distancer) == Approx(0.2));
CHECK(*get_first_crossed_line_distance(tcb::span{travel}.subspan(2), distancer) == Approx(0.5));
CHECK(*get_first_crossed_line_distance(tcb::span{travel}.subspan(3), distancer) == Approx(1.0)); //Edge case
CHECK(*get_first_crossed_line_distance(tcb::span{travel}.subspan(4), distancer) == Approx(0.7));
CHECK(*get_first_crossed_line_distance(tcb::span{travel}.subspan(5), distancer) == Approx(1.6));
CHECK_FALSE(get_first_crossed_line_distance(tcb::span{travel}.subspan(6), distancer));
}
TEST_CASE("Generate regular polygon", "[GCode]") {
const unsigned points_count{32};
const Point centroid{scaled(Vec2d{5, -2})};
const Polygon result{generate_regular_polygon(centroid, scaled(Vec2d{0, 0}), points_count)};
const Point oposite_point{centroid * 2};
REQUIRE(result.size() == 32);
CHECK(result[16].x() == Approx(oposite_point.x()));
CHECK(result[16].y() == Approx(oposite_point.y()));
std::vector<double> angles;
angles.reserve(points_count);
for (unsigned index = 0; index < points_count; index++) {
const unsigned previous_index{index == 0 ? points_count - 1 : index - 1};
const unsigned next_index{index == points_count - 1 ? 0 : index + 1};
const Point previous_point = result.points[previous_index];
const Point current_point = result.points[index];
const Point next_point = result.points[next_index];
angles.emplace_back(angle(Vec2crd{previous_point - current_point}, Vec2crd{next_point - current_point}));
}
std::vector<double> expected;
angles.reserve(points_count);
std::generate_n(std::back_inserter(expected), points_count, [&](){
return angles.front();
TEST_CASE("Z moves with offset", "[GCode]") {
DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config();
config.set_deserialize_strict({
{ "z_offset", 5 },
{ "start_gcode", "" },
});
CHECK_THAT(angles, Catch::Matchers::Approx(expected));
INFO("No lift");
CHECK(!has_moves_below_z_offset(config));
config.set_deserialize_strict({{ "retract_lift", "3" }});
INFO("Lift < z offset");
CHECK(!has_moves_below_z_offset(config));
config.set_deserialize_strict({{ "retract_lift", "6" }});
INFO("Lift > z offset");
CHECK(!has_moves_below_z_offset(config));
}
TEST_CASE("Square bed with padding", "[GCode]") {
const Bed bed{
{
Vec2d{0, 0},
Vec2d{100, 0},
Vec2d{100, 100},
Vec2d{0, 100}
},
10.0
};
std::optional<double> parse_axis(const std::string& line, const std::string& axis) {
std::smatch matches;
if (std::regex_search(line, matches, std::regex{axis + "(\\d+)"})) {
std::string matchedValue = matches[1].str();
return std::stod(matchedValue);
}
return std::nullopt;
}
CHECK(bed.centroid.x() == 50);
CHECK(bed.centroid.y() == 50);
CHECK(bed.contains_within_padding(Vec2d{10, 10}));
CHECK_FALSE(bed.contains_within_padding(Vec2d{9, 10}));
/**
* This tests the following behavior:
* - complete objects does not crash
* - no hard-coded "E" are generated
* - Z moves are correctly generated for both objects
* - no travel moves go outside skirt
* - temperatures are set correctly
*/
TEST_CASE("Extrusion, travels, temeperatures", "[GCode]") {
DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config();
config.set_deserialize_strict({
{ "gcode_comments", 1 },
{ "complete_objects", 1 },
{ "extrusion_axis", 'A' },
{ "start_gcode", "" }, // prevent any default extra Z move
{ "layer_height", 0.4 },
{ "first_layer_height", 0.4 },
{ "temperature", "200" },
{ "first_layer_temperature", "210" },
{ "retract_length", "0" }
});
}
std::vector<double> z_moves;
Points travel_moves;
Points extrusions;
std::vector<double> temps;
GCodeReader parser;
Print print;
Model model;
Test::init_print({TestMesh::cube_20x20x20}, print, model, config, false, 2);
std::string gcode = Test::gcode(print);
parser.parse_buffer(gcode, [&] (Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line) {
INFO("Unexpected E argument");
CHECK(!line.has_e());
if (line.has_z()) {
z_moves.emplace_back(line.z());
}
if (line.has_x() || line.has_y()) {
if (line.extruding(self) || line.has_unknown_axis()) {
extrusions.emplace_back(scaled(line.x()), scaled(line.y()));
} else if (!extrusions.empty()){ // skip initial travel move to first skirt point
travel_moves.emplace_back(scaled(line.x()), scaled(line.y()));
}
} else if (line.cmd_is("M104") || line.cmd_is("M109")) {
const std::optional<double> parsed_temperature = parse_axis(line.raw(), "S");
if (!parsed_temperature) {
FAIL("Failed to parse temperature!");
}
if (temps.empty() || temps.back() != parsed_temperature) {
temps.emplace_back(*parsed_temperature);
}
}
});
const unsigned layer_count = 20 / 0.4;
INFO("Complete_objects generates the correct number of Z moves.");
CHECK(z_moves.size() == layer_count * 2);
auto first_moves = tcb::span{z_moves}.subspan(0, layer_count);
auto second_moves = tcb::span{z_moves}.subspan(layer_count);
CHECK( std::vector(first_moves.begin(), first_moves.end()) == std::vector(second_moves.begin(), second_moves.end()));
const Polygon convex_hull{Geometry::convex_hull(extrusions)};
INFO("All travel moves happen within skirt.");
for (const Point& travel_move : travel_moves) {
CHECK(convex_hull.contains(travel_move));
}
INFO("Expected temperature changes");
CHECK(temps == std::vector<double>{210, 200, 210, 200, 0});
}
TEST_CASE("Used filament", "[GCode]") {
DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config();
config.set_deserialize_strict({
{ "retract_length", "1000000" },
{ "use_relative_e_distances", 1 },
{ "layer_gcode", "G92 E0\n" },
});
GCodeReader parser;
Print print;
Model model;
Test::init_print({TestMesh::cube_20x20x20}, print, model, config);
Test::gcode(print);
INFO("Final retraction is not considered in total used filament");
CHECK(print.print_statistics().total_used_filament > 0);
}
void check_m73s(Print& print){
std::vector<double> percent{};
bool got_100 = false;
bool extruding_after_100 = 0;
GCodeReader parser;
std::string gcode = Slic3r::Test::gcode(print);
parser.parse_buffer(gcode, [&] (Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line) {
if (line.cmd_is("M73")) {
std::optional<double> p = parse_axis(line.raw(), "P");
if (!p) {
FAIL("Failed to parse percent");
}
percent.emplace_back(*p);
got_100 = p == Approx(100);
}
if (line.extruding(self) && got_100) {
extruding_after_100 = true;
}
});
INFO("M73 is never given more than 100%");
for (const double value : percent) {
CHECK(value <= 100);
}
INFO("No extrusions after M73 P100.");
CHECK(!extruding_after_100);
}
TEST_CASE("M73s have correct percent values", "[GCode]") {
DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config();
SECTION("Single object") {
config.set_deserialize_strict({
{" gcode_flavor", "sailfish" },
{" raft_layers", 3 },
});
Print print;
Model model;
Test::init_print({TestMesh::cube_20x20x20}, print, model, config);
check_m73s(print);
}
SECTION("Two copies of single object") {
config.set_deserialize_strict({
{" gcode_flavor", "sailfish" },
});
Print print;
Model model;
Test::init_print({TestMesh::cube_20x20x20}, print, model, config, false, 2);
check_m73s(print);
if constexpr (debug_files) {
std::ofstream gcode_file{"M73_2_copies.gcode"};
gcode_file << Test::gcode(print);
}
}
SECTION("Two objects") {
config.set_deserialize_strict({
{" gcode_flavor", "sailfish" },
});
Print print;
Model model;
Test::init_print({TestMesh::cube_20x20x20, TestMesh::cube_20x20x20}, print, model, config);
check_m73s(print);
}
SECTION("One layer object") {
config.set_deserialize_strict({
{" gcode_flavor", "sailfish" },
});
Print print;
Model model;
TriangleMesh test_mesh{mesh(TestMesh::cube_20x20x20)};
const auto layer_height = static_cast<float>(config.opt_float("layer_height"));
test_mesh.scale(Vec3f{1.0F, 1.0F, layer_height/20.0F});
Test::init_print({test_mesh}, print, model, config);
check_m73s(print);
if constexpr (debug_files) {
std::ofstream gcode_file{"M73_one_layer.gcode"};
gcode_file << Test::gcode(print);
}
}
}
TEST_CASE("M201 for acceleation reset", "[GCode]") {
DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config();
config.set_deserialize_strict({
{ "gcode_flavor", "repetier" },
{ "default_acceleration", 1337 },
});
GCodeReader parser;
std::string gcode = Slic3r::Test::slice({TestMesh::cube_with_hole}, config);
bool has_accel = false;
bool has_m204 = false;
parser.parse_buffer(gcode, [&] (Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line) {
if (line.cmd_is("M201") && line.has_x() && line.has_y()) {
if (line.x() == 1337 && line.y() == 1337) {
has_accel = true;
}
}
if (line.cmd_is("M204") && line.raw().find('S') != std::string::npos) {
has_m204 = true;
}
});
INFO("M201 is generated for repetier firmware.");
CHECK(has_accel);
INFO("M204 is not generated for repetier firmware");
CHECK(!has_m204);
}

View File

@@ -0,0 +1,201 @@
#include <catch2/catch.hpp>
#include <libslic3r/GCode/Travels.hpp>
#include <libslic3r/ExPolygon.hpp>
#include <libslic3r/GCode.hpp>
#include <boost/math/special_functions/pow.hpp>
using namespace Slic3r;
using namespace Slic3r::GCode::Impl::Travels;
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()) > int(this->tolerance)
|| std::abs(point.y() - expected_point.y()) > int(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.f, 0.f, 3.f}),
scaled(Vec3f{0.2f, 0.f, 3.2f}),
scaled(Vec3f{0.5f, 0.f, 3.5f}),
scaled(Vec3f{ 1.f, 0.f, 4.f})
});
}
TEST_CASE("Get first crossed line distance", "[GCode]") {
// A 2x2 square at 0, 0, with 1x1 square hole in its center.
ExPolygon square_with_hole{
{
scaled(Vec2f{-1, -1}),
scaled(Vec2f{1, -1}),
scaled(Vec2f{1, 1}),
scaled(Vec2f{-1, 1})
},
{
scaled(Vec2f{-0.5, -0.5}),
scaled(Vec2f{0.5, -0.5}),
scaled(Vec2f{0.5, 0.5}),
scaled(Vec2f{-0.5, 0.5})
}
};
// A 2x2 square above the previous square at (0, 3).
ExPolygon square_above{
{
scaled(Vec2f{-1, 2}),
scaled(Vec2f{1, 2}),
scaled(Vec2f{1, 4}),
scaled(Vec2f{-1, 4})
}
};
// Bottom-up travel intersecting the squares.
Lines travel{Polyline{
scaled(Vec2f{0, -2}),
scaled(Vec2f{0, -0.7}),
scaled(Vec2f{0, 0}),
scaled(Vec2f{0, 1}),
scaled(Vec2f{0, 1.3}),
scaled(Vec2f{0, 2.4}),
scaled(Vec2f{0, 4.5}),
scaled(Vec2f{0, 5}),
}.lines()};
std::vector<GCode::ObjectOrExtrusionLinef> lines;
for (const ExPolygon& polygon : {square_with_hole, square_above}) {
for (const Line& line : polygon.lines()) {
lines.emplace_back(unscale(line.a), unscale(line.b));
}
}
// Try different cases by skipping lines in the travel.
AABBTreeLines::LinesDistancer<GCode::ObjectOrExtrusionLinef> distancer{std::move(lines)};
CHECK(get_first_crossed_line_distance(travel, distancer) == Approx(1));
CHECK(get_first_crossed_line_distance(tcb::span{travel}.subspan(1), distancer) == Approx(0.2));
CHECK(get_first_crossed_line_distance(tcb::span{travel}.subspan(2), distancer) == Approx(0.5));
CHECK(get_first_crossed_line_distance(tcb::span{travel}.subspan(3), distancer) == Approx(1.0)); //Edge case
CHECK(get_first_crossed_line_distance(tcb::span{travel}.subspan(4), distancer) == Approx(0.7));
CHECK(get_first_crossed_line_distance(tcb::span{travel}.subspan(5), distancer) == Approx(1.6));
CHECK(get_first_crossed_line_distance(tcb::span{travel}.subspan(6), distancer) == std::numeric_limits<double>::max());
}
TEST_CASE("Elevated travel formula", "[GCode]") {
const double lift_height{10};
const double slope_end{10};
const double blend_width{10};
const ElevatedTravelParams params{lift_height, slope_end, blend_width};
ElevatedTravelFormula f{params};
const double distance = slope_end - blend_width / 2;
const double slope = (f(distance) - f(0)) / distance;
// At the begining it has given slope.
CHECK(slope == lift_height / slope_end);
// At the end it is flat.
CHECK(f(slope_end + blend_width / 2) == f(slope_end + blend_width));
// Should be smoothed.
CHECK(f(slope_end) < lift_height);
}

View File

@@ -0,0 +1,104 @@
/**
* Ported from t/layers.t
*/
#include <catch2/catch.hpp>
#include "test_data.hpp"
using namespace Slic3r;
using namespace Slic3r::Test;
void check_layers(const DynamicPrintConfig& config) {
GCodeReader parser;
std::string gcode = Slic3r::Test::slice({TestMesh::cube_20x20x20}, config);
std::vector<double> z;
std::vector<double> increments;
parser.parse_buffer(gcode, [&] (Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line) {
if (line.has_z()) {
z.emplace_back(line.z());
increments.emplace_back(line.dist_Z(self));
}
});
const double first_layer_height = config.opt_float("first_layer_height");
const double z_offset = config.opt_float("z_offset");
const double layer_height = config.opt_float("layer_height");
INFO("Correct first layer height.");
CHECK(z.at(0) == Approx(first_layer_height + z_offset));
INFO("Correct second layer height")
CHECK(z.at(1) == Approx(first_layer_height + layer_height + z_offset));
INFO("Correct layer height")
for (const double increment : tcb::span{increments}.subspan(1)) {
CHECK(increment == Approx(layer_height));
}
}
TEST_CASE("Layer heights are correct", "[Layers]") {
DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config();
config.set_deserialize_strict({
{ "start_gcode", "" },
{ "layer_height", 0.3 },
{ "first_layer_height", 0.2 },
{ "retract_length", "0" }
});
SECTION("Absolute first layer height") {
check_layers(config);
}
SECTION("Relative layer height") {
const double layer_height = config.opt_float("layer_height");
config.set_deserialize_strict({
{ "first_layer_height", 0.6 * layer_height },
});
check_layers(config);
}
SECTION("Positive z offset") {
config.set_deserialize_strict({
{ "z_offset", 0.9 },
});
check_layers(config);
}
SECTION("Negative z offset") {
config.set_deserialize_strict({
{ "z_offset", -0.8 },
});
check_layers(config);
}
}
TEST_CASE("GCode has reasonable height", "[Layers]") {
DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config();
config.set_deserialize_strict({
{ "fill_density", 0 },
{ "gcode_binary", 0 },
});
Print print;
Model model;
TriangleMesh test_mesh{mesh(TestMesh::cube_20x20x20)};
test_mesh.scale(2);
Test::init_print({test_mesh}, print, model, config);
const std::string gcode{Test::gcode(print)};
std::vector<double> z;
GCodeReader parser;
parser.parse_buffer(gcode, [&] (Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line) {
if (line.dist_Z(self) != Approx(0)) {
z.emplace_back(line.z());
}
});
REQUIRE(!z.empty());
INFO("Last Z is: " + std::to_string(z.back()));
CHECK((z.back() > 20*1.8 && z.back() < 20*2.2));
}

View File

@@ -0,0 +1,359 @@
/**
* Ported from t/retraction.t
*/
#include <catch2/catch.hpp>
#include <libslic3r/GCodeReader.hpp>
#include <libslic3r/Config.hpp>
#include "test_data.hpp"
#include <regex>
#include <fstream>
using namespace Slic3r;
using namespace Test;
constexpr bool debug_files {false};
void check_gcode(std::initializer_list<TestMesh> meshes, const DynamicPrintConfig& config, const unsigned duplicate) {
constexpr std::size_t tools_count = 4;
std::size_t tool = 0;
std::array<unsigned, tools_count> toolchange_count{0}; // Track first usages so that we don't expect retract_length_toolchange when extruders are used for the first time
std::array<bool, tools_count> retracted{false};
std::array<double, tools_count> retracted_length{0};
bool lifted = false;
double lift_dist = 0; // Track lifted distance for toolchanges and extruders with different retract_lift values
bool changed_tool = false;
bool wait_for_toolchange = false;
Print print;
Model model;
Test::init_print({TestMesh::cube_20x20x20}, print, model, config, false, duplicate);
std::string gcode = Test::gcode(print);
if constexpr(debug_files) {
static int count{0};
std::ofstream file{"check_gcode_" + std::to_string(count++) + ".gcode"};
file << gcode;
}
GCodeReader parser;
parser.parse_buffer(gcode, [&] (Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line) {
std::regex regex{"^T(\\d+)"};
std::smatch matches;
std::string cmd{line.cmd()};
if (std::regex_match(cmd, matches, regex)) {
tool = std::stoul(matches[1].str());
changed_tool = true;
wait_for_toolchange = false;
toolchange_count[tool]++;
} else if (std::regex_match(cmd, std::regex{"^G[01]$"}) && !line.has(Z)) { // ignore lift taking place after retraction
INFO("Toolchange must not happen right after retraction.");
CHECK(!wait_for_toolchange);
}
const double retract_length = config.option<ConfigOptionFloats>("retract_length")->get_at(tool);
const double retract_before_travel = config.option<ConfigOptionFloats>("retract_before_travel")->get_at(tool);
const double retract_length_toolchange = config.option<ConfigOptionFloats>("retract_length_toolchange")->get_at(tool);
const double retract_restart_extra = config.option<ConfigOptionFloats>("retract_restart_extra")->get_at(tool);
const double retract_restart_extra_toolchange = config.option<ConfigOptionFloats>("retract_restart_extra_toolchange")->get_at(tool);
if (line.dist_Z(self) != 0) {
// lift move or lift + change layer
const double retract_lift = config.option<ConfigOptionFloats>("retract_lift")->get_at(tool);
if (
line.dist_Z(self) == Approx(retract_lift)
|| (
line.dist_Z(self) == Approx(config.opt_float("layer_height") + retract_lift)
&& retract_lift > 0
)
) {
INFO("Only lift while retracted");
CHECK(retracted[tool]);
INFO("No double lift");
CHECK(!lifted);
lifted = true;
lift_dist = line.dist_Z(self);
}
if (line.dist_Z(self) < 0) {
INFO("Must be lifted before going down.")
CHECK(lifted);
INFO("Going down by the same amount of the lift or by the amount needed to get to next layer");
CHECK((
line.dist_Z(self) == Approx(-lift_dist)
|| line.dist_Z(self) == Approx(-lift_dist + config.opt_float("layer_height"))
));
lift_dist = 0;
lifted = false;
}
const double feedrate = line.has_f() ? line.f() : self.f();
INFO("move Z at travel speed");
CHECK(feedrate == Approx(config.opt_float("travel_speed") * 60));
}
if (line.retracting(self)) {
retracted[tool] = true;
retracted_length[tool] += -line.dist_E(self);
if (retracted_length[tool] == Approx(retract_length)) {
// okay
} else if (retracted_length[tool] == Approx(retract_length_toolchange)) {
wait_for_toolchange = true;
} else {
INFO("Not retracted by the correct amount.");
CHECK(false);
}
}
if (line.extruding(self)) {
INFO("Only extruding while not lifted");
CHECK(!lifted);
if (retracted[tool]) {
double expected_amount = retracted_length[tool] + retract_restart_extra;
if (changed_tool && toolchange_count[tool] > 1) {
expected_amount = retract_length_toolchange + retract_restart_extra_toolchange;
changed_tool = false;
}
INFO("Unretracted by the correct amount");
REQUIRE(line.dist_E(self) == Approx(expected_amount));
retracted[tool] = false;
retracted_length[tool] = 0;
}
}
if (line.travel() && line.dist_XY(self) >= retract_before_travel) {
INFO("Retracted before long travel move");
CHECK(retracted[tool]);
}
});
}
void test_slicing(std::initializer_list<TestMesh> meshes, DynamicPrintConfig& config, const unsigned duplicate = 1) {
SECTION("Retraction") {
check_gcode(meshes, config, duplicate);
}
SECTION("Restart extra length") {
config.set_deserialize_strict({{ "retract_restart_extra", "1" }});
check_gcode(meshes, config, duplicate);
}
SECTION("Negative restart extra length") {
config.set_deserialize_strict({{ "retract_restart_extra", "-1" }});
check_gcode(meshes, config, duplicate);
}
SECTION("Retract_lift") {
config.set_deserialize_strict({{ "retract_lift", "1,2" }});
check_gcode(meshes, config, duplicate);
}
}
TEST_CASE("Slicing with retraction and lifing", "[retraction]") {
DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config();
config.set_deserialize_strict({
{ "nozzle_diameter", "0.6,0.6,0.6,0.6" },
{ "first_layer_height", config.opt_float("layer_height") },
{ "first_layer_speed", "100%" },
{ "start_gcode", "" }, // To avoid dealing with the nozzle lift in start G-code
{ "retract_length", "1.5" },
{ "retract_before_travel", "3" },
{ "retract_layer_change", "1" },
{ "only_retract_when_crossing_perimeters", 0 },
});
SECTION("Standard run") {
test_slicing({TestMesh::cube_20x20x20}, config);
}
SECTION("With duplicate cube") {
test_slicing({TestMesh::cube_20x20x20}, config, 2);
}
SECTION("Dual extruder with multiple skirt layers") {
config.set_deserialize_strict({
{"infill_extruder", 2},
{"skirts", 4},
{"skirt_height", 3},
});
test_slicing({TestMesh::cube_20x20x20}, config);
}
}
TEST_CASE("Z moves", "[retraction]") {
DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config();
config.set_deserialize_strict({
{ "start_gcode", "" }, // To avoid dealing with the nozzle lift in start G-code
{ "retract_length", "0" },
{ "retract_layer_change", "0" },
{ "retract_lift", "0.2" }
});
bool retracted = false;
unsigned layer_changes_with_retraction = 0;
unsigned retractions = 0;
unsigned z_restores = 0;
std::string gcode = Slic3r::Test::slice({TestMesh::cube_20x20x20}, config);
if constexpr(debug_files) {
std::ofstream file{"zmoves.gcode"};
file << gcode;
}
GCodeReader parser;
parser.parse_buffer(gcode, [&] (Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line) {
if (line.retracting(self)) {
retracted = true;
retractions++;
} else if (line.extruding(self) && retracted) {
retracted = 0;
}
if (line.dist_Z(self) != 0 && retracted) {
layer_changes_with_retraction++;
}
if (line.dist_Z(self) < 0) {
z_restores++;
}
});
INFO("no retraction on layer change");
CHECK(layer_changes_with_retraction == 0);
INFO("no retractions");
CHECK(retractions == 0);
INFO("no lift");
CHECK(z_restores == 0);
}
TEST_CASE("Firmware retraction handling", "[retraction]") {
DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config();
config.set_deserialize_strict({
{ "use_firmware_retraction", 1 },
});
bool retracted = false;
unsigned double_retractions = 0;
unsigned double_unretractions = 0;
std::string gcode = Slic3r::Test::slice({TestMesh::cube_20x20x20}, config);
GCodeReader parser;
parser.parse_buffer(gcode, [&] (Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line) {
if (line.cmd_is("G10")) {
if (retracted)
double_retractions++;
retracted = true;
} else if (line.cmd_is("G11")) {
if (!retracted)
double_unretractions++;
retracted = 0;
}
});
INFO("No double retractions");
CHECK(double_retractions == 0);
INFO("No double unretractions");
CHECK(double_unretractions == 0);
}
TEST_CASE("Firmware retraction when length is 0", "[retraction]") {
DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config();
config.set_deserialize_strict({
{ "use_firmware_retraction", 1 },
{ "retract_length", "0" },
});
bool retracted = false;
std::string gcode = Slic3r::Test::slice({TestMesh::cube_20x20x20}, config);
GCodeReader parser;
parser.parse_buffer(gcode, [&] (Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line) {
if (line.cmd_is("G10")) {
retracted = true;
}
});
INFO("Retracting also when --retract-length is 0 but --use-firmware-retraction is enabled");
CHECK(retracted);
}
std::vector<double> get_lift_layers(const DynamicPrintConfig& config) {
Print print;
Model model;
Test::init_print({TestMesh::cube_20x20x20}, print, model, config, false, 2);
std::string gcode = Test::gcode(print);
std::vector<double> result;
GCodeReader parser;
parser.parse_buffer(gcode, [&] (Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line) {
if (line.cmd_is("G1") && line.dist_Z(self) < 0) {
result.push_back(line.new_Z(self));
}
});
return result;
}
bool values_are_in_range(const std::vector<double>& values, double from, double to) {
for (const double& value : values) {
if (value < from || value > to) {
return false;
}
}
return true;
}
TEST_CASE("Lift above/bellow layers", "[retraction]") {
DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config();
config.set_deserialize_strict({
{ "nozzle_diameter", "0.6,0.6,0.6,0.6" },
{ "start_gcode", "" },
{ "retract_lift", "3,4" },
});
config.set_deserialize_strict({
{ "retract_lift_above", "0, 0" },
{ "retract_lift_below", "0, 0" },
});
std::vector<double> lift_layers = get_lift_layers(config);
INFO("lift takes place when above/below == 0");
CHECK(!lift_layers.empty());
config.set_deserialize_strict({
{ "retract_lift_above", "5, 6" },
{ "retract_lift_below", "15, 13" },
});
lift_layers = get_lift_layers(config);
INFO("lift takes place when above/below != 0");
CHECK(!lift_layers.empty());
double retract_lift_above = config.option<ConfigOptionFloats>("retract_lift_above")->get_at(0);
double retract_lift_below = config.option<ConfigOptionFloats>("retract_lift_below")->get_at(0);
INFO("Z is not lifted above/below the configured value");
CHECK(values_are_in_range(lift_layers, retract_lift_above, retract_lift_below));
// check lifting with different values for 2. extruder
config.set_deserialize_strict({
{"perimeter_extruder", 2},
{"infill_extruder", 2},
{"retract_lift_above", "0, 0"},
{"retract_lift_below", "0, 0"}
});
lift_layers = get_lift_layers(config);
INFO("lift takes place when above/below == 0 for 2. extruder");
CHECK(!lift_layers.empty());
config.set_deserialize_strict({
{ "retract_lift_above", "5, 6" },
{ "retract_lift_below", "15, 13" },
});
lift_layers = get_lift_layers(config);
INFO("lift takes place when above/below != 0 for 2. extruder");
CHECK(!lift_layers.empty());
retract_lift_above = config.option<ConfigOptionFloats>("retract_lift_above")->get_at(1);
retract_lift_below = config.option<ConfigOptionFloats>("retract_lift_below")->get_at(1);
INFO("Z is not lifted above/below the configured value for 2. extruder");
CHECK(values_are_in_range(lift_layers, retract_lift_above, retract_lift_below));
}