From 02e313a115f3ad18cc58e4b76e3b0d7f0077d443 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Sat, 27 Jun 2026 17:50:49 -0500 Subject: [PATCH] Add belt-printer regression test for start-of-print gantry move MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Locks in the fix from the previous commit. A fresh BeltGCodeWriter has an unestablished planar position (is_current_position_clear() == false) and its m_pos.xy is the origin (0,0). With a pending NormalLift z-hop, travel_to_xyz used to lift in place via _travel_to_z(), which in belt mode shears the origin into a machine Y ~= the layer Z — a move far up the gantry. The test configures an X-tilt 45 deg belt transform, defers a z-hop via lazy_lift, travels to a near-belt first point (transformed gantry Y ~= 1mm), and asserts no emitted move has Y anywhere near the layer Z. Verified to fail without the fix (max emitted Y = 100.0 vs the destination's ~1.0) and pass with it. --- tests/fff_print/test_gcodewriter.cpp | 73 ++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/tests/fff_print/test_gcodewriter.cpp b/tests/fff_print/test_gcodewriter.cpp index b09b38e794..b2a11becbc 100644 --- a/tests/fff_print/test_gcodewriter.cpp +++ b/tests/fff_print/test_gcodewriter.cpp @@ -15,6 +15,11 @@ #include #include "test_helpers.hpp" +#include +#include +#include "libslic3r/BeltGCodeWriter.hpp" +#include "libslic3r/GCodeReader.hpp" +#include "libslic3r/PrintConfig.hpp" using namespace Slic3r; using namespace Slic3r::Test; @@ -847,3 +852,71 @@ TEST_CASE("Custom G-code motion limits are restored before generated moves", "[G REQUIRE(gcode.find("M204 S6000 ; adjust acceleration", custom_gcode_pos) != std::string::npos); REQUIRE(gcode.find("M205 X8 Y8 ; adjust jerk", custom_gcode_pos) != std::string::npos); } + +// Regression test for the belt-printer "illegal gantry move at print start" bug. +// +// On a belt printer the layer-change z-hop is deferred (lazy_lift) and consumed +// by the first travel_to_xyz, whose NormalLift branch lifts in place via +// _travel_to_z(). On a normal printer _travel_to_z emits a Z-only move, but in +// belt mode Z is coupled to Y/X, so _travel_to_z re-emits the current m_pos +// through the belt shear. At print start (and after custom gcode) +// is_current_position_clear() is false and m_pos.xy is still the uninitialised +// origin (0,0), which shears into machine (X=bed_max, Y=layer_z) — a move far up +// the gantry, e.g. "G1 X95 Y168.19 Z237.857". The fix guards that lift on +// is_current_position_clear(), mirroring the SlopeLift branch. +SCENARIO("Belt: the first travel does not lift through the uninitialised origin", "[GCodeWriter][belt]") +{ + GIVEN("A fresh BeltGCodeWriter configured for an X-tilt 45 degree belt") { + // Machine-frame + slicer->world back-transform config (X tilt, 45 deg). + PrintConfig belt_config; + belt_config.belt_printer.value = true; + belt_config.gcode_back_transform.value = true; + belt_config.belt_slice_rotation.value = BeltRotationAxis::X; + belt_config.belt_slice_rotation_angle.value = 45.0; + belt_config.belt_slice_rotation_global.value = true; + belt_config.belt_preslice_global.value = true; + belt_config.belt_frame_tilt_decouple.value = false; + belt_config.belt_frame_tilt_angle.value = 45.0; + + BeltGCodeWriter writer; + writer.set_machine_frame_transform(belt_config); + writer.set_belt_back_transform(belt_config); + + std::vector extruder_ids { 0 }; + writer.set_extruders(extruder_ids); + writer.set_extruder(0); + writer.config.travel_speed.value = 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 }; + + // A fresh writer has not established its planar position yet — this is the + // precondition that made the origin leak into the first move. + REQUIRE_FALSE(writer.is_current_position_clear()); + + WHEN("a layer-change z-hop is pending and we travel to the first object point") { + // Defer a z-hop, exactly as a retract on layer change leaves it. + writer.lazy_lift(LiftType::NormalLift); + + // First object point in slicing coordinates: a near-belt point (y ~= -z) + // so its transformed gantry Y is small (~1mm). The bogus origin lift, in + // contrast, would shear to machine Y ~= nominal_z. + const double nominal_z = 100.0; + std::string gcode = writer.travel_to_xyz(Vec3d(10.0, -(nominal_z - 1.0), nominal_z)); + + THEN("no emitted move flies up the gantry; machine Y stays near the part") { + double max_y = std::numeric_limits::lowest(); + GCodeReader reader; + reader.parse_buffer(gcode, [&max_y](GCodeReader &, const GCodeReader::GCodeLine &line) { + if (line.cmd_is("G1") && line.has(Y)) + max_y = std::max(max_y, double(line.y())); + }); + // The destination shears to machine Y ~= 1mm. The old origin-lift bug + // produced a separate move at machine Y ~= nominal_z (100mm), so any + // Y well above the part means the origin leaked into a move. + REQUIRE(max_y > 0.0); // the destination move was emitted and parsed + REQUIRE(max_y < 10.0); // ... and nothing flew up the gantry + } + } + } +}