#include #include "libslic3r/GCodeReader.hpp" #include "libslic3r/Config.hpp" #include "libslic3r/Geometry.hpp" #include "libslic3r/Geometry/ConvexHull.hpp" #include #include #include "test_data.hpp" // get access to init_print, etc using namespace Slic3r::Test; using namespace Slic3r; /// Helper method to find the tool used for the brim (always the first extrusion). [[maybe_unused]] static int get_brim_tool(const std::string &gcode) { int brim_tool = -1; int tool = -1; GCodeReader parser; parser.parse_buffer(gcode, [&tool, &brim_tool] (Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line) { // if the command is a T command, set the the current tool if (boost::starts_with(line.cmd(), "T")) { tool = atoi(line.cmd().data() + 1); } else if (line.cmd() == "G1" && line.extruding(self) && line.dist_XY(self) > 0 && brim_tool < 0) { brim_tool = tool; } }); return brim_tool; } // [NotWorking]: slice() intermittently throws clipper's "Coordinate outside allowed // range" in CI (Linux) while passing locally. Disabled pending a root-cause fix in a // follow-up PR. TEST_CASE("Skirt height is honored", "[SkirtBrim][NotWorking]") { DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config(); config.set_deserialize_strict({ { "skirt_loops", 1 }, { "skirt_height", 5 }, { "wall_loops", 0 }, { "gcode_comments", true } }); std::string gcode; SECTION("printing a single object") { gcode = Slic3r::Test::slice({TestMesh::cube_20x20x20}, config); } SECTION("printing multiple objects") { gcode = Slic3r::Test::slice({TestMesh::cube_20x20x20, TestMesh::cube_20x20x20}, config); } REQUIRE(layers_with_role(gcode, "skirt").size() == (size_t)config.opt_int("skirt_height")); } // [NotWorking]: see "Skirt height is honored" above; same CI-only clipper range throw. SCENARIO("Skirt and brim generation", "[SkirtBrim][NotWorking]") { GIVEN("A default configuration") { DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config(); config.set_num_extruders(4); config.set_deserialize_strict({ { "initial_layer_print_height", 0.3 }, { "gcode_comments", true }, // avoid altering speeds unexpectedly { "slow_down_for_layer_cooling", false }, { "initial_layer_speed", "100%" }, // remove noise from top/solid layers { "top_shell_layers", 0 }, { "bottom_shell_layers", 1 }, { "machine_start_gcode", "T[initial_tool]\n" } }); WHEN("Brim width is set to 5") { config.set_deserialize_strict({ { "wall_loops", 0 }, { "skirt_loops", 0 }, { "brim_type", "outer_only" }, { "brim_width", 5 } }); THEN("Brim is generated") { std::string gcode = Slic3r::Test::slice({TestMesh::cube_20x20x20}, config); REQUIRE(! layers_with_role(gcode, "brim").empty()); } } #if 0 // This is a real error! One shall print the brim with the external perimeter extruder! WHEN("Perimeter extruder = 2 and support extruders = 3") { THEN("Brim is printed with the extruder used for the perimeters of first object") { config.set_deserialize_strict({ { "skirts", 0 }, { "brim_width", 5 }, { "perimeter_extruder", 2 }, { "support_material_extruder", 3 }, { "infill_extruder", 4 } }); std::string gcode = Slic3r::Test::slice({TestMesh::cube_20x20x20}, config); int tool = get_brim_tool(gcode); REQUIRE(tool == config.opt_int("perimeter_extruder") - 1); } } WHEN("Perimeter extruder = 2, support extruders = 3, raft is enabled") { THEN("brim is printed with same extruder as skirt") { config.set_deserialize_strict({ { "skirts", 0 }, { "brim_width", 5 }, { "perimeter_extruder", 2 }, { "support_material_extruder", 3 }, { "infill_extruder", 4 }, { "raft_layers", 1 } }); std::string gcode = Slic3r::Test::slice({TestMesh::cube_20x20x20}, config); int tool = get_brim_tool(gcode); REQUIRE(tool == config.opt_int("support_material_extruder") - 1); } } #endif WHEN("brim width to 1 with layer_width of 0.5") { config.set_deserialize_strict({ { "skirt_loops", 0 }, { "initial_layer_line_width", 0.5 }, { "brim_type", "outer_only" }, { "brim_width", 1 } }); THEN("2 brim lines") { Slic3r::Print print; Slic3r::Test::init_and_process_print({TestMesh::cube_20x20x20}, print, config); size_t total_entities = 0; for (const auto& pair : print.get_brimMap()) { total_entities += pair.second.entities.size(); } REQUIRE(total_entities == 2); } } #if 0 WHEN("brim ears on a square") { config.set_deserialize_strict({ { "skirts", 0 }, { "first_layer_extrusion_width", 0.5 }, { "brim_width", 1 }, { "brim_ears", 1 }, { "brim_ears_max_angle", 91 } }); Slic3r::Print print; Slic3r::Test::init_and_process_print({TestMesh::cube_20x20x20}, print, config); THEN("Four brim ears") { REQUIRE(print.brim().entities.size() == 4); } } WHEN("brim ears on a square but with a too small max angle") { config.set_deserialize_strict({ { "skirts", 0 }, { "first_layer_extrusion_width", 0.5 }, { "brim_width", 1 }, { "brim_ears", 1 }, { "brim_ears_max_angle", 89 } }); THEN("no brim") { Slic3r::Print print; Slic3r::Test::init_and_process_print({ TestMesh::cube_20x20x20 }, print, config); REQUIRE(print.brim().entities.size() == 0); } } #endif WHEN("Object is plated with overhang support and a brim") { config.set_deserialize_strict({ { "layer_height", 0.4 }, { "initial_layer_print_height", 0.4 }, { "skirt_loops", 1 }, { "skirt_distance", 0 }, { "enable_support", 1 }, { "brim_type", "outer_only" }, { "brim_width", 5 } }); THEN("Support and brim are both emitted") { std::string gcode = Slic3r::Test::slice({TestMesh::overhang}, config); REQUIRE(! layers_with_role(gcode, "support").empty()); REQUIRE(! layers_with_role(gcode, "brim").empty()); } } WHEN("an object with support is surrounded by a skirt") { config.set_deserialize_strict({ { "enable_support", 1 }, { "skirt_loops", 1 }, { "skirt_distance", 2 }, { "brim_type", "no_brim" }, { "z_hop", 0 } }); THEN("the skirt is long enough to enclose the object and its support") { std::string gcode = Slic3r::Test::slice({TestMesh::overhang}, config); const double first_layer_z = config.opt_float("initial_layer_print_height"); // On the first layer, accumulate the skirt loop length and collect the // object + support extrusion points; the skirt must enclose them. double skirt_length = 0.0; Points footprint; GCodeReader parser; parser.parse_buffer(gcode, [&] (GCodeReader& self, const GCodeReader::GCodeLine& line) { if (! line.extruding(self) || line.dist_XY(self) <= 0 || std::abs(self.z() - first_layer_z) > 0.01) return; if (line.comment().find("skirt") != std::string_view::npos) skirt_length += line.dist_XY(self); else footprint.push_back(Point::new_scale(line.new_X(self), line.new_Y(self))); }); const double hull_perimeter = unscale(Geometry::convex_hull(footprint).split_at_first_point().length()); REQUIRE(hull_perimeter > 0.0); // guard against an empty footprint passing trivially REQUIRE(skirt_length > hull_perimeter); } } WHEN("Large minimum skirt length is used.") { // One skirt loop around a 20mm cube is ~88mm, so 500mm forces extra loops. config.set_deserialize_strict({ { "skirt_loops", 1 }, { "min_skirt_length", 500 } }); THEN("The skirt is extended to at least the minimum length") { std::string gcode = Slic3r::Test::slice({TestMesh::cube_20x20x20}, config); double skirt_length = 0.0; GCodeReader parser; parser.parse_buffer(gcode, [&skirt_length] (GCodeReader& self, const GCodeReader::GCodeLine& line) { if (line.extruding(self) && line.comment().find("skirt") != std::string_view::npos) skirt_length += line.dist_XY(self); }); REQUIRE(skirt_length >= 500.0); } } } }