#include #include #include #include "libslic3r/libslic3r.h" #include "libslic3r/GCode/GCodeProcessor.hpp" #include "libslic3r/Utils.hpp" #include "test_utils.hpp" #include #include #include #include using namespace Slic3r; // Bambu firmware uses the " FEATURE: " style reserved tags, everything else the Slic3r-compatible // "TYPE:" style, so which list applies depends on the printer kind passed in. TEST_CASE("Reserved keyword detection follows the printer kind it is given", "[GCodeProcessor]") { struct Case { const char* name; std::string gcode; bool reserved_on_bbl; bool reserved_on_non_bbl; }; const auto test_case = GENERATE(values({ {"compatible feature tag", ";TYPE:Prime tower", false, true}, {"compatible layer tag", ";LAYER_CHANGE", false, true}, {"bbl feature tag", "; FEATURE: Outer wall", true, false}, {"tag shared by both lists", ";_GP_FIRST_LINE_M73_PLACEHOLDER", true, true}, {"bbl spells this one with a leading space", ";COLOR_CHANGE", false, true}, {"ordinary comment", "; heat the bed", false, false}, {"not a comment at all", "G1 X10 Y10 F3000", false, false}, // A tag counts only as the whole comment's prefix, so neither a tag mentioned mid-comment // nor one trailing a real command is a reserved use. {"tag text later in the comment", "; the TYPE:Prime tower marker", false, false}, {"tag trailing a command", "G1 X10 ;TYPE:Prime tower", false, false}, })); DYNAMIC_SECTION(test_case.name) { std::vector tags; REQUIRE(GCodeProcessor::contains_reserved_tags(test_case.gcode, 5, tags, true) == test_case.reserved_on_bbl); tags.clear(); REQUIRE(GCodeProcessor::contains_reserved_tags(test_case.gcode, 5, tags, false) == test_case.reserved_on_non_bbl); } } TEST_CASE("Reserved keyword detection reports every offending line", "[GCodeProcessor]") { const std::string gcode = ";TYPE:Prime tower\nG1 X10\n;LAYER_CHANGE\n"; std::vector tags; REQUIRE(GCodeProcessor::contains_reserved_tags(gcode, 5, tags, false)); REQUIRE(tags.size() == 2); // Reported in the order they appear, which is what makes the max_count cut-off meaningful. CHECK(tags[0] == "TYPE:Prime tower"); CHECK(tags[1] == "LAYER_CHANGE"); SECTION("the reported count is capped at max_count") { tags.clear(); REQUIRE(GCodeProcessor::contains_reserved_tags(gcode, 1, tags, false)); CHECK(tags.size() == 1); CHECK(tags[0] == "TYPE:Prime tower"); } SECTION("a max_count of zero still reports the first tag") { tags.clear(); REQUIRE(GCodeProcessor::contains_reserved_tags(gcode, 0, tags, false)); CHECK(tags.size() == 1); } SECTION("g-code with nothing reserved in it reports nothing") { tags.clear(); CHECK_FALSE(GCodeProcessor::contains_reserved_tags("G28\n; home all axes\n", 5, tags, false)); CHECK(tags.empty()); } } namespace { // Closed outer-wall squares, each after a fast travel and before an inner-wall move, so the processor // records seams and inserts actual speed moves. virtual_moves adds a VG1 move after each square. void process_squares(int squares, GCodeProcessorResult &result, bool virtual_moves = false) { std::ostringstream gcode; gcode << "M83\nG90\n"; for (int i = 0; i < squares; ++i) { gcode << "G1 X10 Y10 Z" << 0.2 * (i + 1) << " F12000\n" << "; FEATURE: Outer wall\n" << "G1 X50 Y10 E2 F3000\nG1 X50 Y50 E2\nG1 X10 Y50 E2\nG1 X10 Y10 E2\n" << "; FEATURE: Inner wall\n" << "G1 X12 Y12 E0.1\nG1 X30 Y12 E1\n"; if (virtual_moves) gcode << "VG1 X20 Y30 F12000\n"; } FullPrintConfig config; config.gcode_flavor.value = gcfMarlinFirmware; // s_IsBBLPrinter selects the "; FEATURE: " role tags this G-code uses. const bool was_bbl_printer = GCodeProcessor::s_IsBBLPrinter; const ScopeGuard restore_bbl_printer([was_bbl_printer] { GCodeProcessor::s_IsBBLPrinter = was_bbl_printer; }); GCodeProcessor::s_IsBBLPrinter = true; ScopedTemporaryFile temp(".gcode"); std::ofstream(temp.string()) << gcode.str(); GCodeProcessor processor; processor.apply_config(config); processor.process_file(temp.string()); result = std::move(processor.extract_result()); } bool is_block_move(const GCodeProcessorResult::MoveVertex &move) { return !move.internal_only && (move.type == EMoveType::Extrude || move.type == EMoveType::Travel); } } // namespace TEST_CASE("Actual speed moves are inserted on their block's segment just before its move", "[GCodeProcessor]") { // 60 squares take several planner passes, which remap the blocks kept between passes. const int squares = GENERATE(10, 60); const bool virtual_moves = GENERATE(false, true); GCodeProcessorResult result; process_squares(squares, result, virtual_moves); const auto &moves = result.moves; constexpr size_t normal = size_t(PrintEstimatedStatistics::ETimeMode::Normal); size_t inserted = 0; for (size_t i = 1; i < moves.size(); ++i) { if (!moves[i].internal_only) continue; ++inserted; // Inserted moves have zero time, but a VG1 block's time is written to whatever move its move_id names. if (!virtual_moves) CHECK(moves[i].time[normal] == 0.f); size_t block = i + 1; while (block < moves.size() && moves[block].internal_only) ++block; size_t previous = i - 1; while (previous > 0 && moves[previous].internal_only) --previous; REQUIRE(block < moves.size()); CHECK(moves[block].gcode_id == moves[i].gcode_id); const Vec3f segment = moves[block].position - moves[previous].position; const Vec3f offset = moves[i].position - moves[previous].position; CHECK(segment.cross(offset).norm() / segment.norm() < 1e-3f); } REQUIRE(inserted > 0); } TEST_CASE("A seam takes the actual speed of the move it follows", "[GCodeProcessor]") { GCodeProcessorResult result; // 10 squares fit in one planner pass, so the seam's move and the block after it are timed together. process_squares(10, result); const auto &moves = result.moves; size_t seams = 0; for (size_t i = 1; i < moves.size(); ++i) if (moves[i].type == EMoveType::Seam && is_block_move(moves[i - 1])) { ++seams; CHECK_THAT(moves[i].actual_feedrate, Catch::Matchers::WithinAbs(moves[i - 1].actual_feedrate, 1e-4)); } REQUIRE(seams > 0); }