From f64b49ab52f494258c30ca28ab46cdc71e53de95 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Wed, 7 Oct 2026 02:23:54 -0500 Subject: [PATCH] Belt printer: no layer changes that print nothing, and a preview that survives them Since the slicing frame of a belt object starts at the belt below its leading end, 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: the top slider layer held nearly the whole print, the slider jumped every other layer through the single-colour stretch before a second part on another filament, and with the belt purge tower the whole print greyed out while dragging. Drop the belt layers that print nothing (no object, support or brim content) in GCode::collect_layers_to_print, and renumber the layers consecutively over the moves that exist when converting a result for libvgcode, so a file with empty layers from any source still previews correctly. The layer slider labels a 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 ended; the slider assumes that list increases and 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 elsewhere. Regression test: two cubes 60 mm apart along the belt produce no layer without an extrusion and the header's layer count matches. Co-Authored-By: Claude Fable 5.1 --- src/libslic3r/GCode.cpp | 30 +++++++++ src/libslic3r/GCode/GCodeProcessor.cpp | 9 +++ src/slic3r/GUI/GCodeViewer.cpp | 22 +++++++ src/slic3r/GUI/GCodeViewer.hpp | 6 ++ src/slic3r/GUI/LibVGCode/LibVGCodeWrapper.cpp | 22 +++++-- tests/fff_print/test_print.cpp | 62 +++++++++++++++++++ 6 files changed, 147 insertions(+), 4 deletions(-) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 8ad30a611a..c96d299211 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -2440,6 +2440,36 @@ 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.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> &group) { + return std::none_of(group.second.begin(), group.second.end(), prints_something); + }), + layers_to_print.end()); + } + return layers_to_print; } diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp index deb776bf32..ac5a435bcd 100644 --- a/src/libslic3r/GCode/GCodeProcessor.cpp +++ b/src/libslic3r/GCode/GCodeProcessor.cpp @@ -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; diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index 949177d4ec..dba2546f0c 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -87,6 +87,7 @@ #include #include +#include #include #include #include @@ -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::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; diff --git a/src/slic3r/GUI/GCodeViewer.hpp b/src/slic3r/GUI/GCodeViewer.hpp index e5703939b2..c038aec671 100644 --- a/src/slic3r/GUI/GCodeViewer.hpp +++ b/src/slic3r/GUI/GCodeViewer.hpp @@ -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 m_belt_layer_zs; //BBS: save m_gcode_result as well const GCodeProcessorResult* m_gcode_result; std::array(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 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 zs = m_viewer.get_layers_zs(); std::vector ret; std::transform(zs.begin(), zs.end(), std::back_inserter(ret), [](float z) { return static_cast(z); }); diff --git a/src/slic3r/GUI/LibVGCode/LibVGCodeWrapper.cpp b/src/slic3r/GUI/LibVGCode/LibVGCodeWrapper.cpp index d6f9650972..ea010a41b5 100644 --- a/src/slic3r/GUI/LibVGCode/LibVGCodeWrapper.cpp +++ b/src/slic3r/GUI/LibVGCode/LibVGCodeWrapper.cpp @@ -17,6 +17,8 @@ #include "libslic3r/libslic3r.h" #include "LibVGCodeWrapper.hpp" + +#include #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& 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::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::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(curr.gcode_id), static_cast(curr.layer_id), + static_cast(curr.gcode_id), layer_id, static_cast(curr.extruder_id), static_cast(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(curr.gcode_id), static_cast(curr.layer_id), + static_cast(curr.gcode_id), layer_id, static_cast(curr.extruder_id), static_cast(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(curr.gcode_id), static_cast(curr.layer_id), + convert(curr.extrusion_role), curr_type, static_cast(curr.gcode_id), layer_id, static_cast(curr.extruder_id), static_cast(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(curr.gcode_id), static_cast(curr.layer_id), + static_cast(curr.gcode_id), layer_id, static_cast(curr.extruder_id), static_cast(curr.cp_color_id), curr.time, /* ORCA: Add Pressure Advance visualization support */ 0.0f, curr.pressure_advance, /* ORCA: Add Acceleration visualization support */ curr.acceleration, diff --git a/tests/fff_print/test_print.cpp b/tests/fff_print/test_print.cpp index 7185b1eae5..0f1d33c67f 100644 --- a/tests/fff_print/test_print.cpp +++ b/tests/fff_print/test_print.cpp @@ -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