#include #include #include #include "libslic3r/GCode/GCodeProcessor.hpp" #include "libslic3r/GCode/WipeTower.hpp" #include "libslic3r/PrintConfig.hpp" #include "test_helpers.hpp" using namespace Slic3r; using namespace Slic3r::Test; // Taken from the config enum map rather than hand-listed, so a flavor added to GCodeFlavor later // is covered here without editing this file. static std::vector non_klipper_flavors() { std::vector flavors; for (const auto &[name, value] : ConfigOptionEnum::get_enum_values()) if (GCodeFlavor(value) != gcfKlipper) flavors.push_back(GCodeFlavor(value)); return flavors; } static std::string flavor_name(GCodeFlavor flavor) { return ConfigOptionEnum::get_enum_names()[int(flavor)]; } TEST_CASE("Klipper flushes the wipe tower planner queue with M400", "[WipeTower]") { CHECK(std::string(flush_planner_queue_command(gcfKlipper)) == "M400\n"); } TEST_CASE("Other flavors flush the wipe tower planner queue with a zero dwell", "[WipeTower]") { const GCodeFlavor flavor = GENERATE(from_range(non_klipper_flavors())); INFO("gcode flavor: " << flavor_name(flavor)); CHECK(std::string(flush_planner_queue_command(flavor)) == "G4 S0\n"); } // 1.5s is exactly representable as a float, so neither form can drift when rounded. TEST_CASE("Klipper waits in the wipe tower with a millisecond dwell", "[WipeTower]") { CHECK(wait_command(gcfKlipper, 1.5f) == "G4 P1500\n"); } TEST_CASE("Other flavors wait in the wipe tower with a seconds dwell", "[WipeTower]") { const GCodeFlavor flavor = GENERATE(from_range(non_klipper_flavors())); INFO("gcode flavor: " << flavor_name(flavor)); CHECK(wait_command(flavor, 1.5f) == "G4 S1.500\n"); } // The cases above only exercise the helpers in isolation. The one below slices a real // two-filament print, so it also covers the binding constraint of both changes: that the // configured `gcode_flavor` reaches the wipe tower writer and lands in the exported G-code. // The G-code inside each WIPE_TOWER_START/WIPE_TOWER_END pair, concatenated, so an M400 emitted // outside the tower (e.g. GCodeProcessor's pre-heat injector) cannot create a false match. static std::string wipe_tower_regions(const std::string &gcode) { const std::string &start_tag = GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Wipe_Tower_Start); const std::string &end_tag = GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Wipe_Tower_End); std::string regions; size_t pos = 0; while (true) { size_t start = gcode.find(start_tag, pos); if (start == std::string::npos) break; size_t end = gcode.find(end_tag, start); if (end == std::string::npos) break; regions.append(gcode, start, end - start); pos = end + 1; } return regions; } // A per-layer toolchange between the wall and infill filaments, same shape as // test_multifilament.cpp's "Each feature prints with its assigned filament", so the wipe tower // runs its toolchange path (and so `flush_planner_queue()`) on every layer. static DynamicPrintConfig wipe_tower_toolchange_config(const std::string &gcode_flavor) { return multifilament_config(2, { { "sparse_infill_filament_id", 1 }, { "internal_solid_filament_id", 1 }, { "top_surface_filament_id", 1 }, { "bottom_surface_filament_id", 1 }, { "outer_wall_filament_id", 2 }, { "inner_wall_filament_id", 2 }, { "enable_prime_tower", true }, { "layer_height", 0.3 }, { "gcode_flavor", gcode_flavor }, }); } // Slices a 10mm cube under `config`. Not plain Test::slice: a brand-new Print's first `apply()` // counts one filament in use, and DynamicPrintConfig::normalize_fdm_2's single-filament rule then // clears `enable_prime_tower`. A second apply, once init_print's regions have settled, sees both // filaments and the tower survives. static std::string slice_with_prime_tower(const DynamicPrintConfig &config) { Print print; Model model; init_print({ cube(10) }, print, model, config); print.apply(model, config); return gcode(print); } TEST_CASE("The wipe tower's toolchange planner flush follows the gcode flavor", "[WipeTower]") { auto [flavor, expected, unexpected] = GENERATE(table({ { "klipper", "M400", "G4 S0" }, { "marlin", "G4 S0", "M400" } })); DYNAMIC_SECTION(flavor) { const std::string tower = wipe_tower_regions(slice_with_prime_tower(wipe_tower_toolchange_config(flavor))); REQUIRE_FALSE(tower.empty()); CHECK_THAT(tower, Catch::Matchers::ContainsSubstring(expected)); CHECK_THAT(tower, !Catch::Matchers::ContainsSubstring(unexpected)); } }