Files
OrcaSlicer/tests/fff_print/test_skirt_brim.cpp
raistlin7447 3cf0f4dba5 test(fff_print): re-enable tests disabled by the m_origin clipper throw (#14482)
PR #13712 fixed the uninitialized Print::m_origin (commit 99dea01cc3, "Fix coord
out-of-range exception caused by m_origin memory not initialized to 0") that made
headless slice() intermittently throw ClipperLib's "Coordinate outside allowed
range". With that root cause fixed, the three tests disabled for it pass again,
so drop their [NotWorking] tags.
2026-06-30 09:03:44 +08:00

236 lines
9.3 KiB
C++

#include <catch2/catch_all.hpp>
#include "libslic3r/GCodeReader.hpp"
#include "libslic3r/Config.hpp"
#include "libslic3r/Geometry.hpp"
#include "libslic3r/Geometry/ConvexHull.hpp"
#include <boost/algorithm/string.hpp>
#include <cmath>
#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 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;
}
TEST_CASE("Skirt height is honored", "[SkirtBrim]") {
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"));
}
SCENARIO("Skirt and brim generation", "[SkirtBrim]") {
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<double>(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);
}
}
}
}