diff --git a/src/libslic3r/PlaceholderParser.cpp b/src/libslic3r/PlaceholderParser.cpp index 5f8624ad3..6ecb52233 100644 --- a/src/libslic3r/PlaceholderParser.cpp +++ b/src/libslic3r/PlaceholderParser.cpp @@ -249,6 +249,7 @@ namespace client TYPE_STRING, }; Type type() const { return m_type; } + bool numeric_type() const { return m_type == TYPE_INT || m_type == TYPE_DOUBLE; } bool& b() { return m_data.b; } bool b() const { return m_data.b; } @@ -472,8 +473,7 @@ namespace client static void compare_op(expr &lhs, expr &rhs, char op, bool invert) { bool value = false; - if ((lhs.type() == TYPE_INT || lhs.type() == TYPE_DOUBLE) && - (rhs.type() == TYPE_INT || rhs.type() == TYPE_DOUBLE)) { + if (lhs.numeric_type() && rhs.numeric_type()) { // Both types are numeric. switch (op) { case '=': @@ -681,7 +681,7 @@ namespace client void throw_if_not_numeric(const char *message) const { - if (this->type() != TYPE_INT && this->type() != TYPE_DOUBLE) + if (! this->numeric_type()) this->throw_exception(message); } @@ -964,6 +964,10 @@ namespace client { if (! opt.writable) ctx->throw_exception("Cannot modify a read-only variable", opt.it_range); + auto check_numeric = [](const expr ¶m) { + if (! param.numeric_type()) + param.throw_exception("Right side is not a numeric expression"); + }; if (opt.opt->is_vector()) { if (! opt.has_index()) ctx->throw_exception("Referencing an output vector variable when scalar is expected", opt.it_range); @@ -974,21 +978,18 @@ namespace client ctx->throw_exception("Index out of range", opt.it_range); switch (opt.opt->type()) { case coFloats: - if (param.type() != expr::TYPE_INT && param.type() != expr::TYPE_DOUBLE) - ctx->throw_exception("Right side is not a numeric expression", param.it_range); + check_numeric(param); static_cast(vec)->values[opt.index] = param.as_d(); break; case coInts: - if (param.type() != expr::TYPE_INT && param.type() != expr::TYPE_DOUBLE) - ctx->throw_exception("Right side is not a numeric expression", param.it_range); + check_numeric(param); static_cast(vec)->values[opt.index] = param.as_i(); break; case coStrings: static_cast(vec)->values[opt.index] = param.to_string(); break; case coPercents: - if (param.type() != expr::TYPE_INT && param.type() != expr::TYPE_DOUBLE) - ctx->throw_exception("Right side is not a numeric expression", param.it_range); + check_numeric(param); static_cast(vec)->values[opt.index] = param.as_d(); break; case coBools: @@ -1004,21 +1005,18 @@ namespace client ConfigOption *wropt = const_cast(opt.opt); switch (wropt->type()) { case coFloat: - if (param.type() != expr::TYPE_INT && param.type() != expr::TYPE_DOUBLE) - ctx->throw_exception("Right side is not a numeric expression", param.it_range); + check_numeric(param); static_cast(wropt)->value = param.as_d(); break; case coInt: - if (param.type() != expr::TYPE_INT && param.type() != expr::TYPE_DOUBLE) - ctx->throw_exception("Right side is not a numeric expression", param.it_range); + check_numeric(param); static_cast(wropt)->value = param.as_i(); break; case coString: static_cast(wropt)->value = param.to_string(); break; case coPercent: - if (param.type() != expr::TYPE_INT && param.type() != expr::TYPE_DOUBLE) - ctx->throw_exception("Right side is not a numeric expression", param.it_range); + check_numeric(param); static_cast(wropt)->value = param.as_d(); break; case coBool: @@ -1109,6 +1107,72 @@ namespace client } }; + template + struct InterpolateTableContext { + template + struct Item { + double x; + boost::iterator_range it_range_x; + double y; + }; + std::vector> table; + + static void init(const expr &x) { + if (!x.numeric_type()) + x.throw_exception("Interpolation value must be a number."); + } + static void add_pair(const expr &x, const expr &y, InterpolateTableContext &table) { + if (! x.numeric_type()) + x.throw_exception("X value of a table point must be a number."); + if (! y.numeric_type()) + y.throw_exception("Y value of a table point must be a number."); + table.table.push_back({ x.as_d(), x.it_range, y.as_d() }); + } + static void evaluate(const expr &expr_x, const InterpolateTableContext &table, expr &out) { + // Check whether the table X values are sorted. + double x = expr_x.as_d(); + bool evaluated = false; + for (size_t i = 1; i < table.table.size(); ++i) { + double x0 = table.table[i - 1].x; + double x1 = table.table[i].x; + if (x0 > x1) + boost::throw_exception(qi::expectation_failure( + table.table[i - 1].it_range_x.begin(), table.table[i].it_range_x.end(), spirit::info("X coordinates of the table must be increasing"))); + if (! evaluated && x >= x0 && x <= x1) { + double y0 = table.table[i - 1].y; + double y1 = table.table[i].y; + if (x == x0) + out.set_d(y0); + else if (x == x1) + out.set_d(y1); + else if (is_approx(x0, x1)) + out.set_d(0.5 * (y0 + y1)); + else + out.set_d(Slic3r::lerp(y0, y1, (x - x0) / (x1 - x0))); + evaluated = true; + } + } + if (! evaluated) { + // Clamp x into the table range with EPSILON. + if (x > table.table.front().x - EPSILON) + out.set_d(table.table.front().y); + else if (x < table.table.back().x + EPSILON) + out.set_d(table.table.back().y); + else + // The value is really outside the table range. + expr_x.throw_exception("Interpolation value is outside the table range"); + } + } + }; + + template + std::ostream& operator<<(std::ostream &os, const InterpolateTableContext &table_context) + { + for (const auto &item : table_context.table) + os << "(" << item.x << "," << item.y << ")"; + return os; + } + // Table to translate symbol tag to a human readable error message. std::map MyContext::tag_to_error_message = { { "eoi", "Unknown syntax error" }, @@ -1428,6 +1492,7 @@ namespace client | (kw["round"] > '(' > conditional_expression(_r1) > ')') [ px::bind(&FactorActions::round, _1, _val) ] | (kw["is_nil"] > '(' > is_nil_test(_r1) > ')') [ _val = _1 ] | (kw["one_of"] > '(' > one_of(_r1) > ')') [ _val = _1 ] + | (kw["interpolate_table"] > '(' > interpolate_table(_r1) > ')') [ _val = _1 ] | (strict_double > iter_pos) [ px::bind(&FactorActions::double_, _1, _2, _val) ] | (int_ > iter_pos) [ px::bind(&FactorActions::int_, _1, _2, _val) ] | (kw[bool_] > iter_pos) [ px::bind(&FactorActions::bool_, _1, _2, _val) ] @@ -1440,16 +1505,26 @@ namespace client one_of.name("one_of"); one_of_list = eps[px::bind(&expr::one_of_test_init, _val)] > - ( ',' > *( + ( ( ',' > *( ( unary_expression(_r1)[px::bind(&expr::template one_of_test, _r2, _1, _val)] | (lit('~') > unary_expression(_r1))[px::bind(&expr::template one_of_test, _r2, _1, _val)] | regular_expression[px::bind(&expr::one_of_test_regex, _r2, _1, _val)] ) >> -lit(',')) - | eps + ) + | eps ); one_of_list.name("one_of_list"); + interpolate_table = (unary_expression(_r1)[_a = _1] > ',' > interpolate_table_list(_r1, _a)) + [px::bind(&InterpolateTableContext::evaluate, _a, _2, _val)]; + interpolate_table.name("interpolate_table"); + interpolate_table_list = + eps[px::bind(&InterpolateTableContext::init, _r2)] > + ( *(( lit('(') > unary_expression(_r1) > ',' > unary_expression(_r1) > ')' ) + [px::bind(&InterpolateTableContext::add_pair, _1, _2, _val)] >> -lit(',')) ); + interpolate_table.name("interpolate_table_list"); + optional_parameter = iter_pos[px::bind(&FactorActions::set_start_pos, _1, _val)] >> ( lit(')') [ px::bind(&FactorActions::noexpr, _val) ] | (lit(',') > conditional_expression(_r1) > ')') [ _val = _1 ] @@ -1486,6 +1561,7 @@ namespace client ("elsif") ("endif") ("false") + ("interpolate_table") ("min") ("max") ("random") @@ -1501,9 +1577,12 @@ namespace client debug(text_block); debug(macro); debug(if_else_output); + debug(interpolate_table); // debug(switch_output); debug(legacy_variable_expansion); debug(identifier); + debug(interpolate_table); + debug(interpolate_table_list); debug(conditional_expression); debug(logical_or_expression); debug(logical_and_expression); @@ -1569,6 +1648,9 @@ namespace client // Evaluating "one of" list of patterns. qi::rule(const MyContext*), qi::locals>, spirit_encoding::space_type> one_of; qi::rule(const MyContext*, const expr ¶m), spirit_encoding::space_type> one_of_list; + // Evaluating the "interpolate_table" expression. + qi::rule(const MyContext*), qi::locals>, spirit_encoding::space_type> interpolate_table; + 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; diff --git a/tests/libslic3r/test_placeholder_parser.cpp b/tests/libslic3r/test_placeholder_parser.cpp index e40657d16..320b004ae 100644 --- a/tests/libslic3r/test_placeholder_parser.cpp +++ b/tests/libslic3r/test_placeholder_parser.cpp @@ -71,6 +71,9 @@ SCENARIO("Placeholder parser scripting", "[PlaceholderParser]") { SECTION("math: zdigits(5., 15, 8)") { REQUIRE(parser.process("{zdigits(5, 15, 8)}") == "000005.00000000"); } SECTION("math: digits(13.84375892476, 15, 8)") { REQUIRE(parser.process("{digits(13.84375892476, 15, 8)}") == " 13.84375892"); } SECTION("math: zdigits(13.84375892476, 15, 8)") { REQUIRE(parser.process("{zdigits(13.84375892476, 15, 8)}") == "000013.84375892"); } + SECTION("math: interpolate_table(13.84375892476, (0, 0), (20, 20))") { REQUIRE(std::stod(parser.process("{interpolate_table(13.84375892476, (0, 0), (20, 20))}")) == Approx(13.84375892476)); } + SECTION("math: interpolate_table(13, (0, 0), (20, 20), (30, 20))") { REQUIRE(std::stod(parser.process("{interpolate_table(13, (0, 0), (20, 20), (30, 20))}")) == Approx(13.)); } + SECTION("math: interpolate_table(25, (0, 0), (20, 20), (30, 20))") { REQUIRE(std::stod(parser.process("{interpolate_table(25, (0, 0), (20, 20), (30, 20))}")) == Approx(20.)); } // Test the "coFloatOrPercent" and "xxx_extrusion_width" substitutions. // first_layer_extrusion_width ratio_over first_layer_heigth.