Belt printer: no layer changes that print nothing, and a preview that survives them (#16245)

Fixes the preview layer bar on belt prints with several parts along the
belt (reported with a cube on filament 1 and a 3DBenchy on filament 2,
no purge tower): the top slider layer held nearly the whole print, the
slider jumped every other layer through the single-colour stretch before
the second part, and with the belt purge tower the whole print greyed
out while dragging.

## Cause

Since #16236 the slicing frame of a belt object starts at the belt below
its leading end, so its first layers are empty. On a single part they
carry the brim bands. With several parts along the belt the later parts'
empty layers fall between the earlier parts' printing layers and were
written to the G-code as layer changes with no moves at all. The preview
numbers its layers (`libvgcode::Layers`) from the vertices it is given
and expects consecutive ids, so at the first such gap it stopped
creating layers and folded everything after it into the last one.

## Fix

- `GCode::collect_layers_to_print` drops the belt layers that print
nothing (no object, support or brim content): no layer change without
moves in the file.
- `libvgcode::convert` renumbers the layers consecutively over the moves
that exist, so a file with empty layers from any source still previews
correctly.
- The layer slider labels each belt layer with its print Z (the slicer's
layer Z, which increases along the belt) instead of libvgcode's toolpath
height, which on a tilted layer is wherever its last extrusion happened
to end; the slider assumes the list increases, so the labels showed "0 /
max" on alternate layers. The processor reads that print Z from the
`;Z:` tag non-BBL printers write (it only knew `; Z_HEIGHT:`), on belt
printers only, so nothing changes for other printers.

## Verification

- New regression test *Belt G-code has no layer that prints nothing*
(two cubes 60 mm apart along the belt): fails on the previous code with
one empty layer, passes now.
- `fff_print_tests` 356 passed, `libslic3r_tests` 1116 passed;
`scripts/clang_tidy_diff.py --base upstream/belt-printer`: no findings.
- The reported project sliced through the CLI: 595 layers, none without
an extrusion, Z strictly increasing.
- Scripted GUI pass on the reported project with and without the purge
tower: the slider has one entry per G-code layer, each step shows a thin
tilted strip advancing along the belt, the top layer alone is a thin
strip, nothing greys out while dragging, the slider opens at the top
after slicing, and every label reads the layer number and the print Z
matching the G-code's `;Z:` (checked at the top, mid-print and through
the two-part stretch); raw-view toggle and slider retention unchanged.

