test_gcode.cpp 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  1. /**
  2. * Mostly ported from t/gcode.t
  3. */
  4. #include <catch2/catch.hpp>
  5. #include <memory>
  6. #include <regex>
  7. #include <fstream>
  8. #include "libslic3r/GCode.hpp"
  9. #include "libslic3r/Geometry/ConvexHull.hpp"
  10. #include "libslic3r/ModelArrange.hpp"
  11. #include "test_data.hpp"
  12. using namespace Slic3r;
  13. using namespace Test;
  14. constexpr bool debug_files = false;
  15. SCENARIO("Origin manipulation", "[GCode]") {
  16. Slic3r::GCodeGenerator gcodegen;
  17. WHEN("set_origin to (10,0)") {
  18. gcodegen.set_origin(Vec2d(10,0));
  19. REQUIRE(gcodegen.origin() == Vec2d(10, 0));
  20. }
  21. WHEN("set_origin to (10,0) and translate by (5, 5)") {
  22. gcodegen.set_origin(Vec2d(10,0));
  23. gcodegen.set_origin(gcodegen.origin() + Vec2d(5, 5));
  24. THEN("origin returns reference to point") {
  25. REQUIRE(gcodegen.origin() == Vec2d(15,5));
  26. }
  27. }
  28. }
  29. TEST_CASE("Wiping speeds", "[GCode]") {
  30. DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config();
  31. config.set_deserialize_strict({
  32. { "wipe", "1" },
  33. { "retract_layer_change", "0" },
  34. });
  35. bool have_wipe = false;
  36. std::vector<double> retract_speeds;
  37. bool extruded_on_this_layer = false;
  38. bool wiping_on_new_layer = false;
  39. GCodeReader parser;
  40. std::string gcode = Slic3r::Test::slice({TestMesh::cube_20x20x20}, config);
  41. parser.parse_buffer(gcode, [&] (Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line) {
  42. if (line.travel() && line.dist_Z(self) != 0) {
  43. extruded_on_this_layer = false;
  44. } else if (line.extruding(self) && line.dist_XY(self) > 0) {
  45. extruded_on_this_layer = true;
  46. } else if (line.retracting(self) && line.dist_XY(self) > 0) {
  47. have_wipe = true;
  48. wiping_on_new_layer = !extruded_on_this_layer;
  49. const double f = line.has_f() ? line.f() : self.f();
  50. double move_time = line.dist_XY(self) / f;
  51. retract_speeds.emplace_back(std::abs(line.dist_E(self)) / move_time);
  52. }
  53. });
  54. CHECK(have_wipe);
  55. double expected_retract_speed = config.option<ConfigOptionFloats>("retract_speed")->get_at(0) * 60;
  56. for (const double retract_speed : retract_speeds) {
  57. INFO("Wipe moves don\'t retract faster than configured speed");
  58. CHECK(retract_speed < expected_retract_speed);
  59. }
  60. INFO("No wiping after layer change")
  61. CHECK(!wiping_on_new_layer);
  62. }
  63. bool has_moves_below_z_offset(const DynamicPrintConfig& config) {
  64. GCodeReader parser;
  65. std::string gcode = Slic3r::Test::slice({TestMesh::cube_20x20x20}, config);
  66. unsigned moves_below_z_offset{};
  67. double configured_offset = config.opt_float("z_offset");
  68. parser.parse_buffer(gcode, [&] (Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line) {
  69. if (line.travel() && line.has_z() && line.z() < configured_offset) {
  70. moves_below_z_offset++;
  71. }
  72. });
  73. return moves_below_z_offset > 0;
  74. }
  75. TEST_CASE("Z moves with offset", "[GCode]") {
  76. DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config();
  77. config.set_deserialize_strict({
  78. { "z_offset", 5 },
  79. { "start_gcode", "" },
  80. });
  81. INFO("No lift");
  82. CHECK(!has_moves_below_z_offset(config));
  83. config.set_deserialize_strict({{ "retract_lift", "3" }});
  84. INFO("Lift < z offset");
  85. CHECK(!has_moves_below_z_offset(config));
  86. config.set_deserialize_strict({{ "retract_lift", "6" }});
  87. INFO("Lift > z offset");
  88. CHECK(!has_moves_below_z_offset(config));
  89. }
  90. std::optional<double> parse_axis(const std::string& line, const std::string& axis) {
  91. std::smatch matches;
  92. if (std::regex_search(line, matches, std::regex{axis + "(\\d+)"})) {
  93. std::string matchedValue = matches[1].str();
  94. return std::stod(matchedValue);
  95. }
  96. return std::nullopt;
  97. }
  98. /**
  99. * This tests the following behavior:
  100. * - complete objects does not crash
  101. * - no hard-coded "E" are generated
  102. * - Z moves are correctly generated for both objects
  103. * - no travel moves go outside skirt
  104. * - temperatures are set correctly
  105. */
  106. TEST_CASE("Extrusion, travels, temeperatures", "[GCode]") {
  107. DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config();
  108. config.set_deserialize_strict({
  109. { "gcode_comments", 1 },
  110. { "complete_objects", 1 },
  111. { "extrusion_axis", 'A' },
  112. { "start_gcode", "" }, // prevent any default extra Z move
  113. { "layer_height", 0.4 },
  114. { "first_layer_height", 0.4 },
  115. { "temperature", "200" },
  116. { "first_layer_temperature", "210" },
  117. { "retract_length", "0" }
  118. });
  119. std::vector<double> z_moves;
  120. Points travel_moves;
  121. Points extrusions;
  122. std::vector<double> temps;
  123. GCodeReader parser;
  124. Print print;
  125. Model model;
  126. Test::init_print({TestMesh::cube_20x20x20}, print, model, config, false, 2);
  127. std::string gcode = Test::gcode(print);
  128. parser.parse_buffer(gcode, [&] (Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line) {
  129. INFO("Unexpected E argument");
  130. CHECK(!line.has_e());
  131. if (line.has_z()) {
  132. z_moves.emplace_back(line.z());
  133. }
  134. if (line.has_x() || line.has_y()) {
  135. if (line.extruding(self) || line.has_unknown_axis()) {
  136. extrusions.emplace_back(scaled(line.x()), scaled(line.y()));
  137. } else if (!extrusions.empty()){ // skip initial travel move to first skirt point
  138. travel_moves.emplace_back(scaled(line.x()), scaled(line.y()));
  139. }
  140. } else if (line.cmd_is("M104") || line.cmd_is("M109")) {
  141. const std::optional<double> parsed_temperature = parse_axis(line.raw(), "S");
  142. if (!parsed_temperature) {
  143. FAIL("Failed to parse temperature!");
  144. }
  145. if (temps.empty() || temps.back() != parsed_temperature) {
  146. temps.emplace_back(*parsed_temperature);
  147. }
  148. }
  149. });
  150. const unsigned layer_count = 20 / 0.4;
  151. INFO("Complete_objects generates the correct number of Z moves.");
  152. CHECK(z_moves.size() == layer_count * 2);
  153. auto first_moves = tcb::span{z_moves}.subspan(0, layer_count);
  154. auto second_moves = tcb::span{z_moves}.subspan(layer_count);
  155. CHECK( std::vector(first_moves.begin(), first_moves.end()) == std::vector(second_moves.begin(), second_moves.end()));
  156. const Polygon convex_hull{Geometry::convex_hull(extrusions)};
  157. INFO("All travel moves happen within skirt.");
  158. for (const Point& travel_move : travel_moves) {
  159. CHECK(convex_hull.contains(travel_move));
  160. }
  161. INFO("Expected temperature changes");
  162. CHECK(temps == std::vector<double>{210, 200, 210, 200, 0});
  163. }
  164. TEST_CASE("Used filament", "[GCode]") {
  165. DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config();
  166. config.set_deserialize_strict({
  167. { "retract_length", "1000000" },
  168. { "use_relative_e_distances", 1 },
  169. { "layer_gcode", "G92 E0\n" },
  170. });
  171. GCodeReader parser;
  172. Print print;
  173. Model model;
  174. Test::init_print({TestMesh::cube_20x20x20}, print, model, config);
  175. Test::gcode(print);
  176. INFO("Final retraction is not considered in total used filament");
  177. CHECK(print.print_statistics().total_used_filament > 0);
  178. }
  179. void check_m73s(Print& print){
  180. std::vector<double> percent{};
  181. bool got_100 = false;
  182. bool extruding_after_100 = 0;
  183. GCodeReader parser;
  184. std::string gcode = Slic3r::Test::gcode(print);
  185. parser.parse_buffer(gcode, [&] (Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line) {
  186. if (line.cmd_is("M73")) {
  187. std::optional<double> p = parse_axis(line.raw(), "P");
  188. if (!p) {
  189. FAIL("Failed to parse percent");
  190. }
  191. percent.emplace_back(*p);
  192. got_100 = p == Approx(100);
  193. }
  194. if (line.extruding(self) && got_100) {
  195. extruding_after_100 = true;
  196. }
  197. });
  198. INFO("M73 is never given more than 100%");
  199. for (const double value : percent) {
  200. CHECK(value <= 100);
  201. }
  202. INFO("No extrusions after M73 P100.");
  203. CHECK(!extruding_after_100);
  204. }
  205. TEST_CASE("M73s have correct percent values", "[GCode]") {
  206. DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config();
  207. SECTION("Single object") {
  208. config.set_deserialize_strict({
  209. {" gcode_flavor", "sailfish" },
  210. {" raft_layers", 3 },
  211. });
  212. Print print;
  213. Model model;
  214. Test::init_print({TestMesh::cube_20x20x20}, print, model, config);
  215. check_m73s(print);
  216. }
  217. SECTION("Two copies of single object") {
  218. config.set_deserialize_strict({
  219. {" gcode_flavor", "sailfish" },
  220. });
  221. Print print;
  222. Model model;
  223. Test::init_print({TestMesh::cube_20x20x20}, print, model, config, false, 2);
  224. check_m73s(print);
  225. if constexpr (debug_files) {
  226. std::ofstream gcode_file{"M73_2_copies.gcode"};
  227. gcode_file << Test::gcode(print);
  228. }
  229. }
  230. SECTION("Two objects") {
  231. config.set_deserialize_strict({
  232. {" gcode_flavor", "sailfish" },
  233. });
  234. Print print;
  235. Model model;
  236. Test::init_print({TestMesh::cube_20x20x20, TestMesh::cube_20x20x20}, print, model, config);
  237. check_m73s(print);
  238. }
  239. SECTION("One layer object") {
  240. config.set_deserialize_strict({
  241. {" gcode_flavor", "sailfish" },
  242. });
  243. Print print;
  244. Model model;
  245. TriangleMesh test_mesh{mesh(TestMesh::cube_20x20x20)};
  246. const auto layer_height = static_cast<float>(config.opt_float("layer_height"));
  247. test_mesh.scale(Vec3f{1.0F, 1.0F, layer_height/20.0F});
  248. Test::init_print({test_mesh}, print, model, config);
  249. check_m73s(print);
  250. if constexpr (debug_files) {
  251. std::ofstream gcode_file{"M73_one_layer.gcode"};
  252. gcode_file << Test::gcode(print);
  253. }
  254. }
  255. }
  256. TEST_CASE("M201 for acceleation reset", "[GCode]") {
  257. DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config();
  258. config.set_deserialize_strict({
  259. { "gcode_flavor", "repetier" },
  260. { "default_acceleration", 1337 },
  261. });
  262. GCodeReader parser;
  263. std::string gcode = Slic3r::Test::slice({TestMesh::cube_with_hole}, config);
  264. bool has_accel = false;
  265. bool has_m204 = false;
  266. parser.parse_buffer(gcode, [&] (Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line) {
  267. if (line.cmd_is("M201") && line.has_x() && line.has_y()) {
  268. if (line.x() == 1337 && line.y() == 1337) {
  269. has_accel = true;
  270. }
  271. }
  272. if (line.cmd_is("M204") && line.raw().find('S') != std::string::npos) {
  273. has_m204 = true;
  274. }
  275. });
  276. INFO("M201 is generated for repetier firmware.");
  277. CHECK(has_accel);
  278. INFO("M204 is not generated for repetier firmware");
  279. CHECK(!has_m204);
  280. }