From c59eb1bcf9d9fead326b3cf17e9c075c76b0ec56 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Thu, 8 Oct 2026 13:40:52 -0500 Subject: [PATCH 1/3] Belt brim: cut the leading-edge brim at the first layer on the belt The leading-edge-only brim is the outer brim cut down to the part's first contact with the belt. The cut was taken at the first layer with geometry, but with an overhang on the leading side that layer is the overhang's tip, which is sliced before the part reaches the belt and does not touch it. The cut then lay ahead of the part: a 30 mm fin left a sliver of brim well ahead of a 20 mm cube, and a 40 mm fin, or a leading brim length of 0, left none at all. The footprint loop already finds the layers that touch the belt (their contact band is not empty); record the first of them and cut there. The new test slices a cube with and without a 30 or 40 mm fin over its leading end and checks that the leading-edge brim is the same. It fails without the fix. Reported by raistlin7447 in the review of #14394. Co-Authored-By: Claude Opus 5.5 --- src/libslic3r/BeltBrim.cpp | 25 ++++++++-------- tests/fff_print/test_skirt_brim.cpp | 45 +++++++++++++++++++++++++++-- 2 files changed, 55 insertions(+), 15 deletions(-) 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/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 From d29e3be0a0c3f0755f781e3db5433fe71a0c61d5 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Thu, 8 Oct 2026 13:40:52 -0500 Subject: [PATCH 2/3] Belt G-code: drop the empty layers in one place The by-layer collect_layers_to_print() dropped every merged layer group in which nothing prints, and the per-object overload drops every belt entry that prints nothing. The merged groups are built only from the per-object entries, so after the second drop every group holds at least one entry that prints and the first never removed anything. Remove it and keep its explanation at the drop that does the work. No output changes. Reported by raistlin7447 in the review of #14394. Co-Authored-By: Claude Opus 5.5 --- src/libslic3r/GCode.cpp | 34 +++++++--------------------------- 1 file changed, 7 insertions(+), 27 deletions(-) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 453317ef2c..c2db414fc5 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; } From 679638fca37f42fbbedd349976254c2448a501bd Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Thu, 8 Oct 2026 13:40:52 -0500 Subject: [PATCH 3/3] Belt G-code: correct the first-layer point test comment The comment said the lambda takes off "the instance part" of m_origin. On a belt m_origin has been rotated by on_set_origin() by then, so m_origin minus the plate offset is not the instance shift. Say what the code does: undo what point_to_gcode() added and what the writer took off. Comment only. Reported by raistlin7447 in the review of #14394. Co-Authored-By: Claude Opus 5.5 --- src/libslic3r/GCode.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index c2db414fc5..d29363e639 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -3173,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(),