diff --git a/src/libslic3r/BeltBrim.cpp b/src/libslic3r/BeltBrim.cpp index 920790891c..d51f1248de 100644 --- a/src/libslic3r/BeltBrim.cpp +++ b/src/libslic3r/BeltBrim.cpp @@ -443,6 +443,8 @@ void make_belt_brim(PrintObject &object) // own contact band. This is the object's bottom face, which on a belt is // spread over every layer instead of sitting in layer 0. ExPolygons footprint_acc; + // The first layer that touches the belt: where the leading-edge brim is cut. + const Layer *first_contact = nullptr; for (size_t i = 0; i < nlayers; ++ i) { const Layer &layer = *object.layers()[i]; if (layer.lslices.empty()) @@ -458,7 +460,12 @@ void make_belt_brim(PrintObject &object) const Polygon band = band_box(bb, bc.frame.from_axis, u_lo, u_hi); if (band.empty()) continue; - expolygons_append(footprint_acc, intersection_ex(layer.lslices, Polygons{ band })); + ExPolygons contact = intersection_ex(layer.lslices, Polygons{ band }); + if (contact.empty()) + continue; + if (first_contact == nullptr) + first_contact = &layer; + expolygons_append(footprint_acc, std::move(contact)); } const ExPolygons footprint = union_ex(footprint_acc); if (footprint.empty()) @@ -488,19 +495,13 @@ void make_belt_brim(PrintObject &object) width, gap, leading, lateral, bc.frame), bc.frame); - if (bt == btLeadingEdgeOnly && ! bc.region.empty()) { + if (bt == btLeadingEdgeOnly && first_contact != nullptr && ! bc.region.empty()) // The cut is the uphill edge of the first contact's band: everything past it - // belongs to later contacts. The first contact is the first layer with - // geometry, not layers().front(): the slicing frame starts at the belt below - // the footprint, so the leading layers are empty and their contact lies ahead - // of the part. - const Layer *first_contact = nullptr; - for (const Layer *layer : object.layers()) - if (! layer->lslices.empty()) { first_contact = layer; break; } - if (first_contact == nullptr) - return; + // belongs to later contacts. The first contact is the first layer that touches + // the belt (step 1), neither layers().front(), an empty lead-in layer, nor the + // first layer with geometry, which is an overhang's tip when the part overhangs + // its leading end: both lie ahead of the part. bc.region = belt_brim_clip_leading_edge(bc.region, bc.frame, bc.ctx.cutoff_u(first_contact->print_z)); - } if (bc.region.empty()) return; diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 453317ef2c..d29363e639 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -2370,8 +2370,13 @@ std::vector GCode::collect_layers_to_print(const PrintObjec PrintStateBase::WarningLevel::CRITICAL, warning, PrintStateBase::SlicingEmptyGcodeLayers); } - // Belt printers: drop the layers that print nothing (see the by-layer overload), so - // the by-object export writes the same layer changes as the by-layer one. + // Belt printers: drop the layers that print nothing at all. An object's slicing + // frame starts at the belt below its leading end, so its first layers are empty, + // and with several objects along the belt those empty layers fall between other + // objects' printing layers. A layer change with no moves is noise in the file, and + // the preview (libvgcode) numbers its layers from the moves it sees, so a gap folds + // every later layer into the one before it. Both print sequences collect their + // layers here, so neither writes such a layer. if (object.print()->config().belt_printer.value) layers_to_print.erase( std::remove_if(layers_to_print.begin(), layers_to_print.end(), [&object](const LayerToPrint <p) { @@ -2446,31 +2451,6 @@ std::vector>> GCode::collec layers_to_print.emplace_back(std::move(merged)); } - // Belt printers: drop the layers that print nothing at all. An object's - // slicing frame starts at the belt below its leading end, so its first layers - // are empty, and with several objects along the belt those empty layers fall - // between other objects' printing layers. A layer change with no moves is - // noise in the file, and the preview (libvgcode) numbers its layers from the - // moves it sees, so a gap folds every later layer into the one before it. - if (print.config().belt_printer.value) { - auto prints_something = [](const LayerToPrint <p) { - if (ltp.object_layer != nullptr && ltp.original_object != nullptr && - belt_object_layer_prints_something(*ltp.original_object, *ltp.object_layer)) - return true; - if (ltp.support_layer != nullptr && ltp.support_layer->has_extrusions()) - return true; - if (ltp.belt_brim_band != nullptr && ! ltp.belt_brim_band->fills.empty()) - return true; - return false; - }; - layers_to_print.erase( - std::remove_if(layers_to_print.begin(), layers_to_print.end(), - [&prints_something](const std::pair> &group) { - return std::none_of(group.second.begin(), group.second.end(), prints_something); - }), - layers_to_print.end()); - } - return layers_to_print; } @@ -3193,8 +3173,8 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato if (print.config().belt_printer.value) { m_writer.set_first_layer_point_test([this](const Vec3d &point_logical) { const Vec2d extruder_offset = m_writer.filament() != nullptr ? EXTRUDER_CONFIG(extruder_offset) : Vec2d::Zero(); - // The writer hands over the point with the plate origin (its XY offset) already - // taken off, while m_origin still carries it: take off the instance part only. + // Undo what point_to_gcode() added (m_origin, minus the extruder offset) and + // what the writer then took off (its XY offset, the plate origin). const Vec2d plate_offset = m_writer.get_xy_offset().cast(); return this->on_first_layer(Vec3d(point_logical.x() - (m_origin.x() - plate_offset.x()) + extruder_offset.x(), point_logical.y() - (m_origin.y() - plate_offset.y()) + extruder_offset.y(), diff --git a/tests/fff_print/test_skirt_brim.cpp b/tests/fff_print/test_skirt_brim.cpp index 639c36d6c7..c51dd5ebc6 100644 --- a/tests/fff_print/test_skirt_brim.cpp +++ b/tests/fff_print/test_skirt_brim.cpp @@ -1158,9 +1158,10 @@ TEST_CASE("Every brim type slices on a belt printer", "[SkirtBrim][belt]") } // The leading-edge-only brim is the outer brim cut down to the part's first contact -// with the belt. The cut has to be taken at the first layer with geometry: the slicing -// frame starts at the belt below the footprint, so layers().front() is an empty lead-in -// layer whose contact lies ahead of the part, and a cut taken there left no brim at all. +// with the belt. The cut has to be taken at the first layer that touches the belt: the +// slicing frame starts at the belt below the footprint, so layers().front() is an empty +// lead-in layer whose contact lies ahead of the part, and a cut taken there left no brim +// at all. TEST_CASE("Leading-edge-only brim is laid at the first contact and nowhere else", "[SkirtBrim][belt][Regression]") { auto brim_gcode = [](const char *brim_type) { @@ -1191,6 +1192,44 @@ TEST_CASE("Leading-edge-only brim is laid at the first contact and nowhere else" CHECK(leading_layers < outer_layers); } +// An overhang on the leading side is sliced before the part reaches the belt, so the +// first layer with geometry is the overhang's tip, above the belt. A leading-edge cut +// taken there lies ahead of the part: the brim shrank to a sliver well ahead of it, or +// vanished once the overhang reached further forward than the brim. The overhang does +// not touch the belt, so it must not change the brim at all. +TEST_CASE("Leading-edge-only brim ignores an overhang ahead of the part", "[SkirtBrim][belt][Regression]") +{ + const double fin_length = GENERATE(30., 40.); + CAPTURE(fin_length); + // A 20 mm cube, with or without a 2 mm thick fin leaving its top edge and reaching + // `fin` toward -Y, the end of the part that prints first. The fin overlaps the cube + // by 1 mm so the two shells merge instead of sharing a face. + auto brim_layers = [](double fin) { + indexed_triangle_set its = its_make_cube(20., 20., 20.); + if (fin > 0.) { + indexed_triangle_set fin_its = its_make_cube(20., fin + 1., 2.); + its_translate(fin_its, Vec3f(0.f, float(-fin), 18.f)); + its_merge(its, fin_its); + } + DynamicPrintConfig config = belt_brim_config(); + config.set_deserialize_strict({ + { "brim_type", "leading_edge_only" }, + { "brim_width", 5 }, + { "leading_brim_length", 10 }, + { "extra_brim_width", 0 }, + { "brim_object_gap", 0 }, + }); + return role_layers(slice({ TriangleMesh(std::move(its)) }, config), "brim"); + }; + const int plain = brim_layers(0.); + const int with_fin = brim_layers(fin_length); + INFO("leading-edge brim layers: plain cube " << plain << ", with the fin " << with_fin); + REQUIRE(plain > 0); + // One layer of slack: the fin widens the part's footprint on the plate, which can + // move the layer grid by a fraction of a layer. + CHECK(std::abs(with_fin - plain) <= 1); +} + 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