Add belt-printer regression test for start-of-print gantry move

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.
This commit is contained in:
harrierpigeon
2026-06-27 17:50:49 -05:00
parent 04554abae6
commit 02e313a115

View File

@@ -15,6 +15,11 @@
#include <boost/filesystem.hpp>
#include "test_helpers.hpp"
#include <algorithm>
#include <limits>
#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<unsigned int> 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<double>::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
}
}
}
}