Add belt-printer regression test for prepare-stage move Z

Processes a minimal belt start sequence through GCodeProcessor::process_buffer
and asserts the move preceding the first extrusion keeps its real Z, so it can
no longer back-transform to model Y~=0 and produce the phantom extrusion line.

Belt printers are non-Bambu, so the processor uses the compatible reserved
tags ("TYPE:"); the test sets s_IsBBLPrinter=false (saved/restored via an RAII
guard) to mirror the real printer. Proven to fail without the fix (the
prepare-stage move's Z is pinned to the first-layer height, 0 here) and pass
with it.
This commit is contained in:
harrierpigeon
2026-06-29 02:59:07 -05:00
parent a83cd8aa29
commit 613dad92a1

View File

@@ -15,6 +15,8 @@
#include <boost/filesystem.hpp>
#include "test_helpers.hpp"
#include <cmath>
#include "libslic3r/GCode/GCodeProcessor.hpp"
#include <algorithm>
#include <limits>
#include "libslic3r/BeltGCodeWriter.hpp"
@@ -885,7 +887,8 @@ SCENARIO("Belt: the first travel does not lift through the uninitialised origin"
std::vector<unsigned int> extruder_ids { 0 };
writer.set_extruders(extruder_ids);
writer.set_extruder(0);
writer.config.travel_speed.value = 100.0;
// travel_speed became per-extruder (ConfigOptionFloatsNullable) upstream.
writer.config.travel_speed.values = { 100.0 };
writer.config.z_hop.values = { 0.4 };
writer.config.retract_lift_above.values = { 0.0 };
writer.config.retract_lift_below.values = { 0.0 };
@@ -920,3 +923,77 @@ SCENARIO("Belt: the first travel does not lift through the uninitialised origin"
}
}
}
// Regression test for the belt-printer "phantom extrusion line from Y=0" bug.
//
// GCodeProcessor::store_move_vertex pins a move's stored Z to the first-layer
// height while m_processing_start_custom_gcode is set (the start G-code "prepare"
// stage), because on a normal printer the toolhead Z there is not yet a real print
// height. On a belt printer that override is wrong: Z is written explicitly and the
// designed-view back-transform couples machine Z into the rendered model Y (the
// belt tilt mixes the height and belt-feed axes). Overriding it back-transforms the
// last prepare-stage move (the unretract right before the first extrusion) to
// model Y ~= 0, and libvgcode then draws a phantom extrusion segment from Y ~= 0 to
// the first real toolpath — rendered in the first extrusion role's color. The fix
// keeps the real Z for belt printers (gated on belt_tilt_angle). Here we assert the
// prepare-stage move keeps its real Z so it can no longer leak to Y ~= 0.
SCENARIO("Belt: start-gcode prepare-stage moves keep their real Z", "[GCode][belt]")
{
// Belt printers are non-Bambu, so the G-code uses the "compatible" reserved
// tags ("TYPE:" for the extrusion role). The processor selects the tag table
// from the static s_IsBBLPrinter flag, so mirror the belt-printer setting here
// (saved/restored so test ordering stays unaffected).
struct BBLPrinterGuard {
bool prev = GCodeProcessor::s_IsBBLPrinter;
BBLPrinterGuard() { GCodeProcessor::s_IsBBLPrinter = false; }
~BBLPrinterGuard() { GCodeProcessor::s_IsBBLPrinter = prev; }
} bbl_guard;
GIVEN("A belt G-code whose start sequence travels to a high machine Z before the first extrusion") {
// The leading "; belt_slice_rotation_angle = 45" header sets belt_tilt_angle
// (parsed before the body), enabling the belt code path. ;TYPE:Custom before
// any G1 turns on the prepare stage; ;TYPE:Outer wall turns it off, exactly
// as a sliced belt print is laid out.
const std::string gcode =
"; belt_slice_rotation_angle = 45\n"
"G90\n"
"G21\n"
"M83\n"
";TYPE:Custom\n"
"G1 E-1.5 F2100\n" // retract at the (0,0,0) origin
"G1 X45 Y0.3 Z50 F12000\n" // travel to the approach point (prepare stage)
"G1 E1.5 F1800\n" // unretract in place (prepare stage)
";TYPE:Outer wall\n"
"G1 X46 Y0.3 Z50 E0.05\n"; // first extrusion, same Z as the approach
GCodeProcessor processor;
processor.process_buffer(gcode);
const GCodeProcessorResult& result = processor.get_result();
THEN("the belt code path is active") {
REQUIRE_THAT(result.belt_tilt_angle, Catch::Matchers::WithinAbs(45.0, 1e-4));
}
WHEN("locating the first extrusion and the move that precedes it") {
size_t first_extrude = result.moves.size();
for (size_t i = 0; i < result.moves.size(); ++i)
if (result.moves[i].type == EMoveType::Extrude) { first_extrude = i; break; }
THEN("an extrusion and a preceding move exist") {
REQUIRE(first_extrude < result.moves.size());
REQUIRE(first_extrude > 0);
}
THEN("the preceding prepare-stage move shares the extrusion's real Z (no leak to Y=0)") {
const float extrude_z = result.moves[first_extrude].position.z();
const float prev_z = result.moves[first_extrude - 1].position.z();
// The first extrusion is at the real Z=50; before the fix the
// prepare-stage move's Z was pinned to the first-layer height
// (0 here) instead, which back-transforms to model Y ~= 0 and
// produces the phantom extrusion segment.
REQUIRE_THAT(extrude_z, Catch::Matchers::WithinAbs(50.0, 1e-3));
REQUIRE_THAT(prev_z, Catch::Matchers::WithinAbs(50.0, 1e-3));
}
}
}
}