This commit is contained in:
enricoturri1966 2022-01-18 10:48:56 +01:00
commit d09a0cea83
21 changed files with 2461 additions and 1595 deletions

View File

@ -139,7 +139,7 @@ documentation_link= https://help.prusa3d.com/en/article/per-model-settings_1674
disabled_tags = SLA
[hint:Solid infill threshold area]
text = Solid infill threshold area\nDid you know that you can make parts of your model with a small cross-section be filled with solid infill automatically? Set the<a>Solid infill threshold area</a>.(Expert mode only.)
text = Solid infill threshold area\nDid you know that you can make parts of your model with a small cross-section be filled with solid infill automatically? Set the<a>Solid infill threshold area.</a>(Expert mode only.)
hypertext_type = settings
hypertext_settings_opt = solid_infill_below_area
hypertext_settings_type = 1
@ -197,7 +197,7 @@ documentation_link = https://help.prusa3d.com/en/article/insert-pause-or-custom-
disabled_tags = SLA
[hint:Configuration snapshots]
text = Configuration snapshots\nDid you know that roll back to a complete backup of all system and user profiles? You can view and move back and forth between snapshots using the Configuration - <a>Configuration snapshots menu</a>.
text = Configuration snapshots\nDid you know that you can roll back to a complete backup of all system and user profiles? You can view and move back and forth between snapshots using the Configuration - <a>Configuration snapshots menu</a>.
documentation_link = https://help.prusa3d.com/en/article/configuration-snapshots_1776
hypertext_type = menubar
hypertext_menubar_menu_name = Configuration

File diff suppressed because it is too large Load Diff

View File

@ -100,6 +100,8 @@ set(SLIC3R_SOURCES
GCode/ThumbnailData.hpp
GCode/CoolingBuffer.cpp
GCode/CoolingBuffer.hpp
GCode/FindReplace.cpp
GCode/FindReplace.hpp
GCode/PostProcessor.cpp
GCode/PostProcessor.hpp
# GCode/PressureEqualizer.cpp

View File

@ -1099,6 +1099,11 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
// modifies m_silent_time_estimator_enabled
DoExport::init_gcode_processor(print.config(), m_processor, m_silent_time_estimator_enabled);
if (! print.config().gcode_substitutions.values.empty()) {
m_find_replace = make_unique<GCodeFindReplace>(print.config());
file.set_find_replace(m_find_replace.get());
}
// resets analyzer's tracking data
m_last_height = 0.f;
m_last_layer_z = 0.f;
@ -1560,15 +1565,25 @@ void GCode::process_layers(
[&cooling_buffer = *this->m_cooling_buffer.get()](GCode::LayerResult in) -> std::string {
return cooling_buffer.process_layer(std::move(in.gcode), in.layer_id, in.cooling_buffer_flush);
});
const auto find_replace = tbb::make_filter<std::string, std::string>(slic3r_tbb_filtermode::serial_in_order,
[&self = *this->m_find_replace.get()](std::string s) -> std::string {
return self.process_layer(std::move(s));
});
const auto output = tbb::make_filter<std::string, void>(slic3r_tbb_filtermode::serial_in_order,
[&output_stream](std::string s) { output_stream.write(s); }
);
// The pipeline elements are joined using const references, thus no copying is performed.
if (m_spiral_vase)
output_stream.set_find_replace(nullptr);
if (m_spiral_vase && m_find_replace)
tbb::parallel_pipeline(12, generator & spiral_vase & cooling & find_replace & output);
else if (m_spiral_vase)
tbb::parallel_pipeline(12, generator & spiral_vase & cooling & output);
else if (m_find_replace)
tbb::parallel_pipeline(12, generator & cooling & find_replace & output);
else
tbb::parallel_pipeline(12, generator & cooling & output);
output_stream.set_find_replace(m_find_replace.get());
}
// Process all layers of a single object instance (sequential mode) with a parallel pipeline:
@ -1603,15 +1618,25 @@ void GCode::process_layers(
[&cooling_buffer = *this->m_cooling_buffer.get()](GCode::LayerResult in)->std::string {
return cooling_buffer.process_layer(std::move(in.gcode), in.layer_id, in.cooling_buffer_flush);
});
const auto find_replace = tbb::make_filter<std::string, std::string>(slic3r_tbb_filtermode::serial_in_order,
[&self = *this->m_find_replace.get()](std::string s) -> std::string {
return self.process_layer(std::move(s));
});
const auto output = tbb::make_filter<std::string, void>(slic3r_tbb_filtermode::serial_in_order,
[&output_stream](std::string s) { output_stream.write(s); }
);
// The pipeline elements are joined using const references, thus no copying is performed.
if (m_spiral_vase)
output_stream.set_find_replace(nullptr);
if (m_spiral_vase && m_find_replace)
tbb::parallel_pipeline(12, generator & spiral_vase & cooling & find_replace & output);
else if (m_spiral_vase)
tbb::parallel_pipeline(12, generator & spiral_vase & cooling & output);
else if (m_find_replace)
tbb::parallel_pipeline(12, generator & cooling & find_replace & output);
else
tbb::parallel_pipeline(12, generator & cooling & output);
output_stream.set_find_replace(m_find_replace.get());
}
std::string GCode::placeholder_parser_process(const std::string &name, const std::string &templ, unsigned int current_extruder_id, const DynamicConfig *config_override)
@ -2824,11 +2849,11 @@ void GCode::GCodeOutputStream::close()
void GCode::GCodeOutputStream::write(const char *what)
{
if (what != nullptr) {
const char* gcode = what;
// writes string to file
fwrite(gcode, 1, ::strlen(gcode), this->f);
//FIXME don't allocate a string, maybe process a batch of lines?
m_processor.process_buffer(std::string(gcode));
std::string gcode(m_find_replace ? m_find_replace->process_layer(what) : what);
// writes string to file
fwrite(gcode.c_str(), 1, gcode.size(), this->f);
m_processor.process_buffer(gcode);
}
}

