diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index e841a5c43..65549d2e8 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -1086,6 +1086,8 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato m_placeholder_parser = print.placeholder_parser(); m_placeholder_parser.update_timestamp(); m_placeholder_parser_context.rng = std::mt19937(std::chrono::high_resolution_clock::now().time_since_epoch().count()); + // Enable passing global variables between PlaceholderParser invocations. + m_placeholder_parser_context.global_config = std::make_unique(); print.update_object_placeholders(m_placeholder_parser.config_writable(), ".gcode"); // Get optimal tool ordering to minimize tool switches of a multi-exruder print. diff --git a/src/libslic3r/PlaceholderParser.cpp b/src/libslic3r/PlaceholderParser.cpp index 6ecb52233..78ded40c4 100644 --- a/src/libslic3r/PlaceholderParser.cpp +++ b/src/libslic3r/PlaceholderParser.cpp @@ -191,6 +191,10 @@ namespace client struct expr { expr() {} + expr(const expr &rhs) : m_type(rhs.type()), it_range(rhs.it_range) + { if (rhs.type() == TYPE_STRING) m_data.s = new std::string(*rhs.m_data.s); else m_data.set(rhs.m_data); } + expr(expr &&rhs) : expr(std::move(rhs), rhs.it_range.begin(), rhs.it_range.end()) {} + explicit expr(bool b) : m_type(TYPE_BOOL) { m_data.b = b; } explicit expr(bool b, const Iterator &it_begin, const Iterator &it_end) : m_type(TYPE_BOOL), it_range(it_begin, it_end) { m_data.b = b; } explicit expr(int i) : m_type(TYPE_INT) { m_data.i = i; } @@ -201,11 +205,8 @@ namespace client explicit expr(const std::string &s) : m_type(TYPE_STRING) { m_data.s = new std::string(s); } explicit expr(const std::string &s, const Iterator &it_begin, const Iterator &it_end) : m_type(TYPE_STRING), it_range(it_begin, it_end) { m_data.s = new std::string(s); } - expr(const expr &rhs) : m_type(rhs.type()), it_range(rhs.it_range) - { if (rhs.type() == TYPE_STRING) m_data.s = new std::string(*rhs.m_data.s); else m_data.set(rhs.m_data); } - explicit expr(expr &&rhs) : expr(rhs, rhs.it_range.begin(), rhs.it_range.end()) {} explicit expr(expr &&rhs, const Iterator &it_begin, const Iterator &it_end) : m_type(rhs.type()), it_range{ it_begin, it_end } - { + { m_data.set(rhs.m_data); rhs.m_type = TYPE_EMPTY; } @@ -723,7 +724,10 @@ namespace client const DynamicConfig *config = nullptr; const DynamicConfig *config_override = nullptr; mutable DynamicConfig *config_outputs = nullptr; + // Local variables, read / write + mutable DynamicConfig config_local; size_t current_extruder_id = 0; + // Random number generator and optionally global variables. PlaceholderParser::ContextData *context_data = nullptr; // If false, the macro_processor will evaluate a full macro. // If true, the macro processor will evaluate just a boolean condition using the full expressive power of the macro processor. @@ -748,7 +752,24 @@ namespace client } const ConfigOption* resolve_symbol(const std::string &opt_key) const { return this->optptr(opt_key); } - ConfigOption* resolve_output_symbol(const std::string &opt_key) const { return this->config_outputs ? this->config_outputs->optptr(opt_key, false) : nullptr; } + ConfigOption* resolve_output_symbol(const std::string &opt_key) const { + ConfigOption *out = nullptr; + if (this->config_outputs) + out = this->config_outputs->optptr(opt_key, false); + if (out == nullptr && this->context_data != nullptr && this->context_data->global_config) + out = this->context_data->global_config->optptr(opt_key); + if (out == nullptr) + out = this->config_local.optptr(opt_key); + return out; + } + void store_new_variable(const std::string &opt_key, ConfigOption *opt, bool global_variable) { + assert(opt != nullptr); + if (global_variable) { + assert(this->context_data != nullptr && this->context_data->global_config); + this->context_data->global_config->set_key_value(opt_key, opt); + } else + this->config_local.set_key_value(opt_key ,opt); + } template static void legacy_variable_expansion( @@ -953,17 +974,101 @@ namespace client output.it_range = opt.it_range; } - // Decoding a scalar variable symbol "opt", assigning it a value of "param". + template + struct NewOldVariable { + std::string name; + boost::iterator_range it_range; + ConfigOption *opt{ nullptr }; + }; template - static void variable_assign( - const MyContext *ctx, - OptWithPos &opt, - expr ¶m, - // Not used, just clear it. - std::string &out) + static void new_old_variable( + const MyContext *ctx, + bool global_variable, + const boost::iterator_range &it_range, + NewOldVariable &out) { + t_config_option_key key(std::string(it_range.begin(), it_range.end())); + if (const ConfigOption* opt = ctx->resolve_symbol(key); opt) + ctx->throw_exception("Symbol is already defined in read-only system dictionary", it_range); + if (ctx->config_outputs && ctx->config_outputs->optptr(key)) + ctx->throw_exception("Symbol is already defined as system output variable", it_range); + + bool has_global_dictionary = ctx->context_data != nullptr && ctx->context_data->global_config; + if (global_variable) { + if (! has_global_dictionary) + ctx->throw_exception("Global variables are not available in this context", it_range); + if (ctx->config_local.optptr(key)) + ctx->throw_exception("Variable name already defined in local scope", it_range); + out.opt = ctx->context_data->global_config->optptr(key); + } else { + if (has_global_dictionary && ctx->context_data->global_config->optptr(key)) + ctx->throw_exception("Variable name already defined in global scope", it_range); + out.opt = ctx->config_local.optptr(key); + } + + out.name = std::move(key); + out.it_range = it_range; + } + + template + static void new_scalar_variable( + const MyContext *ctx, + bool global_variable, + NewOldVariable &output_variable, + const expr ¶m) + { + auto check_numeric = [](const expr ¶m) { + if (! param.numeric_type()) + param.throw_exception("Right side is not a numeric expression"); + }; + if (output_variable.opt) { + if (output_variable.opt->is_vector()) + param.throw_exception("Cannot assign a scalar value to a vector variable."); + switch (output_variable.opt->type()) { + case coFloat: + check_numeric(param); + static_cast(output_variable.opt)->value = param.as_d(); + break; + case coInt: + check_numeric(param); + static_cast(output_variable.opt)->value = param.as_i(); + break; + case coString: + static_cast(output_variable.opt)->value = param.to_string(); + break; + case coBool: + if (param.type() != expr::TYPE_BOOL) + param.throw_exception("Right side is not a boolean expression"); + static_cast(output_variable.opt)->value = param.b(); + break; + default: assert(false); + } + } else { + switch (param.type()) { + case expr::TYPE_BOOL: output_variable.opt = new ConfigOptionBool(param.b()); break; + case expr::TYPE_INT: output_variable.opt = new ConfigOptionInt(param.i()); break; + case expr::TYPE_DOUBLE: output_variable.opt = new ConfigOptionFloat(param.d()); break; + case expr::TYPE_STRING: output_variable.opt = new ConfigOptionString(param.s()); break; + default: assert(false); + } + const_cast(ctx)->store_new_variable(output_variable.name, output_variable.opt, global_variable); + } + } + + template + static void check_writable(const MyContext *ctx, OptWithPos &opt) { if (! opt.writable) ctx->throw_exception("Cannot modify a read-only variable", opt.it_range); + } + + // Decoding a scalar variable symbol "opt", assigning it a value of "param". + template + static void assign_scalar_variable( + const MyContext *ctx, + OptWithPos &opt, + expr ¶m) + { + check_writable(ctx, opt); auto check_numeric = [](const expr ¶m) { if (! param.numeric_type()) param.throw_exception("Right side is not a numeric expression"); @@ -1028,7 +1133,363 @@ namespace client ctx->throw_exception("Unsupported output scalar variable type", opt.it_range); } } - out.clear(); + } + + template + static void new_vector_variable_array( + const MyContext *ctx, + bool global_variable, + NewOldVariable &output_variable, + const expr &expr_count, + const expr &expr_value) + { + auto check_numeric = [](const expr ¶m) { + if (! param.numeric_type()) + param.throw_exception("Right side is not a numeric expression"); + }; + auto evaluate_count = [](const expr &expr_count) -> size_t { + if (expr_count.type() != expr::TYPE_INT) + expr_count.throw_exception("Expected number of elements to fill a vector with."); + int count = expr_count.i(); + if (count < 0) + expr_count.throw_exception("Negative number of elements specified."); + return size_t(count); + }; + if (output_variable.opt) { + if (output_variable.opt->is_scalar()) + expr_value.throw_exception("Cannot assign a vector value to a scalar variable."); + size_t count = evaluate_count(expr_count); + switch (output_variable.opt->type()) { + case coFloats: + check_numeric(expr_value); + static_cast(output_variable.opt)->values.assign(count, expr_value.as_d()); + break; + case coInts: + check_numeric(expr_value); + static_cast(output_variable.opt)->values.assign(count, expr_value.as_i()); + break; + case coStrings: + static_cast(output_variable.opt)->values.assign(count, expr_value.to_string()); + break; + case coBools: + if (expr_value.type() != expr::TYPE_BOOL) + expr_value.throw_exception("Right side is not a boolean expression"); + static_cast(output_variable.opt)->values.assign(count, expr_value.b()); + break; + default: assert(false); + } + } else { + size_t count = evaluate_count(expr_count); + switch (expr_value.type()) { + case expr::TYPE_BOOL: output_variable.opt = new ConfigOptionBools(count, expr_value.b()); break; + case expr::TYPE_INT: output_variable.opt = new ConfigOptionInts(count, expr_value.i()); break; + case expr::TYPE_DOUBLE: output_variable.opt = new ConfigOptionFloats(count, expr_value.d()); break; + case expr::TYPE_STRING: output_variable.opt = new ConfigOptionStrings(count, expr_value.s()); break; + default: assert(false); + } + const_cast(ctx)->store_new_variable(output_variable.name, output_variable.opt, global_variable); + } + } + + template + static void assign_vector_variable_array( + const MyContext *ctx, + OptWithPos &lhs, + const expr &expr_count, + const expr &expr_value) + { + check_writable(ctx, lhs); + auto check_numeric = [](const expr ¶m) { + if (! param.numeric_type()) + param.throw_exception("Right side is not a numeric expression"); + }; + auto evaluate_count = [](const expr &expr_count) -> size_t { + if (expr_count.type() != expr::TYPE_INT) + expr_count.throw_exception("Expected number of elements to fill a vector with."); + int count = expr_count.i(); + if (count < 0) + expr_count.throw_exception("Negative number of elements specified."); + return size_t(count); + }; + if (lhs.opt->is_scalar()) + expr_value.throw_exception("Cannot assign a vector value to a scalar variable."); + auto *opt = const_cast(lhs.opt); + size_t count = evaluate_count(expr_count); + switch (lhs.opt->type()) { + case coFloats: + check_numeric(expr_value); + static_cast(opt)->values.assign(count, expr_value.as_d()); + break; + case coInts: + check_numeric(expr_value); + static_cast(opt)->values.assign(count, expr_value.as_i()); + break; + case coStrings: + static_cast(opt)->values.assign(count, expr_value.to_string()); + break; + case coBools: + if (expr_value.type() != expr::TYPE_BOOL) + expr_value.throw_exception("Right side is not a boolean expression"); + static_cast(opt)->values.assign(count, expr_value.b()); + break; + default: assert(false); + } + } + + template + static void new_vector_variable_initializer_list( + const MyContext *ctx, + bool global_variable, + NewOldVariable &output_variable, + const std::vector> &il) + { + if (! output_variable.opt) { + // First guesstimate type of the output vector. + size_t num_bool = 0; + size_t num_int = 0; + size_t num_double = 0; + size_t num_string = 0; + for (auto &i : il) + switch (i.type()) { + case expr::TYPE_BOOL: ++ num_bool; break; + case expr::TYPE_INT: ++ num_int; break; + case expr::TYPE_DOUBLE: ++ num_double; break; + case expr::TYPE_STRING: ++ num_string; break; + default: assert(false); + } + if (num_string > 0) + // Convert everything to strings. + output_variable.opt = new ConfigOptionStrings(); + else if (num_bool > 0) { + if (num_double + num_int > 0) + ctx->throw_exception("Right side is not valid: Mixing numeric and boolean types.", boost::iterator_range{ il.front().it_range.begin(), il.back().it_range.end() }); + output_variable.opt = new ConfigOptionBools(); + } else + // Output is numeric. + output_variable.opt = num_double == 0 ? static_cast(new ConfigOptionInts()) : static_cast(new ConfigOptionFloats()); + const_cast(ctx)->store_new_variable(output_variable.name, output_variable.opt, global_variable); + } + + auto check_numeric = [](const std::vector> &il) { + for (auto& i : il) + if (!i.numeric_type()) + i.throw_exception("Right side is not a numeric expression"); + }; + + if (output_variable.opt->is_scalar()) + ctx->throw_exception("Cannot assign a vector value to a scalar variable.", output_variable.it_range); + + switch (output_variable.opt->type()) { + case coFloats: + { + check_numeric(il); + auto &out = static_cast(output_variable.opt)->values; + out.clear(); + out.reserve(il.size()); + for (auto &i : il) + out.emplace_back(i.as_d()); + break; + } + case coInts: + { + check_numeric(il); + auto &out = static_cast(output_variable.opt)->values; + out.clear(); + out.reserve(il.size()); + for (auto& i : il) + out.emplace_back(i.as_i()); + break; + } + case coStrings: + { + auto &out = static_cast(output_variable.opt)->values; + out.clear(); + out.reserve(il.size()); + for (auto &i : il) + out.emplace_back(i.to_string()); + break; + } + case coBools: + { + auto &out = static_cast(output_variable.opt)->values; + out.clear(); + out.reserve(il.size()); + for (auto &i : il) + if (i.type() == expr::TYPE_BOOL) + out.emplace_back(i.b()); + else + i.throw_exception("Right side is not a boolean expression"); + break; + } + default: + assert(false); + } + } + + template + static void assign_vector_variable_initializer_list( + const MyContext *ctx, + OptWithPos &lhs, + const std::vector> &il) + { + check_writable(ctx, lhs); + auto check_numeric = [](const std::vector> &il) { + for (auto &i : il) + if (! i.numeric_type()) + i.throw_exception("Right side is not a numeric expression"); + }; + + if (lhs.opt->is_scalar()) + ctx->throw_exception("Cannot assign a vector value to a scalar variable.", lhs.it_range); + + ConfigOption *opt = const_cast(lhs.opt); + switch (lhs.opt->type()) { + case coFloats: + { + check_numeric(il); + auto &out = static_cast(opt)->values; + out.clear(); + out.reserve(il.size()); + for (auto &i : il) + out.emplace_back(i.as_d()); + break; + } + case coInts: + { + check_numeric(il); + auto &out = static_cast(opt)->values; + out.clear(); + out.reserve(il.size()); + for (auto& i : il) + out.emplace_back(i.as_i()); + break; + } + case coStrings: + { + auto &out = static_cast(opt)->values; + out.clear(); + out.reserve(il.size()); + for (auto &i : il) + out.emplace_back(i.to_string()); + break; + } + case coBools: + { + auto &out = static_cast(opt)->values; + out.clear(); + out.reserve(il.size()); + for (auto &i : il) + if (i.type() == expr::TYPE_BOOL) + out.emplace_back(i.b()); + else + i.throw_exception("Right side is not a boolean expression"); + break; + } + default: + assert(false); + } + } + + template + static bool new_vector_variable_copy( + const MyContext *ctx, + bool global_variable, + NewOldVariable &output_variable, + const OptWithPos &src_variable) + { + if (! is_vector_variable_reference(src_variable)) + // Skip parsing this branch, bactrack. + return false; + + if (! output_variable.opt) { + if (one_of(src_variable.opt->type(), { coFloats, coInts, coStrings, coBools })) + output_variable.opt = src_variable.opt->clone(); + else if (src_variable.opt->type() == coPercents) + output_variable.opt = new ConfigOptionFloats(static_cast(src_variable.opt)->values); + else + ctx->throw_exception("Duplicating this vector variable is not supported", src_variable.it_range); + const_cast(ctx)->store_new_variable(output_variable.name, output_variable.opt, global_variable); + } + + switch (output_variable.opt->type()) { + case coFloats: + if (output_variable.opt->type() != coFloats) + ctx->throw_exception("Left hand side is a float vector, while the right hand side is not.", boost::iterator_range{ output_variable.it_range.begin(), src_variable.it_range.end() }); + static_cast(output_variable.opt)->values = static_cast(src_variable.opt)->values; + break; + case coInts: + if (output_variable.opt->type() != coInts) + ctx->throw_exception("Left hand side is an int vector, while the right hand side is not.", boost::iterator_range{ output_variable.it_range.begin(), src_variable.it_range.end() }); + static_cast(output_variable.opt)->values = static_cast(src_variable.opt)->values; + break; + case coStrings: + if (output_variable.opt->type() != coStrings) + ctx->throw_exception("Left hand side is a string vector, while the right hand side is not.", boost::iterator_range{ output_variable.it_range.begin(), src_variable.it_range.end() }); + static_cast(output_variable.opt)->values = static_cast(src_variable.opt)->values; + break; + case coBools: + if (output_variable.opt->type() != coBools) + ctx->throw_exception("Left hand side is a bool vector, while the right hand side is not.", boost::iterator_range{ output_variable.it_range.begin(), src_variable.it_range.end() }); + static_cast(output_variable.opt)->values = static_cast(src_variable.opt)->values; + break; + default: + assert(false); + } + // Continue parsing. + return true; + } + + template + static bool is_vector_variable_reference(const OptWithPos &var) { + return ! var.has_index() && var.opt->is_vector(); + } + + template + static bool assign_vector_variable_copy( + const MyContext *ctx, + OptWithPos &lhs, + const OptWithPos &src_variable) + { + if (! is_vector_variable_reference(src_variable)) + // Skip parsing this branch, bactrack. + return false; + + check_writable(ctx, lhs); + + auto *opt = const_cast(lhs.opt); + switch (lhs.opt->type()) { + case coFloats: + if (lhs.opt->type() != coFloats) + ctx->throw_exception("Left hand side is a float vector, while the right hand side is not.", lhs.it_range); + static_cast(opt)->values = static_cast(src_variable.opt)->values; + break; + case coInts: + if (lhs.opt->type() != coInts) + ctx->throw_exception("Left hand side is an int vector, while the right hand side is not.", lhs.it_range); + static_cast(opt)->values = static_cast(src_variable.opt)->values; + break; + case coStrings: + if (lhs.opt->type() != coStrings) + ctx->throw_exception("Left hand side is a string vector, while the right hand side is not.", lhs.it_range); + static_cast(opt)->values = static_cast(src_variable.opt)->values; + break; + case coBools: + if (lhs.opt->type() != coBools) + ctx->throw_exception("Left hand side is a bool vector, while the right hand side is not.", lhs.it_range); + static_cast(opt)->values = static_cast(src_variable.opt)->values; + break; + default: + assert(false); + } + + // Continue parsing. + return true; + } + + template + static void new_vector_variable_initializer_list_append(std::vector> &list, expr &expr) + { + list.emplace_back(std::move(expr)); } // Verify that the expression returns an integer, which may be used @@ -1175,6 +1636,7 @@ namespace client // Table to translate symbol tag to a human readable error message. std::map MyContext::tag_to_error_message = { + { "array", "Unknown syntax error" }, { "eoi", "Unknown syntax error" }, { "start", "Unknown syntax error" }, { "text", "Invalid text." }, @@ -1335,7 +1797,7 @@ namespace client // Allow back tracking after '{' in case of a text_block embedded inside a condition. // In that case the inner-most {else} wins and the {if}/{elsif}/{else} shall be paired. // {elsif}/{else} without an {if} will be allowed to back track from the embedded text_block. - | (lit('{') >> macro(_r1) [_val+=_1] > '}') + | (lit('{') >> macro(_r1)[_val+=_1] > *(+lit(';') >> macro(_r1)[_val+=_1]) > *lit(';') > '}') | (lit('[') > legacy_variable_expansion(_r1) [_val+=_1] > ']') ); text_block.name("text_block"); @@ -1351,6 +1813,7 @@ namespace client (kw["if"] > if_else_output(_r1) [_val = _1]) // | (kw["switch"] > switch_output(_r1) [_val = _1]) | (assignment_statement(_r1) [_val = _1]) + | (new_variable_statement(_r1) [_val = _1]) | (additive_expression(_r1) [ px::bind(&expr::to_string2, _1, _val) ]) ; macro.name("macro"); @@ -1445,8 +1908,39 @@ namespace client multiplicative_expression.name("multiplicative_expression"); assignment_statement = - (variable_reference(_r1) >> '=' > additive_expression(_r1)) - [px::bind(&MyContext::variable_assign, _r1, _1, _2, _val)]; + variable_reference(_r1)[_a = _1] >> '=' > + ( // Consumes also '(' conditional_expression ')', that means enclosing an expression into braces makes it a single value vector initializer. + (lit('(') > new_variable_initializer_list(_r1) > ')') + [px::bind(&MyContext::assign_vector_variable_initializer_list, _r1, _a, _1)] + // Process it before conditional_expression, as conditional_expression requires a vector reference to be augmented with an index. + // Only process such variable references, which return a naked vector variable. + | variable_reference(_r1) + [px::ref(qi::_pass) = px::bind(&MyContext::assign_vector_variable_copy, _r1, _a, _1)] + // Would NOT consume '(' conditional_expression ')' because such value was consumed with the expression above. + | conditional_expression(_r1) + [px::bind(&MyContext::assign_scalar_variable, _r1, _a, _1)] + | (kw["array"] > "(" > additive_expression(_r1) > "," > conditional_expression(_r1) > ")") + [px::bind(&MyContext::assign_vector_variable_array, _r1, _a, _1, _2)] + ); + + new_variable_statement = + (kw["local"][_a = false] | kw["global"][_a = true]) > identifier[px::bind(&MyContext::new_old_variable, _r1, _a, _1, _b)] > lit('=') > + ( // Consumes also '(' conditional_expression ')', that means enclosing an expression into braces makes it a single value vector initializer. + (lit('(') > new_variable_initializer_list(_r1) > ')') + [px::bind(&MyContext::new_vector_variable_initializer_list, _r1, _a, _b, _1)] + // Process it before conditional_expression, as conditional_expression requires a vector reference to be augmented with an index. + // Only process such variable references, which return a naked vector variable. + | variable_reference(_r1) + [px::ref(qi::_pass) = px::bind(&MyContext::new_vector_variable_copy, _r1, _a, _b, _1)] + // Would NOT consume '(' conditional_expression ')' because such value was consumed with the expression above. + | conditional_expression(_r1) + [px::bind(&MyContext::new_scalar_variable, _r1, _a, _b, _1)] + | (kw["array"] > "(" > additive_expression(_r1) > "," > conditional_expression(_r1) > ")") + [px::bind(&MyContext::new_vector_variable_array, _r1, _a, _b, _1, _2)] + ); + new_variable_initializer_list = + conditional_expression(_r1)[px::bind(&MyContext::new_vector_variable_initializer_list_append, _val, _1)] >> + *(lit(',') > conditional_expression(_r1)[px::bind(&MyContext::new_vector_variable_initializer_list_append, _val, _1)]); struct FactorActions { static void set_start_pos(Iterator &start_pos, expr &out) @@ -1544,23 +2038,26 @@ namespace client variable_reference.name("variable reference"); variable = identifier[ px::bind(&MyContext::resolve_variable, _r1, _1, _val) ]; - variable.name("variable reference"); + variable.name("variable name"); regular_expression = raw[lexeme['/' > *((utf8char - char_('\\') - char_('/')) | ('\\' > char_)) > '/']]; regular_expression.name("regular_expression"); keywords.add ("and") + ("array") ("digits") ("zdigits") ("if") ("int") ("is_nil") + ("local") //("inf") ("else") ("elsif") ("endif") ("false") + ("global") ("interpolate_table") ("min") ("max") @@ -1653,9 +2150,12 @@ namespace client qi::rule(const MyContext*, const expr ¶m), spirit_encoding::space_type> interpolate_table_list; qi::rule, spirit_encoding::space_type> if_else_output; - qi::rule, int>, spirit_encoding::space_type> assignment_statement; -// qi::rule, bool, std::string>, spirit_encoding::space_type> switch_output; + qi::rule>, spirit_encoding::space_type> assignment_statement; + // Allocating new local or global variables. + qi::rule>, spirit_encoding::space_type> new_variable_statement; + qi::rule>(const MyContext*), spirit_encoding::space_type> new_variable_initializer_list; +// qi::rule, bool, std::string>, spirit_encoding::space_type> switch_output; qi::symbols keywords; }; } diff --git a/src/libslic3r/PlaceholderParser.hpp b/src/libslic3r/PlaceholderParser.hpp index a3f051558..39c33206c 100644 --- a/src/libslic3r/PlaceholderParser.hpp +++ b/src/libslic3r/PlaceholderParser.hpp @@ -20,7 +20,10 @@ public: // In the future, the context may hold variables created and modified by the PlaceholderParser // and shared between the PlaceholderParser::process() invocations. struct ContextData { - std::mt19937 rng; + std::mt19937 rng; + // If defined, then this dictionary is used by the scripts to define user variables and persist them + // between PlaceholderParser evaluations. + std::unique_ptr global_config; }; PlaceholderParser(const DynamicConfig *external_config = nullptr); diff --git a/tests/libslic3r/test_placeholder_parser.cpp b/tests/libslic3r/test_placeholder_parser.cpp index 320b004ae..ee1461baf 100644 --- a/tests/libslic3r/test_placeholder_parser.cpp +++ b/tests/libslic3r/test_placeholder_parser.cpp @@ -39,6 +39,10 @@ SCENARIO("Placeholder parser scripting", "[PlaceholderParser]") { SECTION("nullable is not null") { REQUIRE(parser.process("{is_nil(filament_retract_length[0])}") == "false"); } SECTION("nullable is null") { REQUIRE(parser.process("{is_nil(filament_retract_length[1])}") == "true"); } SECTION("nullable is not null 2") { REQUIRE(parser.process("{is_nil(filament_retract_length[2])}") == "false"); } + SECTION("multiple expressions") { REQUIRE(parser.process("{temperature[foo];temperature[foo]}") == "357357"); } + SECTION("multiple expressions with semicolons") { REQUIRE(parser.process("{temperature[foo];;;temperature[foo];}") == "357357"); } + SECTION("multiple expressions with semicolons 2") { REQUIRE(parser.process("{temperature[foo];;temperature[foo];}") == "357357"); } + SECTION("multiple expressions with semicolons 3") { REQUIRE(parser.process("{temperature[foo];;;temperature[foo];;}") == "357357"); } // Test the math expressions. SECTION("math: 2*3") { REQUIRE(parser.process("{2*3}") == "6"); } @@ -146,3 +150,76 @@ SCENARIO("Placeholder parser scripting", "[PlaceholderParser]") { REQUIRE(config_outputs.opt_float("writable_floats", 1) == Approx(33.)); } } + +SCENARIO("Placeholder parser variables", "[PlaceholderParser]") { + PlaceholderParser parser; + auto config = DynamicPrintConfig::full_print_config(); + + config.set_deserialize_strict({ + { "filament_notes", "testnotes" }, + { "enable_dynamic_fan_speeds", "1" }, + { "nozzle_diameter", "0.6;0.6;0.6;0.6" }, + { "temperature", "357;359;363;378" } + }); + + PlaceholderParser::ContextData context_with_global_dict; + context_with_global_dict.global_config = std::make_unique(); + + SECTION("create an int local variable") { REQUIRE(parser.process("{local myint = 33+2}{myint}", 0, nullptr, nullptr, nullptr) == "35"); } + SECTION("create a string local variable") { REQUIRE(parser.process("{local mystr = \"mine\" + \"only\" + \"mine\"}{mystr}", 0, nullptr, nullptr, nullptr) == "mineonlymine"); } + SECTION("create a bool local variable") { REQUIRE(parser.process("{local mybool = 1 + 1 == 2}{mybool}", 0, nullptr, nullptr, nullptr) == "true"); } + SECTION("create an int global variable") { REQUIRE(parser.process("{global myint = 33+2}{myint}", 0, nullptr, nullptr, &context_with_global_dict) == "35"); } + SECTION("create a string global variable") { REQUIRE(parser.process("{global mystr = \"mine\" + \"only\" + \"mine\"}{mystr}", 0, nullptr, nullptr, &context_with_global_dict) == "mineonlymine"); } + SECTION("create a bool global variable") { REQUIRE(parser.process("{global mybool = 1 + 1 == 2}{mybool}", 0, nullptr, nullptr, &context_with_global_dict) == "true"); } + + SECTION("create an int local variable and overwrite it") { REQUIRE(parser.process("{local myint = 33+2}{myint = 12}{myint}", 0, nullptr, nullptr, nullptr) == "12"); } + SECTION("create a string local variable and overwrite it") { REQUIRE(parser.process("{local mystr = \"mine\" + \"only\" + \"mine\"}{mystr = \"yours\"}{mystr}", 0, nullptr, nullptr, nullptr) == "yours"); } + SECTION("create a bool local variable and overwrite it") { REQUIRE(parser.process("{local mybool = 1 + 1 == 2}{mybool = false}{mybool}", 0, nullptr, nullptr, nullptr) == "false"); } + SECTION("create an int global variable and overwrite it") { REQUIRE(parser.process("{global myint = 33+2}{myint = 12}{myint}", 0, nullptr, nullptr, &context_with_global_dict) == "12"); } + SECTION("create a string global variable and overwrite it") { REQUIRE(parser.process("{global mystr = \"mine\" + \"only\" + \"mine\"}{mystr = \"yours\"}{mystr}", 0, nullptr, nullptr, &context_with_global_dict) == "yours"); } + SECTION("create a bool global variable and overwrite it") { REQUIRE(parser.process("{global mybool = 1 + 1 == 2}{mybool = false}{mybool}", 0, nullptr, nullptr, &context_with_global_dict) == "false"); } + + SECTION("create an int local variable and redefine it") { REQUIRE(parser.process("{local myint = 33+2}{local myint = 12}{myint}", 0, nullptr, nullptr, nullptr) == "12"); } + SECTION("create a string local variable and redefine it") { REQUIRE(parser.process("{local mystr = \"mine\" + \"only\" + \"mine\"}{local mystr = \"yours\"}{mystr}", 0, nullptr, nullptr, nullptr) == "yours"); } + SECTION("create a bool local variable and redefine it") { REQUIRE(parser.process("{local mybool = 1 + 1 == 2}{local mybool = false}{mybool}", 0, nullptr, nullptr, nullptr) == "false"); } + SECTION("create an int global variable and redefine it") { REQUIRE(parser.process("{global myint = 33+2}{global myint = 12}{myint}", 0, nullptr, nullptr, &context_with_global_dict) == "12"); } + SECTION("create a string global variable and redefine it") { REQUIRE(parser.process("{global mystr = \"mine\" + \"only\" + \"mine\"}{global mystr = \"yours\"}{mystr}", 0, nullptr, nullptr, &context_with_global_dict) == "yours"); } + SECTION("create a bool global variable and redefine it") { REQUIRE(parser.process("{global mybool = 1 + 1 == 2}{global mybool = false}{mybool}", 0, nullptr, nullptr, &context_with_global_dict) == "false"); } + + SECTION("create an ints local variable with array()") { REQUIRE(parser.process("{local myint = array(2*3, 4*6)}{myint[5]}", 0, nullptr, nullptr, nullptr) == "24"); } + SECTION("create a strings local variable array()") { REQUIRE(parser.process("{local mystr = array(2*3, \"mine\" + \"only\" + \"mine\")}{mystr[5]}", 0, nullptr, nullptr, nullptr) == "mineonlymine"); } + SECTION("create a bools local variable array()") { REQUIRE(parser.process("{local mybool = array(5, 1 + 1 == 2)}{mybool[4]}", 0, nullptr, nullptr, nullptr) == "true"); } + SECTION("create an ints global variable array()") { REQUIRE(parser.process("{global myint = array(2*3, 4*6)}{myint[5]}", 0, nullptr, nullptr, &context_with_global_dict) == "24"); } + SECTION("create a strings global variable array()") { REQUIRE(parser.process("{global mystr = array(2*3, \"mine\" + \"only\" + \"mine\")}{mystr[5]}", 0, nullptr, nullptr, &context_with_global_dict) == "mineonlymine"); } + SECTION("create a bools global variable array()") { REQUIRE(parser.process("{global mybool = array(5, 1 + 1 == 2)}{mybool[4]}", 0, nullptr, nullptr, &context_with_global_dict) == "true"); } + + SECTION("create an ints local variable with initializer list") { REQUIRE(parser.process("{local myint = (2*3, 4*6, 5*5)}{myint[1]}", 0, nullptr, nullptr, nullptr) == "24"); } + SECTION("create a strings local variable with initializer list") { REQUIRE(parser.process("{local mystr = (2*3, \"mine\" + \"only\" + \"mine\", 8)}{mystr[1]}", 0, nullptr, nullptr, nullptr) == "mineonlymine"); } + SECTION("create a bools local variable with initializer list") { REQUIRE(parser.process("{local mybool = (3*3 == 8, 1 + 1 == 2)}{mybool[1]}", 0, nullptr, nullptr, nullptr) == "true"); } + SECTION("create an ints global variable with initializer list") { REQUIRE(parser.process("{global myint = (2*3, 4*6, 5*5)}{myint[1]}", 0, nullptr, nullptr, &context_with_global_dict) == "24"); } + SECTION("create a strings global variable with initializer list") { REQUIRE(parser.process("{global mystr = (2*3, \"mine\" + \"only\" + \"mine\", 8)}{mystr[1]}", 0, nullptr, nullptr, &context_with_global_dict) == "mineonlymine"); } + SECTION("create a bools global variable with initializer list") { REQUIRE(parser.process("{global mybool = (2*3 == 8, 1 + 1 == 2, 5*5 != 33)}{mybool[1]}", 0, nullptr, nullptr, &context_with_global_dict) == "true"); } + + SECTION("create an ints local variable by a copy") { REQUIRE(parser.process("{local myint = temperature}{myint[0]}", 0, &config, nullptr, nullptr) == "357"); } + SECTION("create a strings local variable by a copy") { REQUIRE(parser.process("{local mystr = filament_notes}{mystr[0]}", 0, &config, nullptr, nullptr) == "testnotes"); } + SECTION("create a bools local variable by a copy") { REQUIRE(parser.process("{local mybool = enable_dynamic_fan_speeds}{mybool[0]}", 0, &config, nullptr, nullptr) == "true"); } + SECTION("create an ints global variable by a copy") { REQUIRE(parser.process("{global myint = temperature}{myint[0]}", 0, &config, nullptr, &context_with_global_dict) == "357"); } + SECTION("create a strings global variable by a copy") { REQUIRE(parser.process("{global mystr = filament_notes}{mystr[0]}", 0, &config, nullptr, &context_with_global_dict) == "testnotes"); } + SECTION("create a bools global variable by a copy") { REQUIRE(parser.process("{global mybool = enable_dynamic_fan_speeds}{mybool[0]}", 0, &config, nullptr, &context_with_global_dict) == "true"); } + + SECTION("create an ints local variable by a copy and overwrite it") { + REQUIRE(parser.process("{local myint = temperature}{myint = array(2*3, 4*6)}{myint[5]}", 0, &config, nullptr, nullptr) == "24"); + REQUIRE(parser.process("{local myint = temperature}{myint = (2*3, 4*6)}{myint[1]}", 0, &config, nullptr, nullptr) == "24"); + REQUIRE(parser.process("{local myint = temperature}{myint = (1)}{myint = temperature}{myint[0]}", 0, &config, nullptr, nullptr) == "357"); + } + SECTION("create a strings local variable by a copy and overwrite it") { + REQUIRE(parser.process("{local mystr = filament_notes}{mystr = array(2*3, \"mine\" + \"only\" + \"mine\")}{mystr[5]}", 0, &config, nullptr, nullptr) == "mineonlymine"); + REQUIRE(parser.process("{local mystr = filament_notes}{mystr = (2*3, \"mine\" + \"only\" + \"mine\")}{mystr[1]}", 0, &config, nullptr, nullptr) == "mineonlymine"); + REQUIRE(parser.process("{local mystr = filament_notes}{mystr = (2*3, \"mine\" + \"only\" + \"mine\")}{mystr = filament_notes}{mystr[0]}", 0, &config, nullptr, nullptr) == "testnotes"); + } + SECTION("create a bools local variable by a copy and overwrite it") { + REQUIRE(parser.process("{local mybool = enable_dynamic_fan_speeds}{mybool = array(2*3, true)}{mybool[5]}", 0, &config, nullptr, nullptr) == "true"); + REQUIRE(parser.process("{local mybool = enable_dynamic_fan_speeds}{mybool = (false, true)}{mybool[1]}", 0, &config, nullptr, nullptr) == "true"); + REQUIRE(parser.process("{local mybool = enable_dynamic_fan_speeds}{mybool = (false, false)}{mybool = enable_dynamic_fan_speeds}{mybool[0]}", 0, &config, nullptr, nullptr) == "true"); + } +}