Belt printer: address the 2026-10-08 review of #14394 (#16292)

## Description

Addresses the three items in @raistlin7447's review of 2026-10-08 on
#14394
(https://github.com/OrcaSlicer/OrcaSlicer/pull/14394#pullrequestreview-5459955028),
one commit each.

**Leading-edge brim with a leading overhang (BeltBrim.cpp).** The
leading-edge cut was taken at the first layer with geometry. 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, so the
cut lay ahead of the part. Reproduced on the BabyBelt profile with a 20
mm cube and a fin over its leading end, leading brim length 10 mm, width
5 mm:

| Part | Leading-edge brim before | After |
|---|---|---|
| Plain cube | 53 brim lines | 53 (unchanged) |
| Cube + 30 mm fin | 9, a sliver well ahead of the part | 53 |
| Cube + 40 mm fin | none | 53 |
| Cube + 30 mm fin, leading length 0 | none | 18 |

As suggested, the cut now uses the first layer whose contact band in the
footprint loop is non-empty: the loop records it while it builds the
footprint.

**Dead empty-layer drop (GCode.cpp).** The by-layer
`collect_layers_to_print()` built its groups only from the per-object
entries, and the per-object overload already drops every belt entry that
prints nothing, so the group-level drop could never remove anything.
Removed; its explanation moved to the drop that does the work. No output
change.

**First-layer point test comment (GCode.cpp).** Reworded to say what the
lambda undoes (what `point_to_gcode()` added and the writer took off),
since on a belt `m_origin` is rotated by `on_set_origin()` and is not
"the instance part". Comment only.

## Tests

- New test "Leading-edge-only brim ignores an overhang ahead of the
part" (30 and 40 mm fins): the leading-edge brim of the cube with the
fin must match the plain cube's. Fails without the fix (plain 53 brim
layers vs 10 and 0 with the fins), passes with it (53 and 53).
- `fff_print_tests` 364 cases and `libslic3r_tests` pass.
- Before/after G-code on the current `belt-printer` head (baseline built
from it, both binaries run from the build tree with the same resources):
byte-identical for a multi-color belt project, a two-filament belt
project with the belt purge tower, two cubes printed by object, a
flat-bed organic-support project, and two plain-cube leading-edge brims.
Only the three overhang cases change, as in the table.
- `OrcaSlicer_profile_validator -s` on Printcepts, IdeaFormer, Custom
(belt) and Prusa (control): clean.
- `scripts/clang_tidy_diff.py` against `belt-printer`: clean.

Unrelated, noticed while comparing outputs: `PrintObject::m_id`
(Print.hpp) has no initializer, so on the CLI path the `; printing
object ... id:` labels can carry an arbitrary value that differs between
builds. Pre-existing; not touched here.

OS: Linux (Ubuntu), GCC, local build. Written with AI assistance (Claude
Code); every change reviewed and tested locally as listed.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
This commit is contained in:
Joseph Robertson
2026-10-08 16:15:36 -05:00
committed by GitHub
3 changed files with 64 additions and 44 deletions
+13 -12
View File
@@ -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;
+9 -29
View File
@@ -2370,8 +2370,13 @@ std::vector<GCode::LayerToPrint> 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 &ltp) {
@@ -2446,31 +2451,6 @@ std::vector<std::pair<coordf_t, std::vector<GCode::LayerToPrint>>> 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 &ltp) {
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<coordf_t, std::vector<LayerToPrint>> &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<double>();
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(),
+42 -3
View File
@@ -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