Math evaluation in gcode (#4157)

* Prototype gcode infix math, very basic.

* Adding exprtk math parser library, header only.
@ArashPartow
https://github.com/ArashPartow/exprtk@4e1315a87dcc99a1ccad21fae1def0c2d4913c0f

* Now evaluating strings with exprtk, only support no variables in input
strings.

* Moved executable code to cpp file, stubbed out xsp and let the testing begin...

* Added conditional gcode parser into export path, added tests.

* Added one more test to ensure that {if0} only removes up to newlines.

* Test failure to parse

* Add some compiler flags to compile out stuff from exprtk

* Fix debug messages to be more specific, don't use deleted stringstream = method.

* Trade expression speed for apparently around 50MB of object size.

* Removed an extra trim that was breaking existing tests.

* fix test

Fixes #3390
This commit is contained in:
Joseph Lenox 2017-10-20 21:40:05 -05:00 committed by GitHub
parent 4f2250dc70
commit 202a90ff90
7 changed files with 38432 additions and 8 deletions

View File

@ -151,7 +151,7 @@ sub export {
# set extruder(s) temperature before and after start G-code
$self->_print_first_layer_temperature(0)
if $self->config->start_gcode !~ /M(?:109|104)/i;
printf $fh "%s\n", $gcodegen->placeholder_parser->process($self->config->start_gcode);
printf $fh "%s\n", Slic3r::ConditionalGCode::apply_math($gcodegen->placeholder_parser->process($self->config->start_gcode));
foreach my $start_gcode (@{ $self->config->start_filament_gcode }) { # process filament gcode in order
printf $fh "%s\n", $gcodegen->placeholder_parser->process($start_gcode);
}
@ -256,7 +256,7 @@ sub export {
&& $self->config->between_objects_gcode !~ /M(?:190|140)/i;
$self->_print_first_layer_temperature(0)
if $self->config->between_objects_gcode !~ /M(?:109|104)/i;
printf $fh "%s\n", $gcodegen->placeholder_parser->process($self->config->between_objects_gcode);
printf $fh "%s\n", Slic3r::ConditionalGCode::apply_math($gcodegen->placeholder_parser->process($self->config->between_objects_gcode));
}
$self->process_layer($layer, [$copy]);
}
@ -294,9 +294,9 @@ sub export {
print $fh $gcodegen->retract; # TODO: process this retract through PressureRegulator in order to discharge fully
print $fh $gcodegen->writer->set_fan(0);
foreach my $end_gcode (@{ $self->config->end_filament_gcode }) { # Process filament-specific gcode in extruder order.
printf $fh "%s\n", $gcodegen->placeholder_parser->process($end_gcode);
printf $fh "%s\n", Slic3r::ConditionalGCode::apply_math($gcodegen->placeholder_parser->process($end_gcode));
}
printf $fh "%s\n", $gcodegen->placeholder_parser->process($self->config->end_gcode);
printf $fh "%s\n", Slic3r::ConditionalGCode::apply_math($gcodegen->placeholder_parser->process($self->config->end_gcode));
print $fh $gcodegen->writer->update_progress($gcodegen->layer_count, $gcodegen->layer_count, 1); # 100%
print $fh $gcodegen->writer->postamble;
@ -444,14 +444,14 @@ sub process_layer {
my $pp = $self->_gcodegen->placeholder_parser->clone;
$pp->set('layer_num' => $self->_gcodegen->layer_index + 1);
$pp->set('layer_z' => $layer->print_z);
$gcode .= $pp->process($self->print->config->before_layer_gcode) . "\n";
$gcode .= Slic3r::ConditionalGCode::apply_math($pp->process($self->print->config->before_layer_gcode) . "\n");
}
$gcode .= $self->_gcodegen->change_layer($layer->as_layer); # this will increase $self->_gcodegen->layer_index
if ($self->print->config->layer_gcode) {
my $pp = $self->_gcodegen->placeholder_parser->clone;
$pp->set('layer_num' => $self->_gcodegen->layer_index);
$pp->set('layer_z' => $layer->print_z);
$gcode .= $pp->process($self->print->config->layer_gcode) . "\n";
$gcode .= Slic3r::ConditionalGCode::apply_math($pp->process($self->print->config->layer_gcode) . "\n");
}
# extrude skirt along raft layers and normal object layers

View File

@ -19,7 +19,7 @@ $ENV{LD_RUN_PATH} //= "";
# HAS_BOOL : stops Perl/lib/CORE/handy.h from doing "# define bool char" for MSVC
# NOGDI : prevents inclusion of wingdi.h which defines functions Polygon() and Polyline() in global namespace
# BOOST_ASIO_DISABLE_KQUEUE : prevents a Boost ASIO bug on OS X: https://svn.boost.org/trac/boost/ticket/5339
my @cflags = qw(-D_GLIBCXX_USE_C99 -DHAS_BOOL -DNOGDI -DSLIC3RXS -DBOOST_ASIO_DISABLE_KQUEUE);
my @cflags = qw(-D_GLIBCXX_USE_C99 -DHAS_BOOL -DNOGDI -DSLIC3RXS -DBOOST_ASIO_DISABLE_KQUEUE -Dexprtk_disable_rtl_io_file -Dexprtk_disable_return_statement -Dexprtk_disable_rtl_vecops -Dexprtk_disable_string_capabilities -Dexprtk_disable_enhanced_features);
if ($cpp_guess->is_gcc) {
# GCC is pedantic with c++11 std, so undefine strict ansi to get M_PI back
push @cflags, qw(-U__STRICT_ANSI__);
@ -211,7 +211,7 @@ if ($ENV{SLIC3R_DEBUG}) {
push @cflags, $cpp_guess->is_msvc ? '-Gd' : '-g';
} else {
# Disable asserts in the release builds.
push @cflags, '-DNDEBUG';
push @cflags, '-DNDEBUG', '-O';
}
if ($cpp_guess->is_gcc) {
# our templated XS bindings cause undefined-var-template warnings

38195
xs/src/exprtk/exprtk.hpp Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,132 @@
#include <string>
#include <sstream>
#include <exprtk/exprtk.hpp>
#include "ConditionalGcode.hpp"
namespace Slic3r {
// trim from start (in place)
static inline void ltrim(std::string &s) {
s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](int ch) {
return !std::isspace(ch);
}));
}
// trim from end (in place)
static inline void rtrim(std::string &s) {
s.erase(std::find_if(s.rbegin(), s.rend(), [](int ch) {
return !std::isspace(ch);
}).base(), s.end());
}
// trim from both ends (in place)
static inline void trim(std::string &s) {
ltrim(s);
rtrim(s);
}
/// Start of recursive function to parse gcode file.
std::string apply_math(const std::string& input) {
return expression(input);
}
/// Evaluate expressions with exprtk
/// Everything must resolve to a number.
std::string evaluate(const std::string& expression_string) {
std::stringstream result;
#if SLIC3R_DEBUG
std::cerr << __FILE__ << ":" << __LINE__ << " "<< "Evaluating expression: " << expression_string << std::endl;
#endif
double num_result = double(0);
if ( exprtk::compute(expression_string, num_result)) {
result << num_result;
} else {
#if SLIC3R_DEBUG
std::cerr << __FILE__ << ":" << __LINE__ << " "<< "Failed to parse: " << expression_string.c_str() << std::endl;
#endif
}
std::string output = result.str();
trim(output);
return output;
}
/// Parse an expression and return a string. We assume that PlaceholderParser has expanded all variables.
std::string expression(const std::string& input, const int depth) {
// check for subexpressions first.
std::string buffer(input);
std::stringstream tmp;
bool is_conditional = false;
auto open_bracket = std::count(buffer.begin(), buffer.end(), '{');
auto close_bracket = std::count(buffer.begin(), buffer.end(), '}');
if (open_bracket != close_bracket) return buffer;
auto i = 0;
#ifdef SLIC3R_DEBUG
std::cerr << __FILE__ << ":" << __LINE__ << " " << "depth " << depth << " input str: " << input << std::endl;
#endif
if (open_bracket == 0 && depth > 0) { // no subexpressions, resolve operators.
return evaluate(buffer);
}
while (open_bracket > 0) {
// a subexpression has been found, find the end of it.
// find the last open bracket, then the first open bracket after it.
size_t pos_if = buffer.rfind("{if");
size_t pos = buffer.rfind("{");
size_t shift_if = ( pos_if >= pos && pos_if < buffer.size() ? 3 : 1 );
is_conditional = (shift_if == 3); // conditional statement
pos_if = (pos_if > buffer.size() ? pos : pos_if);
pos = (pos_if > pos ? pos_if : pos);
#ifdef SLIC3R_DEBUG
std::cerr << __FILE__ << ":" << __LINE__ << " "<< "depth " << depth << " loop " << i << " pos: " << pos << std::endl;
#endif
// find the first bracket after the position
size_t end_pos = buffer.find("}", pos);
if (end_pos > buffer.size()) return buffer; // error!
if (pos > 0)
tmp << buffer.substr(0, pos);
std::string retval = expression(buffer.substr(pos+shift_if, ( end_pos - ( pos+shift_if))), depth+1);
#ifdef SLIC3R_DEBUG
if (is_conditional) {
std::cerr << __FILE__ << ":" << __LINE__ << " "<< "depth " << depth << " loop " << i << " return: '" << retval << "'" << std::endl;
}
#endif
if (is_conditional && retval == "0") {
is_conditional = false;
end_pos = buffer.find('\n', pos); // drop everything to the next line
} else if (!is_conditional) {
tmp << retval;
} // in either case, don't print the output from {if}
if (end_pos < buffer.size()-1) // special case, last } is final char
tmp << buffer.substr(end_pos+1, buffer.size() - (end_pos));
buffer = tmp.str();
// flush the internal string.
tmp.str(std::string());
#ifdef SLIC3R_DEBUG
std::cerr << __FILE__ << ":" << __LINE__ << " "<< "depth: " << depth <<" Result from loop " << i << ": " << buffer << std::endl;
#endif
open_bracket = std::count(buffer.begin(), buffer.end(), '{');
close_bracket = std::count(buffer.begin(), buffer.end(), '}');
i++;
}
// {if that resolves to false/0 signifies dropping everything up to the next newline from the input buffer.
// Also remove the result of the {if itself.
return buffer;
}
}

View File

@ -0,0 +1,33 @@
/**
* https://github.com/alexrj/Slic3r/wiki/Conditional-Gcode-Syntax-Spec
*
*/
#include <iostream>
#include <string>
#include <sstream>
// Valid start tokens
// {, {if
//
// Valid end tokens
// }
//
// Special case:
//
// {if is special, it indicates that the rest of the line is dropped (ignored) if
// it evaluates to False/0.
namespace Slic3r {
/// Recursive expression parser. Offloads mathematics to exprtk.
/// Precondition: All strings inside {} are able to be understood by exprtk (and thus parsed to a number).
/// Starts from the end of the string and works from the inside out.
/// Any statements that resolve to {if0} will remove everything on the same line.
std::string expression(const std::string& input, const int depth = 0);
/// External access function to begin replac
std::string apply_math(const std::string& input);
}

46
xs/t/24_gcodemath.t Normal file
View File

@ -0,0 +1,46 @@
#!/usr/bin/perl
use strict;
use warnings;
use Slic3r::XS;
use Test::More tests => 6;
{
{
my $test_string = "{if{3 == 4}} string";
my $result = Slic3r::ConditionalGCode::apply_math($test_string);
is $result, "", 'If statement with nested bracket removes on false resolution.';
}
{
my $test_string = "{if{3 == 4}} string\notherstring";
my $result = Slic3r::ConditionalGCode::apply_math($test_string);
is $result, "otherstring", 'if false only removes up to newline.';
}
{
my $test_string = "{if{3 == 3}} string";
my $result = Slic3r::ConditionalGCode::apply_math($test_string);
is $result, " string", 'If statement with nested bracket removes itself only on resulting true, does not strip text outside of brackets.';
}
{
my $test_string = "{if{3 == 3}}string";
my $result = Slic3r::ConditionalGCode::apply_math($test_string);
is $result, "string", 'If statement with nested bracket removes itself only on resulting true.';
}
{
my $test_string = "M104 S{4*5}; Sets temp to {4*5}";
my $result = Slic3r::ConditionalGCode::apply_math($test_string);
is $result, "M104 S20; Sets temp to 20", 'Bracket replacement works with math ops';
}
{
my $test_string = "M104 S{a}; Sets temp to {4*5}";
my $result = Slic3r::ConditionalGCode::apply_math($test_string);
is $result, "M104 S; Sets temp to 20", 'Blank string emittal on failure to parse';
}
}

View File

@ -0,0 +1,18 @@
%module{Slic3r::XS};
%{
#include <xsinit.h>
#include "libslic3r/ConditionalGcode.hpp"
%}
%package{Slic3r::ConditionalGCode};
%{
PROTOTYPES: DISABLE
std::string apply_math(std::string input)
CODE:
RETVAL = Slic3r::apply_math(input);
OUTPUT:
RETVAL
%}