2024-03-27 14:38:03 +08:00
/**
* Ported from t / retraction . t
*/
2025-03-22 09:44:19 +08:00
# include <catch2/catch_test_macros.hpp>
# include <catch2/catch_approx.hpp>
2024-03-27 14:38:03 +08:00
2025-03-22 09:44:19 +08:00
# include "libslic3r/GCodeReader.hpp"
# include "libslic3r/GCode/GCodeWriter.hpp"
# include "libslic3r/Config.hpp"
2024-03-27 14:38:03 +08:00
# include "test_data.hpp"
# include <regex>
# include <fstream>
using namespace Slic3r ;
using namespace Test ;
2025-03-22 09:44:19 +08:00
using namespace Catch ;
2024-03-27 14:38:03 +08:00
constexpr bool debug_files { false } ;
void check_gcode ( std : : initializer_list < TestMesh > meshes , const DynamicPrintConfig & config , const unsigned duplicate ) {
constexpr std : : size_t tools_count = 4 ;
std : : size_t tool = 0 ;
std : : array < unsigned , tools_count > toolchange_count { 0 } ; // Track first usages so that we don't expect retract_length_toolchange when extruders are used for the first time
std : : array < bool , tools_count > retracted { false } ;
std : : array < double , tools_count > retracted_length { 0 } ;
bool lifted = false ;
double lift_dist = 0 ; // Track lifted distance for toolchanges and extruders with different retract_lift values
bool changed_tool = false ;
bool wait_for_toolchange = false ;
Print print ;
Model model ;
Test : : init_print ( { TestMesh : : cube_20x20x20 } , print , model , config , false , duplicate ) ;
std : : string gcode = Test : : gcode ( print ) ;
if constexpr ( debug_files ) {
static int count { 0 } ;
std : : ofstream file { " check_gcode_ " + std : : to_string ( count + + ) + " .gcode " } ;
file < < gcode ;
}
GCodeReader parser ;
parser . parse_buffer ( gcode , [ & ] ( Slic3r : : GCodeReader & self , const Slic3r : : GCodeReader : : GCodeLine & line ) {
std : : regex regex { " ^T( \\ d+) " } ;
std : : smatch matches ;
std : : string cmd { line . cmd ( ) } ;
if ( std : : regex_match ( cmd , matches , regex ) ) {
tool = std : : stoul ( matches [ 1 ] . str ( ) ) ;
changed_tool = true ;
wait_for_toolchange = false ;
toolchange_count [ tool ] + + ;
} else if ( std : : regex_match ( cmd , std : : regex { " ^G[01]$ " } ) & & ! line . has ( Z ) ) { // ignore lift taking place after retraction
INFO ( " Toolchange must not happen right after retraction. " ) ;
CHECK ( ! wait_for_toolchange ) ;
}
const double retract_length = config . option < ConfigOptionFloats > ( " retract_length " ) - > get_at ( tool ) ;
const double retract_before_travel = config . option < ConfigOptionFloats > ( " retract_before_travel " ) - > get_at ( tool ) ;
const double retract_length_toolchange = config . option < ConfigOptionFloats > ( " retract_length_toolchange " ) - > get_at ( tool ) ;
const double retract_restart_extra = config . option < ConfigOptionFloats > ( " retract_restart_extra " ) - > get_at ( tool ) ;
const double retract_restart_extra_toolchange = config . option < ConfigOptionFloats > ( " retract_restart_extra_toolchange " ) - > get_at ( tool ) ;
2025-03-22 09:44:19 +08:00
const double travel_speed = config . opt_float ( " travel_speed " ) ;
const double feedrate = line . has_f ( ) ? line . f ( ) : self . f ( ) ;
2024-03-27 14:38:03 +08:00
if ( line . dist_Z ( self ) ! = 0 ) {
// lift move or lift + change layer
const double retract_lift = config . option < ConfigOptionFloats > ( " retract_lift " ) - > get_at ( tool ) ;
if (
line . dist_Z ( self ) = = Approx ( retract_lift )
| | (
line . dist_Z ( self ) = = Approx ( config . opt_float ( " layer_height " ) + retract_lift )
& & retract_lift > 0
)
) {
INFO ( " Only lift while retracted " ) ;
CHECK ( retracted [ tool ] ) ;
INFO ( " No double lift " ) ;
CHECK ( ! lifted ) ;
lifted = true ;
lift_dist = line . dist_Z ( self ) ;
}
if ( line . dist_Z ( self ) < 0 ) {
2025-03-22 09:44:19 +08:00
INFO ( " Must be lifted before going down. " ) ;
2024-03-27 14:38:03 +08:00
CHECK ( lifted ) ;
INFO ( " Going down by the same amount of the lift or by the amount needed to get to next layer " ) ;
CHECK ( (
line . dist_Z ( self ) = = Approx ( - lift_dist )
| | line . dist_Z ( self ) = = Approx ( - lift_dist + config . opt_float ( " layer_height " ) )
) ) ;
lift_dist = 0 ;
lifted = false ;
}
2025-03-22 09:44:19 +08:00
const double travel_speed_z = config . opt_float ( " travel_speed_z " ) ;
if ( travel_speed_z ) {
Vec3d move { line . dist_X ( self ) , line . dist_Y ( self ) , line . dist_Z ( self ) } ;
const double move_u_z = move . z ( ) / move . norm ( ) ;
const double travel_speed_ = std : : abs ( travel_speed_z / move_u_z ) ;
INFO ( " move Z feedrate Z component is less than or equal to travel_speed_z " ) ;
CHECK ( feedrate * std : : abs ( move_u_z ) < = Approx ( travel_speed_z * 60 ) . epsilon ( GCodeFormatter : : XYZ_EPSILON ) ) ;
if ( travel_speed_ < travel_speed ) {
INFO ( " move Z at travel speed Z " ) ;
CHECK ( feedrate = = Approx ( travel_speed_ * 60 ) . epsilon ( GCodeFormatter : : XYZ_EPSILON ) ) ;
INFO ( " move Z feedrate Z component is equal to travel_speed_z " ) ;
CHECK ( feedrate * std : : abs ( move_u_z ) = = Approx ( travel_speed_z * 60 ) . epsilon ( GCodeFormatter : : XYZ_EPSILON ) ) ;
} else {
INFO ( " move Z at travel speed " ) ;
CHECK ( feedrate = = Approx ( travel_speed * 60 ) . epsilon ( GCodeFormatter : : XYZ_EPSILON ) ) ;
}
} else {
INFO ( " move Z at travel speed " ) ;
CHECK ( feedrate = = Approx ( travel_speed * 60 ) . epsilon ( GCodeFormatter : : XYZ_EPSILON ) ) ;
}
2024-03-27 14:38:03 +08:00
}
if ( line . retracting ( self ) ) {
retracted [ tool ] = true ;
retracted_length [ tool ] + = - line . dist_E ( self ) ;
if ( retracted_length [ tool ] = = Approx ( retract_length ) ) {
// okay
} else if ( retracted_length [ tool ] = = Approx ( retract_length_toolchange ) ) {
wait_for_toolchange = true ;
} else {
INFO ( " Not retracted by the correct amount. " ) ;
CHECK ( false ) ;
}
}
if ( line . extruding ( self ) ) {
INFO ( " Only extruding while not lifted " ) ;
CHECK ( ! lifted ) ;
if ( retracted [ tool ] ) {
double expected_amount = retracted_length [ tool ] + retract_restart_extra ;
if ( changed_tool & & toolchange_count [ tool ] > 1 ) {
expected_amount = retract_length_toolchange + retract_restart_extra_toolchange ;
changed_tool = false ;
}
INFO ( " Unretracted by the correct amount " ) ;
REQUIRE ( line . dist_E ( self ) = = Approx ( expected_amount ) ) ;
retracted [ tool ] = false ;
retracted_length [ tool ] = 0 ;
}
}
if ( line . travel ( ) & & line . dist_XY ( self ) > = retract_before_travel ) {
INFO ( " Retracted before long travel move " ) ;
CHECK ( retracted [ tool ] ) ;
}
} ) ;
}
void test_slicing ( std : : initializer_list < TestMesh > meshes , DynamicPrintConfig & config , const unsigned duplicate = 1 ) {
SECTION ( " Retraction " ) {
check_gcode ( meshes , config , duplicate ) ;
}
SECTION ( " Restart extra length " ) {
config . set_deserialize_strict ( { { " retract_restart_extra " , " 1 " } } ) ;
check_gcode ( meshes , config , duplicate ) ;
}
SECTION ( " Retract_lift " ) {
config . set_deserialize_strict ( { { " retract_lift " , " 1,2 " } } ) ;
check_gcode ( meshes , config , duplicate ) ;
}
}
2025-03-22 09:44:19 +08:00
TEST_CASE ( " Slicing with retraction and lifting " , " [retraction] " ) {
DynamicPrintConfig config = Slic3r : : DynamicPrintConfig : : full_print_config ( ) ;
config . set_deserialize_strict ( {
{ " nozzle_diameter " , " 0.6,0.6,0.6,0.6 " } ,
{ " first_layer_height " , config . opt_float ( " layer_height " ) } ,
{ " first_layer_speed " , " 100% " } ,
{ " start_gcode " , " " } , // To avoid dealing with the nozzle lift in start G-code
{ " retract_length " , " 1.5 " } ,
{ " retract_before_travel " , " 3 " } ,
{ " retract_layer_change " , " 1 " } ,
{ " only_retract_when_crossing_perimeters " , 0 } ,
} ) ;
SECTION ( " Standard run " ) {
test_slicing ( { TestMesh : : cube_20x20x20 } , config ) ;
}
SECTION ( " With duplicate cube " ) {
test_slicing ( { TestMesh : : cube_20x20x20 } , config , 2 ) ;
}
SECTION ( " Dual extruder with multiple skirt layers " ) {
config . set_deserialize_strict ( {
{ " infill_extruder " , 2 } ,
{ " skirts " , 4 } ,
{ " skirt_height " , 3 } ,
} ) ;
test_slicing ( { TestMesh : : cube_20x20x20 } , config ) ;
}
}
TEST_CASE ( " Slicing with retraction and lifting with travel_speed_z=10 " , " [retraction] " ) {
2024-03-27 14:38:03 +08:00
DynamicPrintConfig config = Slic3r : : DynamicPrintConfig : : full_print_config ( ) ;
config . set_deserialize_strict ( {
{ " nozzle_diameter " , " 0.6,0.6,0.6,0.6 " } ,
{ " first_layer_height " , config . opt_float ( " layer_height " ) } ,
{ " first_layer_speed " , " 100% " } ,
{ " start_gcode " , " " } , // To avoid dealing with the nozzle lift in start G-code
{ " retract_length " , " 1.5 " } ,
{ " retract_before_travel " , " 3 " } ,
{ " retract_layer_change " , " 1 " } ,
{ " only_retract_when_crossing_perimeters " , 0 } ,
2025-03-22 09:44:19 +08:00
{ " travel_speed " , " 600 " } ,
{ " travel_speed_z " , " 10 " } ,
2024-03-27 14:38:03 +08:00
} ) ;
SECTION ( " Standard run " ) {
test_slicing ( { TestMesh : : cube_20x20x20 } , config ) ;
}
SECTION ( " With duplicate cube " ) {
test_slicing ( { TestMesh : : cube_20x20x20 } , config , 2 ) ;
}
SECTION ( " Dual extruder with multiple skirt layers " ) {
config . set_deserialize_strict ( {
{ " infill_extruder " , 2 } ,
{ " skirts " , 4 } ,
{ " skirt_height " , 3 } ,
} ) ;
test_slicing ( { TestMesh : : cube_20x20x20 } , config ) ;
}
}
TEST_CASE ( " Z moves " , " [retraction] " ) {
DynamicPrintConfig config = Slic3r : : DynamicPrintConfig : : full_print_config ( ) ;
config . set_deserialize_strict ( {
{ " start_gcode " , " " } , // To avoid dealing with the nozzle lift in start G-code
{ " retract_length " , " 0 " } ,
{ " retract_layer_change " , " 0 " } ,
{ " retract_lift " , " 0.2 " }
} ) ;
bool retracted = false ;
unsigned layer_changes_with_retraction = 0 ;
unsigned retractions = 0 ;
unsigned z_restores = 0 ;
std : : string gcode = Slic3r : : Test : : slice ( { TestMesh : : cube_20x20x20 } , config ) ;
if constexpr ( debug_files ) {
std : : ofstream file { " zmoves.gcode " } ;
file < < gcode ;
}
GCodeReader parser ;
parser . parse_buffer ( gcode , [ & ] ( Slic3r : : GCodeReader & self , const Slic3r : : GCodeReader : : GCodeLine & line ) {
if ( line . retracting ( self ) ) {
retracted = true ;
retractions + + ;
} else if ( line . extruding ( self ) & & retracted ) {
retracted = 0 ;
}
if ( line . dist_Z ( self ) ! = 0 & & retracted ) {
layer_changes_with_retraction + + ;
}
if ( line . dist_Z ( self ) < 0 ) {
z_restores + + ;
}
} ) ;
INFO ( " no retraction on layer change " ) ;
CHECK ( layer_changes_with_retraction = = 0 ) ;
INFO ( " no retractions " ) ;
CHECK ( retractions = = 0 ) ;
INFO ( " no lift " ) ;
CHECK ( z_restores = = 0 ) ;
}
TEST_CASE ( " Firmware retraction handling " , " [retraction] " ) {
DynamicPrintConfig config = Slic3r : : DynamicPrintConfig : : full_print_config ( ) ;
config . set_deserialize_strict ( {
{ " use_firmware_retraction " , 1 } ,
} ) ;
bool retracted = false ;
unsigned double_retractions = 0 ;
unsigned double_unretractions = 0 ;
std : : string gcode = Slic3r : : Test : : slice ( { TestMesh : : cube_20x20x20 } , config ) ;
GCodeReader parser ;
parser . parse_buffer ( gcode , [ & ] ( Slic3r : : GCodeReader & self , const Slic3r : : GCodeReader : : GCodeLine & line ) {
if ( line . cmd_is ( " G10 " ) ) {
if ( retracted )
double_retractions + + ;
retracted = true ;
} else if ( line . cmd_is ( " G11 " ) ) {
if ( ! retracted )
double_unretractions + + ;
retracted = 0 ;
}
} ) ;
INFO ( " No double retractions " ) ;
CHECK ( double_retractions = = 0 ) ;
INFO ( " No double unretractions " ) ;
CHECK ( double_unretractions = = 0 ) ;
}
TEST_CASE ( " Firmware retraction when length is 0 " , " [retraction] " ) {
DynamicPrintConfig config = Slic3r : : DynamicPrintConfig : : full_print_config ( ) ;
config . set_deserialize_strict ( {
{ " use_firmware_retraction " , 1 } ,
{ " retract_length " , " 0 " } ,
} ) ;
bool retracted = false ;
std : : string gcode = Slic3r : : Test : : slice ( { TestMesh : : cube_20x20x20 } , config ) ;
GCodeReader parser ;
parser . parse_buffer ( gcode , [ & ] ( Slic3r : : GCodeReader & self , const Slic3r : : GCodeReader : : GCodeLine & line ) {
if ( line . cmd_is ( " G10 " ) ) {
retracted = true ;
}
} ) ;
INFO ( " Retracting also when --retract-length is 0 but --use-firmware-retraction is enabled " ) ;
CHECK ( retracted ) ;
}
std : : vector < double > get_lift_layers ( const DynamicPrintConfig & config ) {
Print print ;
Model model ;
Test : : init_print ( { TestMesh : : cube_20x20x20 } , print , model , config , false , 2 ) ;
std : : string gcode = Test : : gcode ( print ) ;
std : : vector < double > result ;
GCodeReader parser ;
parser . parse_buffer ( gcode , [ & ] ( Slic3r : : GCodeReader & self , const Slic3r : : GCodeReader : : GCodeLine & line ) {
if ( line . cmd_is ( " G1 " ) & & line . dist_Z ( self ) < 0 ) {
result . push_back ( line . new_Z ( self ) ) ;
}
} ) ;
return result ;
}
bool values_are_in_range ( const std : : vector < double > & values , double from , double to ) {
for ( const double & value : values ) {
if ( value < from | | value > to ) {
return false ;
}
}
return true ;
}
TEST_CASE ( " Lift above/bellow layers " , " [retraction] " ) {
DynamicPrintConfig config = Slic3r : : DynamicPrintConfig : : full_print_config ( ) ;
config . set_deserialize_strict ( {
{ " nozzle_diameter " , " 0.6,0.6,0.6,0.6 " } ,
{ " start_gcode " , " " } ,
{ " retract_lift " , " 3,4 " } ,
} ) ;
config . set_deserialize_strict ( {
{ " retract_lift_above " , " 0, 0 " } ,
{ " retract_lift_below " , " 0, 0 " } ,
} ) ;
std : : vector < double > lift_layers = get_lift_layers ( config ) ;
INFO ( " lift takes place when above/below == 0 " ) ;
CHECK ( ! lift_layers . empty ( ) ) ;
config . set_deserialize_strict ( {
{ " retract_lift_above " , " 5, 6 " } ,
{ " retract_lift_below " , " 15, 13 " } ,
} ) ;
lift_layers = get_lift_layers ( config ) ;
INFO ( " lift takes place when above/below != 0 " ) ;
CHECK ( ! lift_layers . empty ( ) ) ;
double retract_lift_above = config . option < ConfigOptionFloats > ( " retract_lift_above " ) - > get_at ( 0 ) ;
double retract_lift_below = config . option < ConfigOptionFloats > ( " retract_lift_below " ) - > get_at ( 0 ) ;
INFO ( " Z is not lifted above/below the configured value " ) ;
CHECK ( values_are_in_range ( lift_layers , retract_lift_above , retract_lift_below ) ) ;
// check lifting with different values for 2. extruder
config . set_deserialize_strict ( {
{ " perimeter_extruder " , 2 } ,
{ " infill_extruder " , 2 } ,
{ " retract_lift_above " , " 0, 0 " } ,
{ " retract_lift_below " , " 0, 0 " }
} ) ;
lift_layers = get_lift_layers ( config ) ;
INFO ( " lift takes place when above/below == 0 for 2. extruder " ) ;
CHECK ( ! lift_layers . empty ( ) ) ;
config . set_deserialize_strict ( {
{ " retract_lift_above " , " 5, 6 " } ,
{ " retract_lift_below " , " 15, 13 " } ,
} ) ;
lift_layers = get_lift_layers ( config ) ;
INFO ( " lift takes place when above/below != 0 for 2. extruder " ) ;
CHECK ( ! lift_layers . empty ( ) ) ;
retract_lift_above = config . option < ConfigOptionFloats > ( " retract_lift_above " ) - > get_at ( 1 ) ;
retract_lift_below = config . option < ConfigOptionFloats > ( " retract_lift_below " ) - > get_at ( 1 ) ;
INFO ( " Z is not lifted above/below the configured value for 2. extruder " ) ;
CHECK ( values_are_in_range ( lift_layers , retract_lift_above , retract_lift_below ) ) ;
}