View File

@ -10,6 +10,7 @@
#include "PrintConfig.hpp"
#include "GCode/AvoidCrossingPerimeters.hpp"
#include "GCode/CoolingBuffer.hpp"
#include "GCode/FindReplace.hpp"
#include "GCode/SpiralVase.hpp"
#include "GCode/ToolOrdering.hpp"
#include "GCode/WipeTower.hpp"
@ -189,6 +190,11 @@ private:
GCodeOutputStream(FILE *f, GCodeProcessor &processor) : f(f), m_processor(processor) {}
~GCodeOutputStream() { this->close(); }
// Set a find-replace post-processor to modify the G-code before GCodePostProcessor.
// It is being set to null inside process_layers(), because the find-replace process
// is being called on a secondary thread to improve performance.
void set_find_replace(GCodeFindReplace *find_replace) { m_find_replace = find_replace; }
bool is_open() const { return f; }
bool is_error() const;
@ -208,7 +214,9 @@ private:
void write_format(const char* format, ...);
private:
FILE *f = nullptr;
FILE *f { nullptr };
// Find-replace post-processor to be called before GCodePostProcessor.
GCodeFindReplace *m_find_replace { nullptr };
GCodeProcessor &m_processor;
};
void _do_export(Print &print, GCodeOutputStream &file, ThumbnailsGeneratorCallback thumbnail_cb);
@ -393,6 +401,7 @@ private:
std::unique_ptr<CoolingBuffer> m_cooling_buffer;
std::unique_ptr<SpiralVase> m_spiral_vase;
std::unique_ptr<GCodeFindReplace> m_find_replace;
#ifdef HAS_PRESSURE_EQUALIZER
std::unique_ptr<PressureEqualizer> m_pressure_equalizer;
#endif /* HAS_PRESSURE_EQUALIZER */

View File