Left as is: the lower handle at the bottom still reads `1 / 0.00` rather
than the first layer's Z (index correct); pre-existing.
This commit is contained in:
Joseph Robertson
2026-10-07 12:01:20 -05:00
committed by GitHub
6 changed files with 147 additions and 4 deletions
+30
View File
@@ -2440,6 +2440,36 @@ 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.object_layer->has_extrusions())
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;
if (ltp.object_layer != nullptr && ltp.original_object != nullptr && ltp.original_object->has_belt_brim()) {
const auto &by_layer = ltp.original_object->belt_brim_by_layer();
const size_t id = ltp.object_layer->id();
if (id < by_layer.size() && ! by_layer[id].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;
}
+9
View File
@@ -4305,6 +4305,15 @@ void GCodeProcessor::process_tags(const std::string_view comment, bool producers
return;
}
// ;Z: -- the layer Z tag non-BBL printers write. Only read on a belt printer,
// where the preview labels its layers with it (GCodeViewer::load_as_gcode);
// elsewhere print_z stays unset, as it always was, so nothing downstream of
// it changes for other printers.
if (m_belt_printer && boost::starts_with(comment, "Z:")) {
m_print_z = get_z_height(comment);
return;
}
if (boost::starts_with(comment, " CONFIG_BLOCK_START")) {
m_in_config_block = true;
return;
+22
View File
@@ -87,6 +87,7 @@
#include <array>
#include <algorithm>
#include <limits>
#include <cmath>
#include <chrono>
#include <Eigen/Geometry>
@@ -1635,6 +1636,26 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const
m_viewer.reset_default_extrusion_roles_colors();
m_viewer.load(std::move(data));
// Belt printers: libvgcode labels a layer with the height of its toolpaths, which
// on a tilted layer is wherever its last extrusion happened to end, and the layer
// slider looks its labels and the colour-change ticks up in that list assuming it
// increases. Give it the layers' print Z instead (the slicer's layer Z, which
// increases along the belt), numbered the way libvgcode::convert() numbers the
// layers: consecutively over the moves that exist.
m_belt_layer_zs.clear();
if (is_belt) {
unsigned int src_layer_id = std::numeric_limits<unsigned int>::max();
for (size_t i = 1; i < gcode_result.moves.size(); ++ i) {
const GCodeProcessorResult::MoveVertex &mv = gcode_result.moves[i];
if (mv.layer_id != src_layer_id) {
src_layer_id = mv.layer_id;
m_belt_layer_zs.emplace_back(double(mv.print_z));
}
}
if (m_belt_layer_zs.size() != m_viewer.get_layers_count())
m_belt_layer_zs.clear();
}
// #if !VGCODE_ENABLE_COG_AND_TOOL_MARKERS
// const size_t vertices_count = m_viewer.get_vertices_count();
// m_cog.reset();
@@ -1905,6 +1926,7 @@ void GCodeViewer::reset()
//BBS: should also reset the result id
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": current result id %1% ")%m_last_result_id;
m_last_result_id = -1;
m_belt_layer_zs.clear();
//BBS: add only gcode mode
m_only_gcode_in_preview = false;
+6
View File
@@ -201,6 +201,8 @@ private:
unsigned int m_last_result_id{ 0 };
// Belt printers: the view the loaded result was converted for (see load_as_gcode).
bool m_last_belt_show_designed{ true };
// Belt printers: the print Z of each viewer layer, in the viewer's layer numbering.
std::vector<double> m_belt_layer_zs;
//BBS: save m_gcode_result as well
const GCodeProcessorResult* m_gcode_result;
std::array<unsigned int, static_cast<size_t>(EMoveType::Count)> m_move_type_counts{};
@@ -329,6 +331,10 @@ public:
const BoundingBoxf3& get_max_bounding_box() const { return m_max_bounding_box; }
const BoundingBoxf3& get_shell_bounding_box() const { return m_shell_bounding_box; }
std::vector<double> get_layers_zs() const {
// Belt printers: the layer Z the slider labels and the colour-change ticks
// use is the layer's print Z (see load_as_gcode), not a toolpath height.
if (! m_belt_layer_zs.empty())
return m_belt_layer_zs;
const std::vector<float> zs = m_viewer.get_layers_zs();
std::vector<double> ret;
std::transform(zs.begin(), zs.end(), std::back_inserter(ret), [](float z) { return static_cast<double>(z); });
+18 -4
View File
@@ -17,6 +17,8 @@
#include "libslic3r/libslic3r.h"
#include "LibVGCodeWrapper.hpp"
#include <limits>
#include "libslic3r/Print.hpp"
#include "libslic3r/Color.hpp"
#include "libslic3r/CustomGCode.hpp"
@@ -230,10 +232,22 @@ GCodeInputData convert(const Slic3r::GCodeProcessorResult& result, const std::ve
const std::vector<Slic3r::GCodeProcessorResult::MoveVertex>& moves = result.moves;
ret.vertices.reserve(2 * moves.size());
// libvgcode numbers its layers from the vertices it is given and expects them to
// arrive one layer after the other with consecutive ids: a layer without any move
// (a belt file can carry layer changes that print nothing) would leave a gap, and
// every later vertex would then be folded into the last layer created. Renumber
// the ids consecutively over the moves that exist.
uint32_t src_layer_id = std::numeric_limits<uint32_t>::max();
uint32_t layer_id = 0;
for (size_t i = 1; i < moves.size(); ++i) {
const Slic3r::GCodeProcessorResult::MoveVertex& curr = moves[i];
const Slic3r::GCodeProcessorResult::MoveVertex& prev = moves[i - 1];
const EMoveType curr_type = convert(curr.type);
if (curr.layer_id != src_layer_id) {
if (src_layer_id != std::numeric_limits<uint32_t>::max())
++ layer_id;
src_layer_id = curr.layer_id;
}
const EOptionType option_type = move_type_to_option(curr_type);
if (option_type == EOptionType::COUNT || option_type == EOptionType::Travels || option_type == EOptionType::Wipes) {
if (ret.vertices.empty() || prev.type != curr.type || prev.extrusion_role != curr.extrusion_role
@@ -245,7 +259,7 @@ GCodeInputData convert(const Slic3r::GCodeProcessorResult& result, const std::ve
#if VGCODE_ENABLE_COG_AND_TOOL_MARKERS
const libvgcode::PathVertex vertex = { xform_pos(prev.position), curr.height, curr.width, curr.feedrate, prev.actual_feedrate,
curr.mm3_per_mm, curr.fan_speed, curr.temperature, 0.0f, convert(curr.extrusion_role), curr_type,
static_cast<uint32_t>(curr.gcode_id), static_cast<uint32_t>(curr.layer_id),
static_cast<uint32_t>(curr.gcode_id), layer_id,
static_cast<uint8_t>(curr.extruder_id), static_cast<uint8_t>(curr.cp_color_id), { 0.0f, 0.0f },
/* ORCA: Add Pressure Advance visualization support */ 0.0f, curr.pressure_advance,
/* ORCA: Add Acceleration visualization support */ curr.acceleration,
@@ -253,7 +267,7 @@ GCodeInputData convert(const Slic3r::GCodeProcessorResult& result, const std::ve
#else
const libvgcode::PathVertex vertex = { xform_pos(prev.position), curr.height, curr.width, curr.feedrate, prev.actual_feedrate,
curr.mm3_per_mm, curr.fan_speed, curr.temperature, convert(curr.extrusion_role), curr_type,
static_cast<uint32_t>(curr.gcode_id), static_cast<uint32_t>(curr.layer_id),
static_cast<uint32_t>(curr.gcode_id), layer_id,
static_cast<uint8_t>(curr.extruder_id), static_cast<uint8_t>(curr.cp_color_id), { 0.0f, 0.0f },
/* ORCA: Add Pressure Advance visualization support */ 0.0f, curr.pressure_advance,
/* ORCA: Add Acceleration visualization support */ curr.acceleration,
@@ -267,7 +281,7 @@ GCodeInputData convert(const Slic3r::GCodeProcessorResult& result, const std::ve
const libvgcode::PathVertex vertex = { xform_pos(curr.position), curr.height, curr.width, curr.feedrate, curr.actual_feedrate,
curr.mm3_per_mm, curr.fan_speed, curr.temperature,
result.filament_densities[curr.extruder_id] * curr.mm3_per_mm * (curr.position - prev.position).norm(),
convert(curr.extrusion_role), curr_type, static_cast<uint32_t>(curr.gcode_id), static_cast<uint32_t>(curr.layer_id),
convert(curr.extrusion_role), curr_type, static_cast<uint32_t>(curr.gcode_id), layer_id,
static_cast<uint8_t>(curr.extruder_id), static_cast<uint8_t>(curr.cp_color_id), curr.time,
/* ORCA: Add Pressure Advance visualization support */ 0.0f, curr.pressure_advance,
/* ORCA: Add Acceleration visualization support */ curr.acceleration,
@@ -275,7 +289,7 @@ GCodeInputData convert(const Slic3r::GCodeProcessorResult& result, const std::ve
#else
const libvgcode::PathVertex vertex = { xform_pos(curr.position), curr.height, curr.width, curr.feedrate, curr.actual_feedrate,
curr.mm3_per_mm, curr.fan_speed, curr.temperature, convert(curr.extrusion_role), curr_type,
static_cast<uint32_t>(curr.gcode_id), static_cast<uint32_t>(curr.layer_id),
static_cast<uint32_t>(curr.gcode_id), layer_id,
static_cast<uint8_t>(curr.extruder_id), static_cast<uint8_t>(curr.cp_color_id), curr.time,
/* ORCA: Add Pressure Advance visualization support */ 0.0f, curr.pressure_advance,
/* ORCA: Add Acceleration visualization support */ curr.acceleration,
+62
View File
@@ -1244,6 +1244,68 @@ TEST_CASE("Organic tree supports place a support blocker at its own height above
CHECK(collides(last + num_raft));
}
// Two parts along the belt: the second part's slicing frame starts at the belt
// below its leading end, so its first layers are empty and interleave with the
// first part's printing layers. Those must not reach the G-code as layer changes
// that print nothing: the preview numbers its layers from the moves it sees, and
// a gap folded every later layer into the one before it.
TEST_CASE("Belt G-code has no layer that prints nothing", "[Print][belt][GCode][Regression]")
{
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
config.set_deserialize_strict({
{ "belt_printer", 1 },
{ "belt_slice_rotation", "x" },
{ "belt_slice_rotation_angle", 45 },
{ "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 },
{ "z_hop", 0 },
{ "brim_type", "outer_only" },
{ "brim_width", 4 },
{ "machine_start_gcode", "T[initial_tool]\n" },
{ "layer_change_gcode", "G92 E0\n" },
});
Print print;
Model model;
TriangleMesh cube_a(its_make_cube(20., 20., 20.));
TriangleMesh cube_b(its_make_cube(20., 20., 20.));
init_print({ cube_a, cube_b }, print, model, config);
// 60 mm apart along the belt: the second cube's lead-in layers fall among the
// first cube's layers.
model.objects[0]->instances.front()->set_offset(Vec3d(50., 40., 0.));
model.objects[1]->instances.front()->set_offset(Vec3d(50., 100., 0.));
print.apply(model, config);
print.set_status_silent();
const std::string gc = gcode(print);
REQUIRE(! gc.empty());
size_t layers = 0, empty = 0, total_header = 0;
bool extruded = true; // before the first layer change
std::istringstream in(gc);
std::string line;
auto close_layer = [&]() { if (! extruded) ++ empty; };
while (std::getline(in, line)) {
if (line.rfind(";LAYER_CHANGE", 0) == 0) {
close_layer();
++ layers;
extruded = false;
} else if (line.rfind("; total layer number: ", 0) == 0) {
total_header = size_t(std::atoi(line.c_str() + 22));
} else if (! extruded && line.rfind("G1 ", 0) == 0 && line.find('E') != std::string::npos
&& (line.find('X') != std::string::npos || line.find('Y') != std::string::npos)) {
extruded = true;
}
}
close_layer();
INFO("layers " << layers << ", header " << total_header << ", layers without extrusion " << empty);
CHECK(layers > 150); // both cubes, 141 layers each, overlapping along the belt
CHECK(empty == 0);
CHECK(total_header == layers);
}
// A part with an overhang on its LEADING side (the end that prints first) needs
// supports below the object's own lowest slicing layer: the belt under that overhang
// is reached before the object's first contact with it, so the support layers sit at