#include #include "libslic3r/ClipperUtils.hpp" #include "libslic3r/GCodeReader.hpp" #include "libslic3r/Layer.hpp" #include "libslic3r/Config.hpp" #include "libslic3r/Geometry.hpp" #include "libslic3r/Geometry/ConvexHull.hpp" #include #include #include "test_helpers.hpp" // get access to init_print, etc using namespace Slic3r::Test; using namespace Slic3r; // Distinct brim regions (combine_brims merges touching brims into one covering >1 object). static int brim_count(const Print &print) { int n = 0; for (const auto &group : print.skirt_brim_groups()) n += (int) group.brims.size(); return n; } // Total brim loops across all objects. static size_t brim_loop_count(Print &print) { size_t n = 0; for (const auto &kv : print.get_brimMap()) n += kv.second.items_count(); return n; } // The span is skirt_height layers, or every layer when a draft shield is on (forced even at // height 0); per-object skirts are rejected in By object printing (no room between objects). TEST_CASE("Skirt is emitted once per layer it spans", "[SkirtBrim]") { const int object_layers = 100; // 20mm cube at 0.2mm layers const char *skirt_type = GENERATE("combined", "perobject"); const char *print_seq = GENERATE("by layer", "by object"); const char *draft_shield = GENERATE("disabled", "enabled"); const int skirt_height = GENERATE(0, 1, 3); DYNAMIC_SECTION(skirt_type << " | " << print_seq << " | draft=" << draft_shield << " | height=" << skirt_height) { auto do_slice = [&] { return slice_two_cubes_arranged({ { "skirt_loops", 1 }, { "skirt_height", skirt_height }, { "skirt_distance", 3 }, { "skirt_type", skirt_type }, { "draft_shield", draft_shield }, { "print_sequence", print_seq }, { "layer_height", 0.2 }, }); }; const bool draft = std::string(draft_shield) == "enabled"; const bool has_skirt = draft || skirt_height > 0; const bool unsafe_by_object = std::string(skirt_type) == "perobject" && std::string(print_seq) == "by object" && has_skirt; if (unsafe_by_object) { REQUIRE_THROWS(do_slice()); } else { const int expected_layers = draft ? object_layers : skirt_height; CHECK(role_passes(do_slice(), "skirt") == expected_layers); } } } // Each per-object skirt prints right before its own object, so distant objects yield two // non-contiguous skirt passes; close objects group into a single skirt. TEST_CASE("Per-object skirts group when objects are close", "[SkirtBrim]") { auto [gap, expected_skirts] = GENERATE(table({ { 5.0, 1 }, { 60.0, 2 } })); DYNAMIC_SECTION("gap=" << gap) { const std::string gcode = slice_two_cubes_apart(gap, { { "skirt_loops", 1 }, { "skirt_height", 1 }, { "skirt_distance", 3 }, { "skirt_type", "perobject" }, { "print_sequence", "by layer" }, { "layer_height", 0.2 }, }); CHECK(role_passes(gcode, "skirt") == expected_skirts); } } TEST_CASE("Per-object skirt is generated per instance", "[SkirtBrim]") { Print print; Model model; place_two_cube_instances_apart(60, { { "skirt_type", "perobject" }, { "skirt_height", 1 }, { "skirt_distance", 2 }, { "skirt_loops", 1 }, { "brim_type", "no_brim" }, }, print, model); print.process(); REQUIRE(print.skirt_brim_groups().size() == 2); REQUIRE(print.skirt().items_count() == 2); for (const Print::SkirtBrimGroup &group : print.skirt_brim_groups()) { REQUIRE(group.instances.size() == 1); REQUIRE(group.instances.front().object_id == print.get_object(0)->id()); } } TEST_CASE("Combine brims merges touching brims", "[SkirtBrim]") { auto [gap, combine, expected_brims] = GENERATE(table({ { 5.0, 1, 1 }, // touching + combine -> one merged brim { 5.0, 0, 2 }, // touching, no combine -> separate { 60.0, 1, 2 }, // far apart -> nothing to merge })); DYNAMIC_SECTION("gap=" << gap << " combine_brims=" << combine) { Print print; Model model; place_two_cubes_apart(gap, { { "skirt_loops", 1 }, { "skirt_height", 1 }, { "skirt_distance", 3 }, { "skirt_type", "perobject" }, { "print_sequence", "by layer" }, { "brim_type", "outer_only" }, { "brim_width", 5 }, { "combine_brims", combine }, { "layer_height", 0.2 }, }, print, model); print.process(); CHECK(brim_count(print) == expected_brims); } } TEST_CASE("Object brims are generated per instance", "[SkirtBrim]") { Print print; Model model; place_two_cube_instances_apart(60, { { "skirt_loops", 0 }, { "brim_type", "outer_only" }, { "brim_width", 5 }, { "combine_brims", 0 }, }, print, model); print.process(); REQUIRE(print.skirt_brim_groups().size() == 1); REQUIRE(print.skirt_brim_groups().front().brims.size() == 2); for (const Print::SkirtBrimGroup::Brim &brim : print.skirt_brim_groups().front().brims) { REQUIRE(brim.instances.size() == 1); REQUIRE(brim.instances.front().object_id == print.get_object(0)->id()); } } TEST_CASE("Uncombined neighboring brims precede their respective objects", "[SkirtBrim]") { Print print; Model model; place_two_cubes_apart(0, { { "skirt_loops", 0 }, { "brim_type", "outer_only" }, { "brim_width", 5 }, { "combine_brims", 0 }, }, print, model); print.process(); REQUIRE(print.skirt_brim_groups().size() == 1); REQUIRE(print.skirt_brim_groups().front().brims.size() == 2); CHECK(role_sequence(gcode(print), { "brim", "perimeter" }) == std::vector{ "brim", "perimeter", "brim", "perimeter" }); } TEST_CASE("Combine brims merges neighboring object instances", "[SkirtBrim]") { Print print; Model model; place_two_cube_instances_apart(5, { { "skirt_loops", 0 }, { "brim_type", "outer_only" }, { "brim_width", 5 }, { "combine_brims", 1 }, }, print, model); print.process(); REQUIRE(print.skirt_brim_groups().size() == 1); REQUIRE(print.skirt_brim_groups().front().brims.size() == 1); REQUIRE(print.skirt_brim_groups().front().brims.front().instances.size() == 2); const std::vector expected{ "brim", "perimeter" }; CHECK(role_sequence(gcode(print), { "brim", "perimeter" }) == expected); } // Each object's skirt and brim come right before that object, not all skirts then all brims first. TEST_CASE("By-layer per-object skirt and brim precede each object", "[SkirtBrim]") { const std::string gcode = slice_two_cubes_apart(60, { // far apart: a skirt+brim per object { "skirt_loops", 1 }, { "skirt_height", 1 }, { "skirt_distance", 3 }, { "skirt_type", "perobject" }, { "print_sequence", "by layer" }, { "brim_type", "outer_only" }, { "brim_width", 5 }, { "layer_height", 0.2 }, }); const std::vector expected{ "skirt", "brim", "perimeter", "skirt", "brim", "perimeter" }; CHECK(role_sequence(gcode, { "skirt", "brim", "perimeter" }) == expected); } // A square's corners are 90 degrees, so they get ears only when brim_ears_max_angle is above 90. TEST_CASE("Brim ears appear only at corners within the max angle", "[SkirtBrim]") { auto [max_angle, expect_ears] = GENERATE(table({ { 91, true }, { 90, false }, { 89, false } })); DYNAMIC_SECTION("brim_ears_max_angle=" << max_angle) { Print print; init_and_process_print({ cube(20) }, print, { { "skirt_loops", 0 }, { "brim_type", "brim_ears" }, { "brim_width", 1 }, { "brim_ears_max_angle", max_angle }, { "initial_layer_line_width", 0.5 }, }); if (expect_ears) CHECK(brim_loop_count(print) > 0); else CHECK(brim_loop_count(print) == 0); } } SCENARIO("Skirt has the configured number of loops", "[SkirtBrim]") { GIVEN("20mm cube and default config") { WHEN("skirt_loops is set to 2") { Print print; init_and_process_print({cube(20)}, print, { { "skirt_height", 1 }, { "skirt_distance", 1 }, { "skirt_loops", 2 } }); THEN("Skirt Extrusion collection has 2 loops in it") { REQUIRE(print.skirt().items_count() == 2); REQUIRE(print.skirt().flatten().entities.size() == 2); } } } } SCENARIO("Brim has the configured number of loops", "[SkirtBrim]") { GIVEN("20mm cube and default config, 1mm first layer width") { WHEN("Brim is set to 6mm") { Print print; init_and_process_print({cube(20)}, print, { { "brim_type", "outer_only" }, { "initial_layer_line_width", 1 }, { "brim_width", 6 } }); THEN("Brim Extrusion collection has 6 loops in it") { REQUIRE(brim_loop_count(print) == 6); } } WHEN("Brim is set to 6mm, extrusion width 0.5mm") { Print print; init_and_process_print({cube(20)}, print, { { "brim_type", "outer_only" }, { "brim_width", 6 }, { "initial_layer_line_width", 0.5 } }); THEN("Brim Extrusion collection has 12 loops in it") { REQUIRE(brim_loop_count(print) == 12); } } } } static double first_extrusion_feedrate_for_feature(const std::string &gcode, const std::string_view feature) { double feedrate = 0.0; bool feature_active = false; GCodeReader parser; parser.parse_buffer(gcode, [&feedrate, &feature_active, feature] (GCodeReader &self, const GCodeReader::GCodeLine &line) { const std::string_view comment = line.comment(); if (comment.find("FEATURE:") != std::string_view::npos || comment.find("TYPE:") != std::string_view::npos) feature_active = comment.find(feature) != std::string_view::npos; if (feature_active && line.extruding(self) && line.dist_XY(self) > 0) { feedrate = line.new_F(self); self.quit_parsing(); } }); return feedrate; } TEST_CASE("Skirt height is honored", "[SkirtBrim]") { DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); config.set_deserialize_strict({ { "skirt_loops", 1 }, { "skirt_height", 5 }, { "wall_loops", 0 }, }); std::string gcode; SECTION("printing a single object") { gcode = slice({ cube(20) }, config); } SECTION("printing multiple objects") { gcode = slice({ cube(20), cube(20) }, config); } REQUIRE(layers_with_role(gcode, "skirt").size() == (size_t) config.opt_int("skirt_height")); } TEST_CASE("Brim uses first layer speed", "[SkirtBrim]") { DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config(); config.set_deserialize_strict({ { "brim_type", "outer_only" }, { "brim_width", 5 }, { "gcode_comments", true }, { "initial_layer_speed", 10 }, { "initial_layer_infill_speed", 20 }, { "machine_start_gcode", "" }, { "skirt_loops", 0 }, { "slow_down_for_layer_cooling", false }, { "z_hop", 0 } }); const std::string gcode = Slic3r::Test::slice({cube(20)}, config); const double brim_feedrate = first_extrusion_feedrate_for_feature(gcode, "Brim"); REQUIRE(brim_feedrate > 0.0); REQUIRE_THAT(brim_feedrate, Catch::Matchers::WithinAbs(600.0, 1e-3)); const double bottom_surface_feedrate = first_extrusion_feedrate_for_feature(gcode, "Bottom surface"); REQUIRE(bottom_surface_feedrate > 0.0); REQUIRE_THAT(bottom_surface_feedrate, Catch::Matchers::WithinAbs(1200.0, 1e-3)); } SCENARIO("Skirt and brim generation", "[SkirtBrim]") { GIVEN("A default configuration") { DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); config.set_num_extruders(4); config.set_deserialize_strict({ { "initial_layer_print_height", 0.3 }, // 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 = slice({ cube(20) }, config); REQUIRE(! layers_with_role(gcode, "brim").empty()); } } 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") { Print print; init_and_process_print({ cube(20) }, print, config); REQUIRE(brim_loop_count(print) == 2); } } 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 = 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 = 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 = slice({ cube(20) }, 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); } } } } // Belt printers --------------------------------------------------------------- // // On a tilted belt the brim is laid onto the belt PLANE rather than into the Z=0 // bed plane, so it is spread across many layers instead of living on the first // one. The discriminating measurement is the number of contiguous brim runs in // the G-code: a flat plate brim gives a single run, a belt brim gives one per // layer that carries a band. Distinct Z values are useless here, because the // machine-frame transform couples Y into Z so every belt move has its own Z. static DynamicPrintConfig belt_brim_config() { DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); config.set_deserialize_strict({ { "belt_printer", 1 }, { "belt_slice_rotation", "x" }, { "belt_slice_rotation_angle", 45 }, { "belt_slice_rotation_global", 1 }, { "gcode_remap_x", "rev_x" }, { "gcode_remap_y", "pos_z" }, { "gcode_remap_z", "pos_y" }, { "layer_height", 0.2 }, { "initial_layer_print_height", 0.2 }, { "skirt_loops", 0 }, { "top_shell_layers", 0 }, { "bottom_shell_layers", 1 }, { "machine_start_gcode", "T[initial_tool]\n" }, }); return config; } TEST_CASE("Belt brim spans many layers instead of one", "[SkirtBrim][belt]") { DynamicPrintConfig config = belt_brim_config(); config.set_deserialize_strict({ { "brim_type", "outer_only" }, { "brim_width", 5 }, }); const std::string gcode = slice({ cube(20) }, config); // A plate-brim implementation would score 1 here. CHECK(role_passes(gcode, "brim") > 10); } TEST_CASE("Belt brim is absent when both widths are zero", "[SkirtBrim][belt]") { // The "no effect when disabled" guard: brim_type Auto is the shipped default and // reports has_brim() even at width 0, so this also pins the gate that keeps the // flat plate brim from running on a tilted belt. const char *brim_type = GENERATE("auto_brim", "outer_only", "no_brim"); DYNAMIC_SECTION("brim_type " << brim_type) { DynamicPrintConfig config = belt_brim_config(); config.set_deserialize_strict({ { "brim_type", brim_type }, { "brim_width", 0 }, { "leading_brim_length", 0 }, { "extra_brim_width", 0 }, }); const std::string gcode = slice({ cube(20) }, config); CHECK(role_passes(gcode, "brim") == 0); } } TEST_CASE("Leading brim length alone produces a belt brim", "[SkirtBrim][belt]") { // Exercises the leading_brim_length-only enablement path and the downhill sweep. DynamicPrintConfig config = belt_brim_config(); config.set_deserialize_strict({ { "brim_type", "outer_only" }, { "brim_width", 0 }, { "leading_brim_length", 5 }, { "brim_object_gap", 0 }, }); const std::string gcode = slice({ cube(20) }, config); CHECK(role_passes(gcode, "brim") > 0); } TEST_CASE("Leading brim length reaches further ahead of the object", "[SkirtBrim][belt]") { // Compared between two runs rather than against an absolute coordinate, so the // assertion survives any change of origin or axis remap. auto brim_extent = [](double extra) { DynamicPrintConfig config = belt_brim_config(); config.set_deserialize_strict({ { "brim_type", "outer_only" }, { "brim_width", 3 }, { "leading_brim_length", extra }, { "brim_object_gap", 0 }, }); const std::string gcode = slice({ cube(20) }, config); // The apron prints before the object reaches the belt, so it shows up as brim // extrusion at the lowest machine Z of any brim move. double min_z = std::numeric_limits::max(); GCodeReader parser; parser.parse_buffer(gcode, [&min_z](GCodeReader &self, const GCodeReader::GCodeLine &line) { if (line.extruding(self) && line.comment().find("brim") != std::string_view::npos) min_z = std::min(min_z, static_cast(self.z())); }); return min_z; }; const double without = brim_extent(0.); const double with = brim_extent(10.); REQUIRE(without < std::numeric_limits::max()); REQUIRE(with < std::numeric_limits::max()); CHECK(with < without); } TEST_CASE("Every brim type slices on a belt printer", "[SkirtBrim][belt]") { // Auto / Mouse ear / Painted collapse to outer-only rather than crashing or // silently producing nothing. const char *brim_type = GENERATE("auto_brim", "brim_ears", "painted", "outer_only", "inner_only", "outer_and_inner", "no_brim"); DYNAMIC_SECTION("brim_type " << brim_type) { DynamicPrintConfig config = belt_brim_config(); config.set_deserialize_strict({ { "brim_type", brim_type }, { "brim_width", 5 }, }); const std::string gcode = slice({ cube(20) }, config); REQUIRE(! gcode.empty()); if (std::string(brim_type) == "no_brim") CHECK(role_passes(gcode, "brim") == 0); else if (std::string(brim_type) != "inner_only") // A solid cube has no holes, so inner_only legitimately yields nothing. CHECK(role_passes(gcode, "brim") > 0); } } TEST_CASE("An untilted belt printer gets no brim", "[SkirtBrim][belt]") { // Belt brim needs a tilt to have a belt plane to lie on, and the flat plate brim // cannot reach the G-code on any belt printer: it is emitted out of // skirt_brim_groups(), which _make_skirt() builds, and that returns early for every // belt printer. So an untilted belt printer gets nothing - unchanged by this // feature. Making the flat brim work here would mean reopening the belt skirt gate, // which is a separate change; Print::validate() warns instead. DynamicPrintConfig config = belt_brim_config(); config.set_deserialize_strict({ { "belt_slice_rotation", "none" }, { "brim_type", "outer_only" }, { "brim_width", 5 }, }); const std::string gcode = slice({ cube(20) }, config); CHECK(role_passes(gcode, "brim") == 0); } TEST_CASE("Belt brim does not resurrect the skirt", "[SkirtBrim][belt]") { DynamicPrintConfig config = belt_brim_config(); config.set_deserialize_strict({ { "brim_type", "outer_only" }, { "brim_width", 5 }, { "skirt_loops", 2 }, }); const std::string gcode = slice({ cube(20) }, config); CHECK(role_passes(gcode, "skirt") == 0); } TEST_CASE("Belt brim lines all have the same width", "[SkirtBrim][belt]") { // Each brim line's extrusion volume comes from its nozzle-to-belt clearance. Anchoring // every line to a fixed fraction of its own band gives them all the same clearance, so // they all come out the same width. The nominal-spacing lattice this replaced let each // line land wherever it fell inside its band, so the clearance - and the width with it - // varied by 2x, which showed up as visibly ragged brim. DynamicPrintConfig config = belt_brim_config(); config.set_deserialize_strict({ { "brim_type", "outer_only" }, { "brim_width", 5 }, { "brim_object_gap", 0 }, }); Print print; init_and_process_print({ cube(20) }, print, config); const PrintObject *obj = print.objects().front(); std::vector widths; auto collect = [&widths](const ExtrusionEntityCollection &coll) { for (const ExtrusionEntity *ee : coll.entities) if (const auto *path = dynamic_cast(ee)) widths.push_back(path->width); }; for (const ExtrusionEntityCollection &band : obj->belt_brim_by_layer()) collect(band); for (const BeltBrimBand &band : obj->belt_brim_prologue()) collect(band.fills); REQUIRE(widths.size() > 10); const float lo = *std::min_element(widths.begin(), widths.end()); const float hi = *std::max_element(widths.begin(), widths.end()); CHECK_THAT(hi, Catch::Matchers::WithinRel(lo, 1e-4)); } TEST_CASE("Belt apron survives another object printing at the same Z", "[SkirtBrim][belt]") { // An apron band prints below its OWN object's first layer, but with two objects on the // belt the second one is already printing at that print_z. The layer then has an // object layer and takes the ordinary process_layer() path rather than the brim-only // branch, so the band must be emitted from both or it is silently dropped. A // single-object print cannot exercise this. auto brim_passes = [](int object_count) { DynamicPrintConfig config = belt_brim_config(); config.set_deserialize_strict({ { "brim_type", "outer_only" }, { "brim_width", 3 }, { "leading_brim_length", 8 }, { "brim_object_gap", 0 }, }); std::vector meshes; for (int i = 0; i < object_count; ++ i) { TriangleMesh m = cube(20); // Offset along the belt so the second object starts well after the first. m.translate(0.f, float(40 * i), 0.f); meshes.emplace_back(std::move(m)); } Print print; Model model; init_print(std::move(meshes), print, model, config); print.process(); return role_passes(gcode(print), "brim"); }; const int one = brim_passes(1); const int two = brim_passes(2); REQUIRE(one > 0); // Two identical objects should carry twice the brim. Merely asserting `two > one` // would not be decisive: the FIRST object's apron survives the bug, because nothing // else is printing that early, so only the second object's apron goes missing. // Requiring close to 2x is what actually detects the dropped bands. CHECK(two >= 1.8 * one); } TEST_CASE("Belt brim allows instances placed across the belt", "[SkirtBrim][belt]") { // Only movement ALONG the belt changes an instance's belt-floor Z, so copies placed // side by side ACROSS it share one set of bands and must still get a brim. The first // version of this guard refused every multi-instance object outright, silently // dropping the brim. // // The global belt flags are off here so the instances stay in one PrintObject; with // them on, PrintApply splits each instance into its own object and the case cannot // arise at all. auto multi_instance_has_brim = [](double dx, double dy) { DynamicPrintConfig config = belt_brim_config(); config.set_deserialize_strict({ { "belt_slice_rotation_global", 0 }, { "belt_preslice_global", 0 }, { "preslice_remap_global", 0 }, { "brim_type", "outer_only" }, { "brim_width", 4 }, { "brim_object_gap", 0 }, }); Print print; Model model; ModelObject *object = model.add_object(); object->name += "object.stl"; object->add_volume(cube(20)); object->add_instance()->set_offset(Vec3d(80., 80., 0.)); object->add_instance()->set_offset(Vec3d(80. + dx, 80. + dy, 0.)); object->ensure_on_bed(); print.auto_assign_extruders(object); print.apply(model, config); print.validate(); print.set_status_silent(); print.process(); REQUIRE(print.objects().size() == 1); REQUIRE(print.objects().front()->instances().size() == 2); return print.objects().front()->has_belt_brim(); }; // X is across the belt when the tilt is about X, since the shear then runs along Y. CHECK(multi_instance_has_brim(40., 0.)); // Y is along the belt: the copies sit at different belt heights and would each need // their own bands, so the brim is refused (and validate() warns). CHECK_FALSE(multi_instance_has_brim(0., 40.)); } TEST_CASE("Belt brim coexists with support material", "[SkirtBrim][belt]") { // Supports put extra layers into the same z stream as the apron bands, which is what // the three-way merge in collect_layers_to_print() exists to handle: a band sharing a // print_z with a support layer of the SAME object used to overwrite it in the // print-wide merge. A smoke test - it cannot prove the collision occurred - but it // does exercise the merge with all three streams populated. DynamicPrintConfig config = belt_brim_config(); config.set_deserialize_strict({ { "brim_type", "outer_only" }, { "brim_width", 4 }, { "leading_brim_length", 6 }, { "brim_object_gap", 0 }, { "enable_support", 1 }, }); const std::string gc = slice({ TestMesh::overhang }, config); REQUIRE(! gc.empty()); CHECK(role_passes(gc, "brim") > 0); }