From c59eb1bcf9d9fead326b3cf17e9c075c76b0ec56 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Thu, 8 Oct 2026 13:40:52 -0500 Subject: [PATCH] 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