@ -439,7 +439,15 @@ static std::vector<TravelPoint> simplify_travel(const AvoidCrossingPerimeters::B
visitor.pt_current = &current_point;
if (!next.do_not_remove)
for (size_t point_idx_2 = point_idx + 1; point_idx_2 < travel.size() && !travel[point_idx_2].do_not_remove; ++point_idx_2) {
for (size_t point_idx_2 = point_idx + 1; point_idx_2 < travel.size(); ++point_idx_2) {
// Workaround for some issue in MSVC 19.29.30037 32-bit compiler.
#if defined(_WIN32) && !defined(_WIN64)
if (bool volatile do_not_remove = travel[point_idx_2].do_not_remove; do_not_remove)
break;
#else
if (travel[point_idx_2].do_not_remove)
break;
#endif
if (travel[point_idx_2].point == current_point) {
next = travel[point_idx_2];
point_idx = point_idx_2;

View File

@ -0,0 +1,153 @@
#include "FindReplace.hpp"
#include "../Utils.hpp"
#include <cctype> // isalpha
namespace Slic3r {
// Similar to https://npp-user-manual.org/docs/searching/#extended-search-mode
const void unescape_extended_search_mode(std::string &s)
{
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)
// 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<std::string> &gcode_substitutions)
{
if ((gcode_substitutions.size() % 3) != 0)
throw RuntimeError("Invalid length of gcode_substitutions parameter");
m_substitutions.reserve(gcode_substitutions.size() / 3);
for (size_t i = 0; i < gcode_substitutions.size(); i += 3) {
Substitution out;
try {
out.plain_pattern = gcode_substitutions[i];
out.format = gcode_substitutions[i + 1];
const std::string &params = 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) {
out.regexp_pattern.assign(
out.whole_word ?
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());
}
m_substitutions.emplace_back(std::move(out));
}
}
class ToStringIterator
{
public:
using iterator_category = std::output_iterator_tag;
using value_type = void;
using difference_type = void;
using pointer = void;
using reference = void;
ToStringIterator(std::string &data) : m_data(&data) {}
ToStringIterator& operator=(const char val) {
size_t needs = m_data->size() + 1;
if (m_data->capacity() < needs)
m_data->reserve(next_highest_power_of_2(needs));
m_data->push_back(val);
return *this;
}
ToStringIterator& operator*() { return *this; }
ToStringIterator& operator++() { return *this; }
ToStringIterator operator++(int) { return *this; }
private:
std::string *m_data;
};
template<typename FindFn>
static void find_and_replace_whole_word(std::string &inout, const std::string &match, const std::string &replace, FindFn find_fn)
{
if (! match.empty() && inout.size() >= match.size() && match != replace) {
std::string out;
auto [i, j] = find_fn(inout, 0, match);
size_t k = 0;
for (; i != std::string::npos; std::tie(i, j) = find_fn(inout, i, match)) {
if ((i == 0 || ! std::isalnum(inout[i - 1])) && (j == inout.size() || ! std::isalnum(inout[j]))) {
out.reserve(inout.size());
out.append(inout, k, i - k);
out.append(replace);
i = k = j;
} else
i += match.size();
}
if (k > 0) {
out.append(inout, k, inout.size() - k);
inout.swap(out);
}
}
}
std::string GCodeFindReplace::process_layer(const std::string &ain)
{
std::string out;
const std::string *in = &ain;
std::string temp;
temp.reserve(in->size());
for (const Substitution &substitution : m_substitutions) {
if (substitution.regexp) {
temp.clear();
temp.reserve(in->size());
boost::regex_replace(ToStringIterator(temp), in->begin(), in->end(),
substitution.regexp_pattern, substitution.format, boost::match_default | boost::match_not_dot_newline | boost::match_not_dot_null | boost::format_all);
std::swap(out, temp);
} else {
if (in == &ain)
out = ain;
// Plain substitution
if (substitution.case_insensitive) {
if (substitution.whole_word)
find_and_replace_whole_word(out, substitution.plain_pattern, substitution.format,
[](const std::string &str, size_t start_pos, const std::string &match) {
auto begin = str.begin() + start_pos;
boost::iterator_range<std::string::const_iterator> r1(begin, str.end());
boost::iterator_range<std::string::const_iterator> r2(match.begin(), match.end());
auto res = boost::ifind_first(r1, r2);
return res ? std::make_pair(size_t(res.begin() - str.begin()), size_t(res.end() - str.begin())) : std::make_pair(std::string::npos, std::string::npos);
});
else
boost::ireplace_all(out, substitution.plain_pattern, substitution.format);
} else {
if (substitution.whole_word)
find_and_replace_whole_word(out, substitution.plain_pattern, substitution.format,
[](const std::string &str, size_t start_pos, const std::string &match) {
size_t pos = str.find(match, start_pos);
return std::make_pair(pos, pos + (pos == std::string::npos ? 0 : match.size()));
});
else
boost::replace_all(out, substitution.plain_pattern, substitution.format);
}
}
in = &out;
}
return out;
}
}

View File

@ -0,0 +1,33 @@
#ifndef slic3r_FindReplace_hpp_
#define slic3r_FindReplace_hpp_
#include "../PrintConfig.hpp"
#include <boost/regex.hpp>
namespace Slic3r {
class GCodeFindReplace {
public:
GCodeFindReplace(const PrintConfig &print_config) : GCodeFindReplace(print_config.gcode_substitutions.values) {}
GCodeFindReplace(const std::vector<std::string> &gcode_substitutions);
std::string process_layer(const std::string &gcode);
private:
struct Substitution {
std::string plain_pattern;
boost::regex regexp_pattern;
std::string format;
bool regexp { false };
bool case_insensitive { false };
bool whole_word { false };
};
std::vector<Substitution> m_substitutions;
};
}
#endif // slic3r_FindReplace_hpp_

View File

@ -439,7 +439,7 @@ static std::vector<std::string> s_Preset_print_options {
"support_material_interface_pattern", "support_material_interface_spacing", "support_material_interface_contact_loops",
"support_material_contact_distance", "support_material_bottom_contact_distance",
"support_material_buildplate_only", "dont_support_bridges", "thick_bridges", "notes", "complete_objects", "extruder_clearance_radius",
"extruder_clearance_height", "gcode_comments", "gcode_label_objects", "output_filename_format", "post_process", "perimeter_extruder",
"extruder_clearance_height", "gcode_comments", "gcode_label_objects", "output_filename_format", "post_process", "gcode_substitutions", "perimeter_extruder",
"infill_extruder", "solid_infill_extruder", "support_material_extruder", "support_material_interface_extruder",
"ooze_prevention", "standby_temperature_delta", "interface_shells", "extrusion_width", "first_layer_extrusion_width",
"perimeter_extrusion_width", "external_perimeter_extrusion_width", "infill_extrusion_width", "solid_infill_extrusion_width",
@ -1133,6 +1133,20 @@ void add_correct_opts_to_diff(const std::string &opt_key, t_config_option_keys&
}
}
// list of options with vector variable, which is independent from number of extruders
static const std::vector<std::string> independent_from_extruder_number_options = {
"bed_shape",
"filament_ramming_parameters",
"gcode_substitutions",
"compatible_prints",
"compatible_printers"
};
bool PresetCollection::is_independent_from_extruder_number_option(const std::string& opt_key)
{
return std::find(independent_from_extruder_number_options.begin(), independent_from_extruder_number_options.end(), opt_key) != independent_from_extruder_number_options.end();
}
// Use deep_diff to correct return of changed options, considering individual options for each extruder.
inline t_config_option_keys deep_diff(const ConfigBase &config_this, const ConfigBase &config_other)
{
@ -1142,7 +1156,7 @@ inline t_config_option_keys deep_diff(const ConfigBase &config_this, const Confi
const ConfigOption *other_opt = config_other.option(opt_key);
if (this_opt != nullptr && other_opt != nullptr && *this_opt != *other_opt)
{
if (opt_key == "bed_shape" || opt_key == "thumbnails" || opt_key == "compatible_prints" || opt_key == "compatible_printers") {
if (PresetCollection::is_independent_from_extruder_number_option(opt_key)) {
// Scalar variable, or a vector variable, which is independent from number of extruders,
// thus the vector is presented to the user as a single input.
diff.emplace_back(opt_key);

View File

@ -558,6 +558,7 @@ private:
public:
static bool is_dirty(const Preset *edited, const Preset *reference);
static std::vector<std::string> dirty_options(const Preset *edited, const Preset *reference, const bool deep_compare = false);
static bool is_independent_from_extruder_number_option(const std::string& opt_key);
private:
// Type of this PresetCollection: TYPE_PRINT, TYPE_FILAMENT or TYPE_PRINTER.
Preset::Type m_type;

View File

@ -110,6 +110,7 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n
"output_filename_format",
"perimeter_acceleration",
"post_process",
"gcode_substitutions",
"printer_notes",
"retract_before_travel",
"retract_before_wipe",

View File

@ -1357,6 +1357,12 @@ void PrintConfigDef::init_fff_params()
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionBool(0));
def = this->add("gcode_substitutions", coStrings);
def->label = L("G-code substitutions");
def->tooltip = L("Find / replace patterns in G-code lines and substitute them.");
def->mode = comExpert;
def->set_default_value(new ConfigOptionStrings());
def = this->add("high_current_on_filament_swap", coBool);
def->label = L("High extruder current on filament swap");
def->tooltip = L("It may be beneficial to increase the extruder motor current during the filament exchange"

View File

@ -641,6 +641,12 @@ PRINT_CONFIG_CLASS_DEFINE(
((ConfigOptionBool, gcode_comments))
((ConfigOptionEnum<GCodeFlavor>, gcode_flavor))
((ConfigOptionBool, gcode_label_objects))
// Triples of strings: "search pattern", "replace with pattern", "attribs"
// where "attribs" are one of:
// r - regular expression
// i - case insensitive
// w - whole word
((ConfigOptionStrings, gcode_substitutions))
((ConfigOptionString, layer_gcode))
((ConfigOptionFloat, max_print_speed))
((ConfigOptionFloat, max_volumetric_speed))

View File

@ -143,7 +143,7 @@ void change_opt_value(DynamicPrintConfig& config, const t_config_option_key& opt
config.set_key_value(opt_key, new ConfigOptionString(boost::any_cast<std::string>(value)));
break;
case coStrings:{
if (opt_key == "compatible_prints" || opt_key == "compatible_printers") {
if (opt_key == "compatible_prints" || opt_key == "compatible_printers" || opt_key == "gcode_substitutions") {
config.option<ConfigOptionStrings>(opt_key)->values =
boost::any_cast<std::vector<std::string>>(value);
}

View File

@ -15,6 +15,7 @@
#include "libslic3r/Exception.hpp"
#include "libslic3r/Utils.hpp"
#include "libslic3r/AppConfig.hpp"
#include "libslic3r/Preset.hpp"
#include "I18N.hpp"
namespace Slic3r { namespace GUI {
@ -596,8 +597,7 @@ void ConfigOptionsGroup::back_to_config_value(const DynamicPrintConfig& config,
}
else if (m_opt_map.find(opt_key) == m_opt_map.end() ||
// This option don't have corresponded field
opt_key == "bed_shape" || opt_key == "filament_ramming_parameters" ||
opt_key == "compatible_printers" || opt_key == "compatible_prints" ) {
PresetCollection::is_independent_from_extruder_number_option(opt_key) ) {
value = get_config_value(config, opt_key);
this->change_opt_value(opt_key, value);
OptionsGroup::on_change_OG(opt_key, value);
@ -876,7 +876,7 @@ boost::any ConfigOptionsGroup::get_config_value(const DynamicPrintConfig& config
ret = from_u8(config.opt_string(opt_key));
break;
case coStrings:
if (opt_key == "compatible_printers" || opt_key == "compatible_prints") {
if (opt_key == "compatible_printers" || opt_key == "compatible_prints" || opt_key == "gcode_substitutions") {
ret = config.option<ConfigOptionStrings>(opt_key)->values;
break;
}

View File

@ -76,7 +76,8 @@ PrintHostSendDialog::PrintHostSendDialog(const fs::path &path, PrintHostPostUplo
txt_filename->SetValue(recent_path);
txt_filename->SetFocus();
m_valid_suffix = recent_path.substr(recent_path.find_last_of('.'));
if (size_t extension_start = recent_path.find_last_of('.'); extension_start != std::string::npos)
m_valid_suffix = recent_path.substr(extension_start);
// .gcode suffix control
auto validate_path = [this](const wxString &path) -> bool {
if (! path.Lower().EndsWith(m_valid_suffix.Lower())) {

View File

@ -479,8 +479,7 @@ void Tab::update_label_colours()
else
color = &m_modified_label_clr;
}
if (opt.first == "bed_shape" || opt.first == "filament_ramming_parameters" ||
opt.first == "compatible_prints" || opt.first == "compatible_printers" ) {
if (PresetCollection::is_independent_from_extruder_number_option(opt.first)) {
if (Line* line = get_line(opt.first))
line->set_label_colour(color);
continue;
@ -521,8 +520,7 @@ void Tab::decorate()
Field* field = nullptr;
bool option_without_field = false;
if (opt.first == "bed_shape" || opt.first == "filament_ramming_parameters" ||
opt.first == "compatible_prints" || opt.first == "compatible_printers")
if(PresetCollection::is_independent_from_extruder_number_option(opt.first))
option_without_field = true;
if (!option_without_field) {
@ -812,6 +810,12 @@ void Tab::on_roll_back_value(const bool to_sys /*= true*/)
if ((m_options_list["filament_ramming_parameters"] & os) == 0)
to_sys ? group->back_to_sys_value("filament_ramming_parameters") : group->back_to_initial_value("filament_ramming_parameters");
}
if (group->title == "G-code Substitutions") {
if ((m_options_list["gcode_substitutions"] & os) == 0) {
to_sys ? group->back_to_sys_value("gcode_substitutions") : group->back_to_initial_value("gcode_substitutions");
load_key_value("gcode_substitutions", true/*some value*/, true);
}
}
if (group->title == "Profile dependencies") {
// "compatible_printers" option doesn't exists in Printer Settimgs Tab
if (m_type != Preset::TYPE_PRINTER && (m_options_list["compatible_printers"] & os) == 0) {
@ -906,7 +910,7 @@ void Tab::update_visibility()
page->update_visibility(m_mode, page.get() == m_active_page);
rebuild_page_tree();
if (m_type == Preset::TYPE_SLA_PRINT)
if (m_type == Preset::TYPE_SLA_PRINT || m_type == Preset::TYPE_PRINT)
update_description_lines();
Layout();
@ -1655,6 +1659,18 @@ void TabPrint::build()
option.opt.height = 5;//50;
optgroup->append_single_option_line(option);
optgroup = page->new_optgroup(L("G-code Substitutions"));
create_line_with_widget(optgroup.get(), "gcode_substitutions", "", [this](wxWindow* parent) {
return create_manage_substitution_widget(parent);
});
line = { "", "" };
line.full_width = 1;
line.widget = [this](wxWindow* parent) {
return create_substitutions_widget(parent);
};
optgroup->append_line(line);
page = add_options_page(L("Notes"), "note.png");
optgroup = page->new_optgroup(L("Notes"), 0);
option = optgroup->get_option("notes");
@ -1699,13 +1715,21 @@ void TabPrint::update_description_lines()
from_u8(PresetHints::top_bottom_shell_thickness_explanation(*m_preset_bundle)));
}
if (m_active_page && m_active_page->title() == "Output options" && m_post_process_explanation) {
if (m_active_page && m_active_page->title() == "Output options") {
if (m_post_process_explanation) {
m_post_process_explanation->SetText(
_L("Post processing scripts shall modify G-code file in place."));
#ifndef __linux__
m_post_process_explanation->SetPathEnd("post-processing-scripts_283913");
#endif // __linux__
}
// upadte G-code substitutions from the current configuration
{
m_subst_manager.update_from_config();
if (m_del_all_substitutions_btn)
m_del_all_substitutions_btn->Show(!m_subst_manager.is_empty_substitutions());
}
}
}
void TabPrint::toggle_options()
@ -3824,6 +3848,244 @@ wxSizer* Tab::compatible_widget_create(wxWindow* parent, PresetDependencies &dep
return sizer;
}
// G-code substitutions
void SubstitutionManager::init(DynamicPrintConfig* config, wxWindow* parent, wxFlexGridSizer* grid_sizer)
{
m_config = config;
m_parent = parent;
m_grid_sizer = grid_sizer;
m_em = em_unit(parent);
}
void SubstitutionManager::create_legend()
{
if (!m_grid_sizer->IsEmpty())
return;
// name of the first column is empty
m_grid_sizer->Add(new wxStaticText(m_parent, wxID_ANY, wxEmptyString));
// Legend for another columns
for (const std::string col : { L("Plain pattern"), L("Format"), L("Params") }) {
auto temp = new wxStaticText(m_parent, wxID_ANY, _(col), wxDefaultPosition, wxDefaultSize, wxST_ELLIPSIZE_MIDDLE);
// temp->SetBackgroundStyle(wxBG_STYLE_PAINT);
m_grid_sizer->Add(temp);
}
}
// delete substitution_id from substitutions
void SubstitutionManager::delete_substitution(int substitution_id)
{
// delete substitution
std::vector<std::string>& substitutions = m_config->option<ConfigOptionStrings>("gcode_substitutions")->values;
if ((substitutions.size() % 3) != 0)
throw RuntimeError("Invalid length of gcode_substitutions parameter");
if ((substitutions.size() / 3) < substitution_id)
throw RuntimeError("Invalid substitution_id to delete");
substitutions.erase(std::next(substitutions.begin(), substitution_id * 3), std::next(substitutions.begin(), substitution_id * 3 + 3));
call_ui_update();
// update grid_sizer
update_from_config();
}
// Add substitution line
void SubstitutionManager::add_substitution(int substitution_id, const std::string& plain_pattern, const std::string& format, const std::string& params)
{
bool call_after_layout = false;
if (substitution_id < 0) {
if (m_grid_sizer->IsEmpty()) {
create_legend();
substitution_id = 0;
}
substitution_id = m_grid_sizer->GetEffectiveRowsCount() - 1;
// create new substitution
// it have to be added toconfig too
std::vector<std::string>& substitutions = m_config->option<ConfigOptionStrings>("gcode_substitutions")->values;
for (size_t i = 0; i < 3; i ++)
substitutions.push_back(std::string());
call_after_layout = true;
}
auto del_btn = new ScalableButton(m_parent, wxID_ANY, "cross");
del_btn->Bind(wxEVT_BUTTON, [substitution_id, this](wxEvent&) {
delete_substitution(substitution_id);
});
m_grid_sizer->Add(del_btn, 0, wxRIGHT | wxLEFT, m_em);
auto add_text_editor = [substitution_id, this](const wxString& value, int opt_pos) {
auto editor = new wxTextCtrl(m_parent, wxID_ANY, value, wxDefaultPosition, wxSize(15 * m_em, wxDefaultCoord), wxTE_PROCESS_ENTER
#ifdef _WIN32
| wxBORDER_SIMPLE
#endif
);
editor->SetFont(wxGetApp().normal_font());
wxGetApp().UpdateDarkUI(editor);
m_grid_sizer->Add(editor, 0, wxALIGN_CENTER_VERTICAL);
editor->Bind(wxEVT_TEXT_ENTER, [this, editor, substitution_id, opt_pos](wxEvent& e) {
#if !defined(__WXGTK__)
e.Skip();
#endif // __WXGTK__
edit_substitution(substitution_id, opt_pos, into_u8(editor->GetValue()));
});
editor->Bind(wxEVT_KILL_FOCUS, [this, editor, substitution_id, opt_pos](wxEvent& e) {
e.Skip();
edit_substitution(substitution_id, opt_pos, into_u8(editor->GetValue()));
});
};
add_text_editor(from_u8(plain_pattern), 0);
add_text_editor(from_u8(format), 1);
auto params_sizer = new wxBoxSizer(wxHORIZONTAL);
bool regexp = strchr(params.c_str(), 'r') != nullptr || strchr(params.c_str(), 'R') != nullptr;
bool case_insensitive = strchr(params.c_str(), 'i') != nullptr || strchr(params.c_str(), 'I') != nullptr;
bool whole_word = strchr(params.c_str(), 'w') != nullptr || strchr(params.c_str(), 'W') != nullptr;
auto chb_regexp = new wxCheckBox(m_parent, wxID_ANY, wxString("Regular expression"));
chb_regexp->SetValue(regexp);
params_sizer->Add(chb_regexp, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, m_em);
auto chb_case_insensitive = new wxCheckBox(m_parent, wxID_ANY, wxString("Case insensitive"));
chb_case_insensitive->SetValue(case_insensitive);
params_sizer->Add(chb_case_insensitive, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT | wxLEFT, m_em);
auto chb_whole_word = new wxCheckBox(m_parent, wxID_ANY, wxString("Whole word"));
chb_whole_word->SetValue(whole_word);
params_sizer->Add(chb_whole_word, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT | wxLEFT, m_em);
for (wxCheckBox* chb : std::initializer_list<wxCheckBox*>{ chb_regexp, chb_case_insensitive, chb_whole_word }) {
chb->SetFont(wxGetApp().normal_font());
chb->Bind(wxEVT_CHECKBOX, [this, substitution_id, chb_regexp, chb_case_insensitive, chb_whole_word](wxCommandEvent e) {
std::string value = std::string();
if (chb_regexp->GetValue())
value += "r";
if (chb_case_insensitive->GetValue())
value += "i";
if (chb_whole_word->GetValue())
value += "w";
edit_substitution(substitution_id, 2, value);
});
}
m_grid_sizer->Add(params_sizer);
if (call_after_layout) {
m_parent->GetParent()->Layout();
call_ui_update();
}
}
void SubstitutionManager::update_from_config()
{
if (!m_grid_sizer->IsEmpty())
m_grid_sizer->Clear(true);
std::vector<std::string>& subst = m_config->option<ConfigOptionStrings>("gcode_substitutions")->values;
if (!subst.empty())
create_legend();
if ((subst.size() % 3) != 0)
throw RuntimeError("Invalid length of gcode_substitutions parameter");
int subst_id = 0;
for (size_t i = 0; i < subst.size(); i += 3)
add_substitution(subst_id++, subst[i], subst[i + 1], subst[i + 2]);
m_parent->GetParent()->Layout();
}
void SubstitutionManager::delete_all()
{
m_config->option<ConfigOptionStrings>("gcode_substitutions")->values.clear();
call_ui_update();
if (!m_grid_sizer->IsEmpty())
m_grid_sizer->Clear(true);
m_parent->GetParent()->Layout();
}
void SubstitutionManager::edit_substitution(int substitution_id, int opt_pos, const std::string& value)
{
std::vector<std::string>& substitutions = m_config->option<ConfigOptionStrings>("gcode_substitutions")->values;
if ((substitutions.size() % 3) != 0)
throw RuntimeError("Invalid length of gcode_substitutions parameter");
if ((substitutions.size() / 3) != m_grid_sizer->GetEffectiveRowsCount()-1)
throw RuntimeError("Invalid compatibility between UI and BE");
if ((substitutions.size() / 3) < substitution_id)
throw RuntimeError("Invalid substitution_id to edit");
substitutions[substitution_id * 3 + opt_pos] = value;
call_ui_update();
}
bool SubstitutionManager::is_empty_substitutions()
{
return m_config->option<ConfigOptionStrings>("gcode_substitutions")->values.empty();
}
// Return a callback to create a TabPrint widget to edit G-code substitutions
wxSizer* TabPrint::create_manage_substitution_widget(wxWindow* parent)
{
auto create_btn = [parent](ScalableButton** btn, const wxString& label, const std::string& icon_name) {
*btn = new ScalableButton(parent, wxID_ANY, icon_name, " " + label + " ", wxDefaultSize, wxDefaultPosition, wxBU_LEFT | wxBU_EXACTFIT, true);
(*btn)->SetFont(wxGetApp().normal_font());
(*btn)->SetSize((*btn)->GetBestSize());
};
ScalableButton* add_substitution_btn;
create_btn(&add_substitution_btn, _L("Add G-code substitution"), "add_copies");
add_substitution_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent e) {
m_subst_manager.add_substitution();
m_del_all_substitutions_btn->Show();
});
create_btn(&m_del_all_substitutions_btn, _L("Delete all G-code substitution"), "cross");
m_del_all_substitutions_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent e) {
m_subst_manager.delete_all();
m_del_all_substitutions_btn->Hide();
});
auto sizer = new wxBoxSizer(wxHORIZONTAL);
sizer->Add(add_substitution_btn, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT | wxLEFT, em_unit(parent));
sizer->Add(m_del_all_substitutions_btn, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT | wxLEFT, em_unit(parent));
parent->GetParent()->Layout();
return sizer;
}
// Return a callback to create a TabPrint widget to edit G-code substitutions
wxSizer* TabPrint::create_substitutions_widget(wxWindow* parent)
{
wxFlexGridSizer* grid_sizer = new wxFlexGridSizer(4, 5, wxGetApp().em_unit()); // delete_button, "Old val", "New val", "Params"
grid_sizer->SetFlexibleDirection(wxHORIZONTAL);
m_subst_manager.init(m_config, parent, grid_sizer);
m_subst_manager.set_cb_edited_substitution([this]() {
update_dirty();
wxGetApp().mainframe->on_config_changed(m_config); // invalidate print
});
auto sizer = new wxBoxSizer(wxHORIZONTAL);
sizer->Add(grid_sizer, 0, wxALIGN_CENTER_VERTICAL);
parent->GetParent()->Layout();
return sizer;
}
// Return a callback to create a TabPrinter widget to edit bed shape
wxSizer* TabPrinter::create_bed_shape_widget(wxWindow* parent)
{

View File

@ -43,6 +43,44 @@ namespace GUI {
class TabPresetComboBox;
class OG_CustomCtrl;
// G-code substitutions
// Substitution Manager - helper for manipuation of the substitutions
class SubstitutionManager
{
DynamicPrintConfig* m_config{ nullptr };
wxWindow* m_parent{ nullptr };
wxFlexGridSizer* m_grid_sizer{ nullptr };
int m_em{10};
std::function<void()> m_cb_edited_substitution{ nullptr };
public:
SubstitutionManager() {};
~SubstitutionManager() {};
void init(DynamicPrintConfig* config, wxWindow* parent, wxFlexGridSizer* grid_sizer);
void create_legend();
void delete_substitution(int substitution_id);
void add_substitution( int substitution_id = -1,
const std::string& plain_pattern = std::string(),
const std::string& format = std::string(),
const std::string& params = std::string());
void update_from_config();
void delete_all();
void edit_substitution(int substitution_id,
int opt_pos, // option position insubstitution [0, 2]
const std::string& value);
void set_cb_edited_substitution(std::function<void()> cb_edited_substitution) {
m_cb_edited_substitution = cb_edited_substitution;
}
void call_ui_update() {
if (m_cb_edited_substitution)
m_cb_edited_substitution();
}
bool is_empty_substitutions();
};
// Single Tab page containing a{ vsizer } of{ optgroups }
// package Slic3r::GUI::Tab::Page;
using ConfigOptionsGroupShp = std::shared_ptr<ConfigOptionsGroup>;
@ -368,11 +406,15 @@ public:
void update() override;
void clear_pages() override;
bool supports_printer_technology(const PrinterTechnology tech) const override { return tech == ptFFF; }
wxSizer* create_manage_substitution_widget(wxWindow* parent);
wxSizer* create_substitutions_widget(wxWindow* parent);
private:
ogStaticText* m_recommended_thin_wall_thickness_description_line = nullptr;
ogStaticText* m_top_bottom_shell_thickness_explanation = nullptr;
ogStaticText* m_post_process_explanation = nullptr;
ScalableButton* m_del_all_substitutions_btn{nullptr};
SubstitutionManager m_subst_manager;
};
class TabFilament : public Tab

View File

@ -1156,6 +1156,14 @@ static wxString get_string_value(std::string opt_key, const DynamicPrintConfig&
out.RemoveLast(1);
return out;
}
if (opt_key == "gcode_substitutions") {
if (!strings->empty())
for (size_t id = 0; id < strings->size(); id += 3)
out += from_u8(strings->get_at(id)) + ";\t" +
from_u8(strings->get_at(id + 1)) + ";\t" +
from_u8(strings->get_at(id + 2)) + ";\n";
return out;
}
if (!strings->empty() && opt_idx < strings->values.size())
return from_u8(strings->get_at(opt_idx));
}

View File

@ -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

View File

@ -0,0 +1,218 @@
#include <catch2/catch.hpp>
#include <memory>
#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");
}
}
}