diff --git a/src/libslic3r/GCode/FindReplace.cpp b/src/libslic3r/GCode/FindReplace.cpp index d349632b5..480994da8 100644 --- a/src/libslic3r/GCode/FindReplace.cpp +++ b/src/libslic3r/GCode/FindReplace.cpp @@ -5,29 +5,47 @@ namespace Slic3r { -GCodeFindReplace::GCodeFindReplace(const PrintConfig &print_config) +// Similar to https://npp-user-manual.org/docs/searching/#extended-search-mode +const void unescape_extended_search_mode(std::string &s) { - const std::vector &subst = print_config.gcode_substitutions.values; + boost::replace_all(s, "\\n", "\n"); // Line Feed control character LF (ASCII 0x0A) + boost::replace_all(s, "\\r", "\r"); // Carriage Return control character CR (ASCII 0x0D) + boost::replace_all(s, "\\t", "\t"); // TAB control character (ASCII 0x09) + boost::replace_all(s, "\\0", "\0x00"); // NUL control character (ASCII 0x00) + boost::replace_all(s, "\\\\", "\\"); // Backslash character (ASCII 0x5C) - if ((subst.size() % 3) != 0) +// Notepad++ also supports: +// \o: the octal representation of a byte, made of 3 digits in the 0-7 range +// \d: the decimal representation of a byte, made of 3 digits in the 0-9 range +// \x: the hexadecimal representation of a byte, made of 2 digits in the 0-9, A-F/a-f range. +// \u: The hexadecimal representation of a two-byte character, made of 4 digits in the 0-9, A-F/a-f range. +} + +GCodeFindReplace::GCodeFindReplace(const std::vector &gcode_substitutions) +{ + if ((gcode_substitutions.size() % 3) != 0) throw RuntimeError("Invalid length of gcode_substitutions parameter"); - m_substitutions.reserve(subst.size() / 3); - for (size_t i = 0; i < subst.size(); i += 3) { + m_substitutions.reserve(gcode_substitutions.size() / 3); + for (size_t i = 0; i < gcode_substitutions.size(); i += 3) { Substitution out; try { - out.plain_pattern = subst[i]; - out.format = subst[i + 1]; - const std::string ¶ms = subst[i + 2]; + out.plain_pattern = gcode_substitutions[i]; + out.format = gcode_substitutions[i + 1]; + const std::string ¶ms = gcode_substitutions[i + 2]; out.regexp = strchr(params.c_str(), 'r') != nullptr || strchr(params.c_str(), 'R') != nullptr; out.case_insensitive = strchr(params.c_str(), 'i') != nullptr || strchr(params.c_str(), 'I') != nullptr; out.whole_word = strchr(params.c_str(), 'w') != nullptr || strchr(params.c_str(), 'W') != nullptr; - if (out.regexp) + if (out.regexp) { out.regexp_pattern.assign( out.whole_word ? - std::string("\b") + out.plain_pattern + "\b" : + std::string("\\b") + out.plain_pattern + "\\b" : out.plain_pattern, (out.case_insensitive ? boost::regex::icase : 0) | boost::regex::optimize); + } else { + unescape_extended_search_mode(out.plain_pattern); + unescape_extended_search_mode(out.format); + } } catch (const std::exception &ex) { throw RuntimeError(std::string("Invalid gcode_substitutions parameter, failed to compile regular expression: ") + ex.what()); } diff --git a/src/libslic3r/GCode/FindReplace.hpp b/src/libslic3r/GCode/FindReplace.hpp index ff25700b9..2d12cf28b 100644 --- a/src/libslic3r/GCode/FindReplace.hpp +++ b/src/libslic3r/GCode/FindReplace.hpp @@ -9,7 +9,9 @@ namespace Slic3r { class GCodeFindReplace { public: - GCodeFindReplace(const PrintConfig &print_config); + GCodeFindReplace(const PrintConfig &print_config) : GCodeFindReplace(print_config.gcode_substitutions.values) {} + GCodeFindReplace(const std::vector &gcode_substitutions); + std::string process_layer(const std::string &gcode); diff --git a/tests/fff_print/CMakeLists.txt b/tests/fff_print/CMakeLists.txt index c69e722af..50b45e384 100644 --- a/tests/fff_print/CMakeLists.txt +++ b/tests/fff_print/CMakeLists.txt @@ -7,6 +7,7 @@ add_executable(${_TEST_NAME}_tests test_fill.cpp test_flow.cpp test_gcode.cpp + test_gcodefindreplace.cpp test_gcodewriter.cpp test_model.cpp test_print.cpp diff --git a/tests/fff_print/test_gcodefindreplace.cpp b/tests/fff_print/test_gcodefindreplace.cpp new file mode 100644 index 000000000..be0dd8a01 --- /dev/null +++ b/tests/fff_print/test_gcodefindreplace.cpp @@ -0,0 +1,218 @@ +#include + +#include + +#include "libslic3r/GCode/FindReplace.hpp" + +using namespace Slic3r; + +SCENARIO("Find/Replace with plain text", "[GCodeFindReplace]") { + GIVEN("G-code") { + const std::string gcode = + "G1 Z0; home\n" + "G1 Z1; move up\n" + "G1 X0 Y1 Z1; perimeter\n" + "G1 X13 Y32 Z1; infill\n" + "G1 X13 Y32 Z1; wipe\n"; + WHEN("Replace \"move up\" with \"move down\", case sensitive") { + GCodeFindReplace find_replace({ "move up", "move down", "" }); + REQUIRE(find_replace.process_layer(gcode) == + "G1 Z0; home\n" + // substituted + "G1 Z1; move down\n" + "G1 X0 Y1 Z1; perimeter\n" + "G1 X13 Y32 Z1; infill\n" + "G1 X13 Y32 Z1; wipe\n"); + } + WHEN("Replace \"move up\" with \"move down\", case insensitive") { + GCodeFindReplace find_replace({ "move up", "move down", "i" }); + REQUIRE(find_replace.process_layer(gcode) == + "G1 Z0; home\n" + // substituted + "G1 Z1; move down\n" + "G1 X0 Y1 Z1; perimeter\n" + "G1 X13 Y32 Z1; infill\n" + "G1 X13 Y32 Z1; wipe\n"); + } + WHEN("Replace \"move UP\" with \"move down\", case insensitive") { + GCodeFindReplace find_replace({ "move UP", "move down", "i" }); + REQUIRE(find_replace.process_layer(gcode) == + "G1 Z0; home\n" + // substituted + "G1 Z1; move down\n" + "G1 X0 Y1 Z1; perimeter\n" + "G1 X13 Y32 Z1; infill\n" + "G1 X13 Y32 Z1; wipe\n"); + } + WHEN("Replace \"move up\" with \"move down\", case sensitive") { + GCodeFindReplace find_replace({ "move UP", "move down", "" }); + REQUIRE(find_replace.process_layer(gcode) == gcode); + } + + // Whole word + WHEN("Replace \"move up\" with \"move down\", whole word") { + GCodeFindReplace find_replace({ "move up", "move down", "w" }); + REQUIRE(find_replace.process_layer(gcode) == + "G1 Z0; home\n" + // substituted + "G1 Z1; move down\n" + "G1 X0 Y1 Z1; perimeter\n" + "G1 X13 Y32 Z1; infill\n" + "G1 X13 Y32 Z1; wipe\n"); + } + WHEN("Replace \"move u\" with \"move down\", whole word") { + GCodeFindReplace find_replace({ "move u", "move down", "w" }); + REQUIRE(find_replace.process_layer(gcode) == gcode); + } + WHEN("Replace \"ove up\" with \"move down\", whole word") { + GCodeFindReplace find_replace({ "move u", "move down", "w" }); + REQUIRE(find_replace.process_layer(gcode) == gcode); + } + + // Multi-line replace + WHEN("Replace \"move up\\nG1 X0 \" with \"move down\\nG0 X1 \"") { + GCodeFindReplace find_replace({ "move up\\nG1 X0 ", "move down\\nG0 X1 ", "" }); + REQUIRE(find_replace.process_layer(gcode) == + "G1 Z0; home\n" + // substituted + "G1 Z1; move down\n" + "G0 X1 Y1 Z1; perimeter\n" + "G1 X13 Y32 Z1; infill\n" + "G1 X13 Y32 Z1; wipe\n"); + } + // Multi-line replace, whole word. + WHEN("Replace \"move up\\nG1 X0\" with \"move down\\nG0 X1\", whole word") { + GCodeFindReplace find_replace({ "move up\\nG1 X0", "move down\\nG0 X1", "w" }); + REQUIRE(find_replace.process_layer(gcode) == + "G1 Z0; home\n" + // substituted + "G1 Z1; move down\n" + "G0 X1 Y1 Z1; perimeter\n" + "G1 X13 Y32 Z1; infill\n" + "G1 X13 Y32 Z1; wipe\n"); + } + // Multi-line replace, whole word, fails. + WHEN("Replace \"move up\\nG1 X\" with \"move down\\nG0 X\", whole word") { + GCodeFindReplace find_replace({ "move up\\nG1 X", "move down\\nG0 X", "w" }); + REQUIRE(find_replace.process_layer(gcode) == gcode); + } + } + + GIVEN("G-code with decimals") { + const std::string gcode = + "G1 Z0.123; home\n" + "G1 Z1.21; move up\n" + "G1 X0 Y.33 Z.431 E1.2; perimeter\n"; + WHEN("Regular expression NOT processed in non-regex mode") { + GCodeFindReplace find_replace({ "( [XYZEF]-?)\\.([0-9]+)", "\\10.\\2", "" }); + REQUIRE(find_replace.process_layer(gcode) == gcode); + } + } +} + +SCENARIO("Find/Replace with regexp", "[GCodeFindReplace]") { + GIVEN("G-code") { + const std::string gcode = + "G1 Z0; home\n" + "G1 Z1; move up\n" + "G1 X0 Y1 Z1; perimeter\n" + "G1 X13 Y32 Z1; infill\n" + "G1 X13 Y32 Z1; wipe\n"; + WHEN("Replace \"move up\" with \"move down\", case sensitive") { + GCodeFindReplace find_replace({ "move up", "move down", "r" }); + REQUIRE(find_replace.process_layer(gcode) == + "G1 Z0; home\n" + // substituted + "G1 Z1; move down\n" + "G1 X0 Y1 Z1; perimeter\n" + "G1 X13 Y32 Z1; infill\n" + "G1 X13 Y32 Z1; wipe\n"); + } + WHEN("Replace \"move up\" with \"move down\", case insensitive") { + GCodeFindReplace find_replace({ "move up", "move down", "ri" }); + REQUIRE(find_replace.process_layer(gcode) == + "G1 Z0; home\n" + // substituted + "G1 Z1; move down\n" + "G1 X0 Y1 Z1; perimeter\n" + "G1 X13 Y32 Z1; infill\n" + "G1 X13 Y32 Z1; wipe\n"); + } + WHEN("Replace \"move UP\" with \"move down\", case insensitive") { + GCodeFindReplace find_replace({ "move UP", "move down", "ri" }); + REQUIRE(find_replace.process_layer(gcode) == + "G1 Z0; home\n" + // substituted + "G1 Z1; move down\n" + "G1 X0 Y1 Z1; perimeter\n" + "G1 X13 Y32 Z1; infill\n" + "G1 X13 Y32 Z1; wipe\n"); + } + WHEN("Replace \"move up\" with \"move down\", case sensitive") { + GCodeFindReplace find_replace({ "move UP", "move down", "r" }); + REQUIRE(find_replace.process_layer(gcode) == gcode); + } + + // Whole word + WHEN("Replace \"move up\" with \"move down\", whole word") { + GCodeFindReplace find_replace({ "move up", "move down", "rw" }); + REQUIRE(find_replace.process_layer(gcode) == + "G1 Z0; home\n" + // substituted + "G1 Z1; move down\n" + "G1 X0 Y1 Z1; perimeter\n" + "G1 X13 Y32 Z1; infill\n" + "G1 X13 Y32 Z1; wipe\n"); + } + WHEN("Replace \"move u\" with \"move down\", whole word") { + GCodeFindReplace find_replace({ "move u", "move down", "rw" }); + REQUIRE(find_replace.process_layer(gcode) == gcode); + } + WHEN("Replace \"ove up\" with \"move down\", whole word") { + GCodeFindReplace find_replace({ "move u", "move down", "rw" }); + REQUIRE(find_replace.process_layer(gcode) == gcode); + } + + // Multi-line replace + WHEN("Replace \"move up\\nG1 X0 \" with \"move down\\nG0 X1 \"") { + GCodeFindReplace find_replace({ "move up\\nG1 X0 ", "move down\\nG0 X1 ", "r" }); + REQUIRE(find_replace.process_layer(gcode) == + "G1 Z0; home\n" + // substituted + "G1 Z1; move down\n" + "G0 X1 Y1 Z1; perimeter\n" + "G1 X13 Y32 Z1; infill\n" + "G1 X13 Y32 Z1; wipe\n"); + } + // Multi-line replace, whole word. + WHEN("Replace \"move up\\nG1 X0\" with \"move down\\nG0 X1\", whole word") { + GCodeFindReplace find_replace({ "move up\\nG1 X0", "move down\\nG0 X1", "rw" }); + REQUIRE(find_replace.process_layer(gcode) == + "G1 Z0; home\n" + // substituted + "G1 Z1; move down\n" + "G0 X1 Y1 Z1; perimeter\n" + "G1 X13 Y32 Z1; infill\n" + "G1 X13 Y32 Z1; wipe\n"); + } + // Multi-line replace, whole word, fails. + WHEN("Replace \"move up\\nG1 X\" with \"move down\\nG0 X\", whole word") { + GCodeFindReplace find_replace({ "move up\\nG1 X", "move down\\nG0 X", "rw" }); + REQUIRE(find_replace.process_layer(gcode) == gcode); + } + } + + GIVEN("G-code with decimals") { + const std::string gcode = + "G1 Z0.123; home\n" + "G1 Z1.21; move up\n" + "G1 X0 Y.33 Z.431 E1.2; perimeter\n"; + WHEN("Missing zeros before dot filled in") { + GCodeFindReplace find_replace({ "( [XYZEF]-?)\\.([0-9]+)", "\\10.\\2", "r" }); + REQUIRE(find_replace.process_layer(gcode) == + "G1 Z0.123; home\n" + "G1 Z1.21; move up\n" + "G1 X0 Y0.33 Z0.431 E1.2; perimeter\n"); + } + } +}