test_multi.cpp 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  1. #include <catch2/catch.hpp>
  2. #include <numeric>
  3. #include <sstream>
  4. #include "libslic3r/ClipperUtils.hpp"
  5. #include "libslic3r/Geometry.hpp"
  6. #include "libslic3r/Geometry/ConvexHull.hpp"
  7. #include "libslic3r/Print.hpp"
  8. #include "libslic3r/libslic3r.h"
  9. #include "test_data.hpp"
  10. using namespace Slic3r;
  11. using namespace std::literals;
  12. SCENARIO("Basic tests", "[Multi]")
  13. {
  14. WHEN("Slicing multi-material print with non-consecutive extruders") {
  15. std::string gcode = Slic3r::Test::slice({ Slic3r::Test::TestMesh::cube_20x20x20 },
  16. {
  17. { "nozzle_diameter", "0.6, 0.6, 0.6, 0.6" },
  18. { "extruder", 2 },
  19. { "infill_extruder", 4 },
  20. { "support_material_extruder", 0 }
  21. });
  22. THEN("Sliced successfully") {
  23. REQUIRE(! gcode.empty());
  24. }
  25. THEN("T3 toolchange command found") {
  26. bool T1_found = gcode.find("\nT3\n") != gcode.npos;
  27. REQUIRE(T1_found);
  28. }
  29. }
  30. WHEN("Slicing with multiple skirts with a single, non-zero extruder") {
  31. std::string gcode = Slic3r::Test::slice({ Slic3r::Test::TestMesh::cube_20x20x20 },
  32. {
  33. { "nozzle_diameter", "0.6, 0.6, 0.6, 0.6" },
  34. { "perimeter_extruder", 2 },
  35. { "infill_extruder", 2 },
  36. { "support_material_extruder", 2 },
  37. { "support_material_interface_extruder", 2 },
  38. });
  39. THEN("Sliced successfully") {
  40. REQUIRE(! gcode.empty());
  41. }
  42. }
  43. }
  44. SCENARIO("Ooze prevention", "[Multi]")
  45. {
  46. DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config_with({
  47. { "nozzle_diameter", "0.6, 0.6, 0.6, 0.6" },
  48. { "raft_layers", 2 },
  49. { "infill_extruder", 2 },
  50. { "solid_infill_extruder", 3 },
  51. { "support_material_extruder", 4 },
  52. { "ooze_prevention", 1 },
  53. { "extruder_offset", "0x0, 20x0, 0x20, 20x20" },
  54. { "temperature", "200, 180, 170, 160" },
  55. { "first_layer_temperature", "206, 186, 166, 156" },
  56. // test that it doesn't crash when this is supplied
  57. { "toolchange_gcode", "T[next_extruder] ;toolchange" }
  58. });
  59. FullPrintConfig print_config;
  60. print_config.apply(config);
  61. // Since July 2019, PrusaSlicer only emits automatic Tn command in case that the toolchange_gcode is empty
  62. // The "T[next_extruder]" is therefore needed in this test.
  63. std::string gcode = Slic3r::Test::slice({ Slic3r::Test::TestMesh::cube_20x20x20 }, config);
  64. GCodeReader parser;
  65. int tool = -1;
  66. int tool_temp[] = { 0, 0, 0, 0};
  67. Points toolchange_points;
  68. Points extrusion_points;
  69. parser.parse_buffer(gcode, [&tool, &tool_temp, &toolchange_points, &extrusion_points, &print_config]
  70. (Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line)
  71. {
  72. // if the command is a T command, set the the current tool
  73. if (boost::starts_with(line.cmd(), "T")) {
  74. // Ignore initial toolchange.
  75. if (tool != -1) {
  76. int expected_temp = is_approx<double>(self.z(), print_config.get_abs_value("first_layer_height") + print_config.z_offset) ?
  77. print_config.first_layer_temperature.get_at(tool) :
  78. print_config.temperature.get_at(tool);
  79. if (tool_temp[tool] != expected_temp + print_config.standby_temperature_delta)
  80. throw std::runtime_error("Standby temperature was not set before toolchange.");
  81. toolchange_points.emplace_back(self.xy_scaled());
  82. }
  83. tool = atoi(line.cmd().data() + 1);
  84. } else if (line.cmd_is("M104") || line.cmd_is("M109")) {
  85. // May not be defined on this line.
  86. int t = tool;
  87. line.has_value('T', t);
  88. // Should be available on this line.
  89. int s;
  90. if (! line.has_value('S', s))
  91. throw std::runtime_error("M104 or M109 without S");
  92. // Following is obsolete. The first printing extruder is newly set to its first layer temperature immediately, not to the standby.
  93. //if (tool_temp[t] == 0 && s != print_config.first_layer_temperature.get_at(t) + print_config.standby_temperature_delta)
  94. // throw std::runtime_error("initial temperature is not equal to first layer temperature + standby delta");
  95. tool_temp[t] = s;
  96. } else if (line.cmd_is("G1") && line.extruding(self) && line.dist_XY(self) > 0) {
  97. extrusion_points.emplace_back(line.new_XY_scaled(self) + scaled<coord_t>(print_config.extruder_offset.get_at(tool)));
  98. }
  99. });
  100. Polygon convex_hull = Geometry::convex_hull(extrusion_points);
  101. // THEN("all nozzles are outside skirt at toolchange") {
  102. // Points t;
  103. // sort_remove_duplicates(toolchange_points);
  104. // size_t inside = 0;
  105. // for (const auto &point : toolchange_points)
  106. // for (const Vec2d &offset : print_config.extruder_offset.values) {
  107. // Point p = point + scaled<coord_t>(offset);
  108. // if (convex_hull.contains(p))
  109. // ++ inside;
  110. // }
  111. // REQUIRE(inside == 0);
  112. // }
  113. #if 0
  114. require "Slic3r/SVG.pm";
  115. Slic3r::SVG::output(
  116. "ooze_prevention_test.svg",
  117. no_arrows => 1,
  118. polygons => [$convex_hull],
  119. red_points => \@t,
  120. points => \@toolchange_points,
  121. );
  122. #endif
  123. THEN("all toolchanges happen within expected area") {
  124. // offset the skirt by the maximum displacement between extruders plus a safety extra margin
  125. const float delta = scaled<float>(20. * sqrt(2.) + 1.);
  126. Polygon outer_convex_hull = expand(convex_hull, delta).front();
  127. size_t inside = std::count_if(toolchange_points.begin(), toolchange_points.end(), [&outer_convex_hull](const Point &p){ return outer_convex_hull.contains(p); });
  128. REQUIRE(inside == toolchange_points.size());
  129. }
  130. }
  131. std::string slice_stacked_cubes(const DynamicPrintConfig &config, const DynamicPrintConfig &volume1config, const DynamicPrintConfig &volume2config)
  132. {
  133. Model model;
  134. ModelObject *object = model.add_object();
  135. object->name = "object.stl";
  136. ModelVolume *v1 = object->add_volume(Test::mesh(Test::TestMesh::cube_20x20x20));
  137. v1->set_material_id("lower_material");
  138. v1->config.assign_config(volume1config);
  139. ModelVolume *v2 = object->add_volume(Test::mesh(Test::TestMesh::cube_20x20x20));
  140. v2->set_material_id("upper_material");
  141. v2->translate(0., 0., 20.);
  142. v2->config.assign_config(volume2config);
  143. object->add_instance();
  144. object->ensure_on_bed();
  145. Print print;
  146. print.auto_assign_extruders(object);
  147. THEN("auto_assign_extruders() assigned correct extruder to first volume") {
  148. REQUIRE(v1->config.extruder() == 1);
  149. }
  150. THEN("auto_assign_extruders() assigned correct extruder to second volume") {
  151. REQUIRE(v2->config.extruder() == 2);
  152. }
  153. print.apply(model, config);
  154. print.validate();
  155. return Test::gcode(print);
  156. }
  157. SCENARIO("Stacked cubes", "[Multi]")
  158. {
  159. DynamicPrintConfig lower_config;
  160. lower_config.set_deserialize_strict({
  161. { "extruder", 1 },
  162. { "bottom_solid_layers", 0 },
  163. { "top_solid_layers", 1 },
  164. });
  165. DynamicPrintConfig upper_config;
  166. upper_config.set_deserialize_strict({
  167. { "extruder", 2 },
  168. { "bottom_solid_layers", 1 },
  169. { "top_solid_layers", 0 }
  170. });
  171. static constexpr const double solid_infill_speed = 99;
  172. auto config = Slic3r::DynamicPrintConfig::full_print_config_with({
  173. { "nozzle_diameter", "0.6, 0.6, 0.6, 0.6" },
  174. { "fill_density", 0 },
  175. { "solid_infill_speed", solid_infill_speed },
  176. { "top_solid_infill_speed", solid_infill_speed },
  177. // for preventing speeds from being altered
  178. { "cooling", "0, 0, 0, 0" },
  179. // for preventing speeds from being altered
  180. { "first_layer_speed", "100%" }
  181. });
  182. auto test_shells = [](const std::string &gcode) {
  183. GCodeReader parser;
  184. int tool = -1;
  185. // Scaled Z heights.
  186. std::set<coord_t> T0_shells, T1_shells;
  187. parser.parse_buffer(gcode, [&tool, &T0_shells, &T1_shells]
  188. (Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line)
  189. {
  190. if (boost::starts_with(line.cmd(), "T")) {
  191. tool = atoi(line.cmd().data() + 1);
  192. } else if (line.cmd() == "G1" && line.extruding(self) && line.dist_XY(self) > 0) {
  193. if (is_approx<double>(line.new_F(self), solid_infill_speed * 60.) && (tool == 0 || tool == 1))
  194. (tool == 0 ? T0_shells : T1_shells).insert(scaled<coord_t>(self.z()));
  195. }
  196. });
  197. return std::make_pair(T0_shells, T1_shells);
  198. };
  199. WHEN("Interface shells disabled") {
  200. std::string gcode = slice_stacked_cubes(config, lower_config, upper_config);
  201. auto [t0, t1] = test_shells(gcode);
  202. THEN("no interface shells") {
  203. REQUIRE(t0.empty());
  204. REQUIRE(t1.empty());
  205. }
  206. }
  207. WHEN("Interface shells enabled") {
  208. config.set_deserialize_strict("interface_shells", "1");
  209. std::string gcode = slice_stacked_cubes(config, lower_config, upper_config);
  210. auto [t0, t1] = test_shells(gcode);
  211. THEN("top interface shells") {
  212. REQUIRE(t0.size() == lower_config.opt_int("top_solid_layers"));
  213. }
  214. THEN("bottom interface shells") {
  215. REQUIRE(t1.size() == upper_config.opt_int("bottom_solid_layers"));
  216. }
  217. }
  218. WHEN("Slicing with auto-assigned extruders") {
  219. auto config = Slic3r::DynamicPrintConfig::full_print_config_with({
  220. { "nozzle_diameter", "0.6,0.6,0.6,0.6" },
  221. { "layer_height", 0.4 },
  222. { "first_layer_height", 0.4 },
  223. { "skirts", 0 }
  224. });
  225. std::string gcode = slice_stacked_cubes(config, DynamicPrintConfig{}, DynamicPrintConfig{});
  226. GCodeReader parser;
  227. int tool = -1;
  228. // Scaled Z heights.
  229. std::set<coord_t> T0_shells, T1_shells;
  230. parser.parse_buffer(gcode, [&tool, &T0_shells, &T1_shells](Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line)
  231. {
  232. if (boost::starts_with(line.cmd(), "T")) {
  233. tool = atoi(line.cmd().data() + 1);
  234. } else if (line.cmd() == "G1" && line.extruding(self) && line.dist_XY(self) > 0) {
  235. if (tool == 0 && self.z() > 20)
  236. // Layers incorrectly extruded with T0 at the top object.
  237. T0_shells.insert(scaled<coord_t>(self.z()));
  238. else if (tool == 1 && self.z() < 20)
  239. // Layers incorrectly extruded with T1 at the bottom object.
  240. T1_shells.insert(scaled<coord_t>(self.z()));
  241. }
  242. });
  243. THEN("T0 is never used for upper object") {
  244. REQUIRE(T0_shells.empty());
  245. }
  246. THEN("T0 is never used for lower object") {
  247. REQUIRE(T1_shells.empty());
  248. }
  249. }
  250. }