mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-13 19:01:21 +00:00
* Add orca-translations Claude skill and PO tool
Add a .claude skill that guides translation passes over the localization/i18n gettext catalogs. It covers the workflow: run gettext, settle new glossary terms, translate in batches, verify, and report.
Includes translation-rules.md, which condenses the placeholder, line-break and glossary rules plus the guidance on review notes.
Also adds scripts/po_tool.py (standard library only). It lists pending entries with glossary hints, validates and applies translations, adding '# AI Translated' and '# Needs human review' comments, and rewrites only the entries it changes. It can also check catalogs, look up existing translations, suggest glossary candidates and update the wiki glossary TSV.
* Let po_tool apply translations by entry id
pending now gives each entry a short id, so a translation file can name
entries by id instead of repeating the English msgid. apply accepts either.
A "review": "" item clears an earlier review note when overwriting an
entry, and check gives each msgfmt run its own temporary output file so
parallel checks do not collide.
* Fill all empty translations
Regenerate the template with run_gettext --full and translate every empty
entry in the 22 catalogs (7,089 entries) with the orca-translations skill,
following the localization glossary, including the new belt printer,
IDEX/IQEX and plugin terms. Every new entry is marked "# AI Translated";
122 also carry "# Needs human review" with what to check.
* Show entry ids in po_tool lookup and search translations
lookup now prints each entry's id, so an entry found there can be
rewritten with apply --overwrite without repeating the English, and
--msgstr searches the translations instead of the msgids, which finds
every entry that still uses a term being changed.
SKILL.md describes how to change an established term: check the
catalog's usage first, update the glossary cell, then rewrite only the
entries where the term has that meaning.
* Fix mistranslated terms and align catalogs with the glossary
Correct labels that the glossary had copied, and the entries that used
them:
- ca: Raft was rendered as the Brim term; it is now "Rai". Exhaust fan,
Monotonic line, Unretract, Slice, Lateral Honeycomb and PA Tower
labels fixed.
- cs: Lightning infill is "Blesk", not "Osvětlení" (illumination).
- sv: Retraction is "Retraktion", not "Reduktion"; Unretract,
Lagerhöjd, Delkylningsfläkt, Utsugsfläkt, Aligned Rectilinear and the
Preset label fixed.
- de: Brim is "Rand" and Skirt is "Umrandung", as most entries already
had them.
- lt: brim entries used the Skirt word "apvadas"; they now use "kraštas".
- es: Sparse infill is "relleno de baja densidad", the catalog majority.
- eu: nozzle wipe is "garbiketa", not purge.
- th: the project plate is "เพลต", distinct from the bed.
- uk: Quarter Cubic, Latin "PA" in PA Tower, and Belt purge tower.
- pl: Cross Hatch, Tri-hexagon, Volumetric speed and Raft labels.
- zh_TW: Brim stays "Brim", as the existing labels have it.
- tr: "Prime Tower" read "Prime Minister Tower".
- ko: the plugin "Capabilities" label read "performance".
- Build Volume labels fixed in tr, de, cs and eu.
All 242 rewritten entries are marked "# AI Translated".
* Make more UI text translatable and fix pluralization and context
Audit the sources for text that translators could not translate
correctly and fix it.
Mark text that reached users in English only: IDEX/IQEX role names,
plugin permission prompts and errors, 3MF/preset loading errors, G-code
placeholder parser errors, calibration and device messages, notification
texts, file dialog filters, and the config validation messages.
Replace sentences built from translated fragments, such as the prime
tower warnings, "left"/"right" nozzle names, and "Filament " + number,
with single msgids that have placeholders, so translators can reorder
and inflect them.
Use plural forms for texts with a count ("%1% mixed filaments",
"%d objects", "%1% triangles") instead of "(s)" and mismatched verbs.
Fix the swapped singular/plural pair in "The following presets inherit
this preset".
Add translation contexts where one English word has several meanings
(Subscribed/Local, Standard, Pause, View, Step, Model, Camera, Flow,
Filter, Others, Prepare, the Design tools). Show "Size" instead of
"Build Volume" in the Cut tool, which displays the object size.
Fix code that compared translated text with English (the Edit G-code
dialog group, the plugin registration failure check), and add the
bundled web pages' missing string-table entries. Use the single
character for degrees Celsius, say "G-code", and describe the head
filament map without internal jargon.
Install the libslic3r translate callback before the presets load, so
their load errors are translated, and list the 28 source files that had
marked strings but were never extracted into localization/i18n/list.txt.
* Suggest earlier translations and list fuzzy entries in po_tool pending
pending now attaches the closest earlier translation of a similar
English text (from obsolete or translated entries), so a reworded or
split string keeps its wording, and it lists fuzzy entries by default,
since users never see them. A fuzzy entry also carries its stale
translation as previous_translation.
* Fix wording of the non-boolean operands error
The logical operation error said "operators" where it means the
operands the operation was applied to.
* Translate the newly translatable strings in all languages
Regenerate the template after the translatability changes and fill every
new or changed entry in the 22 catalogs (about 12,800 entries): the
strings that became translatable, the plural forms, the entries split by
context, and the previously fuzzy singular entries that became plurals.
Every new entry is marked "# AI Translated", and about 360 also carry a
"# Needs human review" note saying what to check. Terms follow the
localization glossary. Earlier translations of reworded strings were
reused where the meaning was unchanged. Existing entries were not
changed, and obsolete entries are kept.
* Document the source translatability audit in the orca-translations skill
Add a reference describing how strings reach translators, what to look
for (untranslated text, spliced sentences, missing plurals, ambiguous
words), what not to translate, where a context cannot be carried, and
what to do after the edits, plus a scanner that lists untranslated
user-facing string literals.
* Document how to write translatable strings in AGENTS.md
Tell developers to keep each message complete in one msgid: use
placeholders instead of concatenating translated fragments, plural forms
for counts, and contexts for ambiguous words, and to list new source
files in localization/i18n/list.txt. Add a matching code review item.
* Translate the strings added since the rebase
Regenerate the template against the current main and translate the 26
new entries per language: the Fix Model commands and messages, and the
texture displacement image adjustment panel. Each new entry is marked
"# AI Translated"; existing entries are unchanged.
* tidy
2635 lines
130 KiB
C++
2635 lines
130 KiB
C++
#include "PlaceholderParser.hpp"
|
|
#include "Config.hpp"
|
|
#include "Exception.hpp"
|
|
#include "Flow.hpp"
|
|
#include "PrintConfig.hpp"
|
|
#include "Point.hpp"
|
|
#include "Utils.hpp"
|
|
#include "I18N.hpp"
|
|
#include "format.hpp"
|
|
#include "libslic3r_version.h"
|
|
#include <cassert>
|
|
#include <boost/range/iterator_range_core.hpp>
|
|
#include <cmath>
|
|
#include <cstdlib>
|
|
#include <boost/throw_exception.hpp>
|
|
#include <boost/spirit/home/support/info.hpp>
|
|
#include <algorithm>
|
|
#include <cstdio>
|
|
#include <boost/algorithm/string/predicate.hpp>
|
|
#include "libslic3r.h"
|
|
#include <boost/spirit/home/qi/numeric/real_policies.hpp>
|
|
#include <boost/spirit/home/qi/parser.hpp>
|
|
#include <boost/assert/source_location.hpp>
|
|
#include <boost/spirit/home/qi/nonterminal/grammar.hpp>
|
|
#include <boost/spirit/home/qi/nonterminal/rule.hpp>
|
|
#include <boost/spirit/home/support/common_terminals.hpp>
|
|
#include <boost/spirit/home/qi/auxiliary/eps.hpp>
|
|
#include <boost/spirit/home/qi/directive/raw.hpp>
|
|
#include <boost/spirit/home/qi/char/char.hpp>
|
|
#include <boost/spirit/home/qi/directive/lexeme.hpp>
|
|
#include <boost/spirit/home/qi/directive/no_skip.hpp>
|
|
#include <boost/spirit/home/qi/numeric/real.hpp>
|
|
#include <boost/spirit/repository/home/qi/primitive/iter_pos.hpp>
|
|
#include <boost/spirit/repository/home/qi/directive/distinct.hpp>
|
|
#include <boost/spirit/home/qi/copy.hpp>
|
|
#include <boost/spirit/home/qi/action/action.hpp>
|
|
#include <boost/spirit/home/qi/nonterminal/error_handler.hpp>
|
|
#include <boost/spirit/home/qi/nonterminal/debug_handler.hpp>
|
|
#include <boost/spirit/home/qi/string/symbols.hpp>
|
|
#include <boost/spirit/home/qi/parse.hpp>
|
|
#include <cstring>
|
|
#include <ctime>
|
|
#include <iomanip>
|
|
#include <random>
|
|
#include <set>
|
|
#include <memory>
|
|
#include <iterator>
|
|
#include <sstream>
|
|
#include <map>
|
|
#include <vector>
|
|
#include <utility>
|
|
#ifdef _MSC_VER
|
|
#include <stdlib.h> // provides **_environ
|
|
#else
|
|
#include <unistd.h> // provides **environ
|
|
#endif
|
|
|
|
#ifdef __APPLE__
|
|
#include <crt_externs.h>
|
|
#undef environ
|
|
#define environ (*_NSGetEnviron())
|
|
#else
|
|
#ifdef _MSC_VER
|
|
#define environ _environ
|
|
#else
|
|
extern char **environ;
|
|
#endif
|
|
#endif
|
|
|
|
#include <boost/algorithm/string.hpp>
|
|
#include <boost/nowide/convert.hpp>
|
|
#include <boost/nowide/cstdlib.hpp>
|
|
|
|
// Spirit v2.5 allows you to suppress automatic generation
|
|
// of predefined terminals to speed up complation. With
|
|
// BOOST_SPIRIT_NO_PREDEFINED_TERMINALS defined, you are
|
|
// responsible in creating instances of the terminals that
|
|
// you need (e.g. see qi::uint_type uint_ below).
|
|
//#define BOOST_SPIRIT_NO_PREDEFINED_TERMINALS
|
|
|
|
#define BOOST_RESULT_OF_USE_DECLTYPE
|
|
#define BOOST_SPIRIT_USE_PHOENIX_V3
|
|
#include <boost/config/warning_disable.hpp>
|
|
#include <boost/lexical_cast.hpp>
|
|
#include <boost/spirit/include/qi.hpp>
|
|
#include <boost/spirit/include/qi_lit.hpp>
|
|
#include <boost/phoenix/core.hpp>
|
|
#include <boost/phoenix/operator.hpp>
|
|
#include <boost/phoenix/fusion.hpp>
|
|
#include <boost/phoenix/stl.hpp>
|
|
#include <boost/phoenix/object.hpp>
|
|
#include <boost/fusion/include/adapt_struct.hpp>
|
|
#include <boost/spirit/repository/include/qi_distinct.hpp>
|
|
#include <boost/spirit/repository/include/qi_iter_pos.hpp>
|
|
#include <boost/variant/recursive_variant.hpp>
|
|
#include <boost/phoenix/bind/bind_function.hpp>
|
|
|
|
#include <iostream>
|
|
#include <string>
|
|
|
|
// #define USE_CPP11_REGEX
|
|
#ifdef USE_CPP11_REGEX
|
|
#include <regex>
|
|
#define SLIC3R_REGEX_NAMESPACE std
|
|
#else /* USE_CPP11_REGEX */
|
|
#include <boost/regex.hpp>
|
|
#define SLIC3R_REGEX_NAMESPACE boost
|
|
#endif /* USE_CPP11_REGEX */
|
|
|
|
namespace Slic3r {
|
|
|
|
PlaceholderParser::PlaceholderParser(const DynamicConfig *external_config) : m_external_config(external_config)
|
|
{
|
|
this->set("version", std::string(SoftFever_VERSION));
|
|
this->apply_env_variables();
|
|
this->update_timestamp();
|
|
this->update_user_name();
|
|
}
|
|
|
|
void PlaceholderParser::update_timestamp(DynamicConfig &config)
|
|
{
|
|
time_t rawtime;
|
|
time(&rawtime);
|
|
struct tm* timeinfo = localtime(&rawtime);
|
|
|
|
{
|
|
std::ostringstream ss;
|
|
ss << (1900 + timeinfo->tm_year);
|
|
ss << std::setw(2) << std::setfill('0') << (1 + timeinfo->tm_mon);
|
|
ss << std::setw(2) << std::setfill('0') << timeinfo->tm_mday;
|
|
ss << "-";
|
|
ss << std::setw(2) << std::setfill('0') << timeinfo->tm_hour;
|
|
ss << std::setw(2) << std::setfill('0') << timeinfo->tm_min;
|
|
ss << std::setw(2) << std::setfill('0') << timeinfo->tm_sec;
|
|
config.set_key_value("timestamp", new ConfigOptionString(ss.str()));
|
|
}
|
|
config.set_key_value("year", new ConfigOptionInt(1900 + timeinfo->tm_year));
|
|
config.set_key_value("month", new ConfigOptionInt(1 + timeinfo->tm_mon));
|
|
config.set_key_value("day", new ConfigOptionInt(timeinfo->tm_mday));
|
|
config.set_key_value("hour", new ConfigOptionInt(timeinfo->tm_hour));
|
|
config.set_key_value("minute", new ConfigOptionInt(timeinfo->tm_min));
|
|
config.set_key_value("second", new ConfigOptionInt(timeinfo->tm_sec));
|
|
}
|
|
|
|
void PlaceholderParser::update_user_name(DynamicConfig &config)
|
|
{
|
|
const char* user = boost::nowide::getenv("USER") ? boost::nowide::getenv("USER") : boost::nowide::getenv("USERNAME") ? boost::nowide::getenv("USERNAME") : "unknown";
|
|
config.set_key_value("user", new ConfigOptionString(user));
|
|
}
|
|
|
|
static inline bool opts_equal(const DynamicConfig &config_old, const DynamicConfig &config_new, const std::string &opt_key)
|
|
{
|
|
const ConfigOption *opt_old = config_old.option(opt_key);
|
|
const ConfigOption *opt_new = config_new.option(opt_key);
|
|
assert(opt_new != nullptr);
|
|
return opt_old != nullptr && *opt_new == *opt_old;
|
|
}
|
|
|
|
std::vector<std::string> PlaceholderParser::config_diff(const DynamicPrintConfig &rhs)
|
|
{
|
|
std::vector<std::string> diff_keys;
|
|
for (const t_config_option_key &opt_key : rhs.keys())
|
|
if (! opts_equal(m_config, rhs, opt_key))
|
|
diff_keys.emplace_back(opt_key);
|
|
return diff_keys;
|
|
}
|
|
|
|
// Scalar configuration values are stored into m_single,
|
|
// vector configuration values are stored into m_multiple.
|
|
// All vector configuration values stored into the PlaceholderParser
|
|
// are expected to be addressed by the extruder ID, therefore
|
|
// if a vector configuration value is addressed without an index,
|
|
// a current extruder ID is used.
|
|
bool PlaceholderParser::apply_config(const DynamicPrintConfig &rhs)
|
|
{
|
|
bool modified = false;
|
|
for (const t_config_option_key &opt_key : rhs.keys()) {
|
|
if (! opts_equal(m_config, rhs, opt_key)) {
|
|
this->set(opt_key, rhs.option(opt_key)->clone());
|
|
modified = true;
|
|
}
|
|
}
|
|
return modified;
|
|
}
|
|
|
|
void PlaceholderParser::apply_only(const DynamicPrintConfig &rhs, const std::vector<std::string> &keys)
|
|
{
|
|
for (const t_config_option_key &opt_key : keys)
|
|
this->set(opt_key, rhs.option(opt_key)->clone());
|
|
}
|
|
|
|
void PlaceholderParser::apply_config(DynamicPrintConfig &&rhs)
|
|
{
|
|
m_config += std::move(rhs);
|
|
}
|
|
|
|
void PlaceholderParser::apply_env_variables()
|
|
{
|
|
for (char** env = environ; *env; ++ env) {
|
|
if (strncmp(*env, "SLIC3R_", 7) == 0) {
|
|
std::stringstream ss(*env);
|
|
std::string key, value;
|
|
std::getline(ss, key, '=');
|
|
ss >> value;
|
|
this->set(key, value);
|
|
}
|
|
}
|
|
}
|
|
|
|
namespace spirit = boost::spirit;
|
|
// Using an encoding, which accepts unsigned chars.
|
|
// Don't use boost::spirit::ascii, as it crashes internally due to indexing with negative char values for UTF8 characters into some 7bit character classification tables.
|
|
//namespace spirit_encoding = boost::spirit::ascii;
|
|
//FIXME iso8859_1 is just a workaround for the problem above. Replace it with UTF8 support!
|
|
namespace spirit_encoding = boost::spirit::iso8859_1;
|
|
namespace qi = boost::spirit::qi;
|
|
namespace px = boost::phoenix;
|
|
|
|
namespace client
|
|
{
|
|
using Iterator = std::string::const_iterator;
|
|
using IteratorRange = boost::iterator_range<Iterator>;
|
|
|
|
struct OptWithPos {
|
|
OptWithPos() {}
|
|
OptWithPos(ConfigOptionConstPtr opt, IteratorRange it_range, bool writable = false) : opt(opt), it_range(it_range), writable(writable) {}
|
|
ConfigOptionConstPtr opt { nullptr };
|
|
bool writable { false };
|
|
// -1 means it is a scalar variable, or it is a vector variable and index was not assigned yet or the whole vector is considered.
|
|
int index { -1 };
|
|
IteratorRange it_range;
|
|
|
|
bool empty() const { return opt == nullptr; }
|
|
bool has_index() const { return index != -1; }
|
|
};
|
|
|
|
std::ostream& operator<<(std::ostream& os, OptWithPos const& opt)
|
|
{
|
|
os << std::string(opt.it_range.begin(), opt.it_range.end());
|
|
return os;
|
|
}
|
|
|
|
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; }
|
|
explicit expr(int i, const Iterator &it_begin, const Iterator &it_end) : m_type(TYPE_INT), it_range(it_begin, it_end) { m_data.i = i; }
|
|
explicit expr(double d) : m_type(TYPE_DOUBLE) { m_data.d = d; }
|
|
explicit expr(double d, const Iterator &it_begin, const Iterator &it_end) : m_type(TYPE_DOUBLE), it_range(it_begin, it_end) { m_data.d = d; }
|
|
explicit expr(const char *s) : m_type(TYPE_STRING) { m_data.s = new std::string(s); }
|
|
explicit expr(const std::string &s) : m_type(TYPE_STRING) { m_data.s = new std::string(s); }
|
|
explicit expr(std::string &&s) : m_type(TYPE_STRING) { m_data.s = new std::string(std::move(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); }
|
|
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;
|
|
}
|
|
expr &operator=(const expr &rhs)
|
|
{
|
|
if (rhs.type() == TYPE_STRING) {
|
|
this->set_s(rhs.s());
|
|
} else {
|
|
m_type = rhs.type();
|
|
m_data.set(rhs.m_data);
|
|
}
|
|
this->it_range = rhs.it_range;
|
|
return *this;
|
|
}
|
|
|
|
expr &operator=(expr &&rhs)
|
|
{
|
|
if (this != &rhs) {
|
|
this->reset();
|
|
m_type = rhs.type();
|
|
this->it_range = rhs.it_range;
|
|
m_data.set(rhs.m_data);
|
|
rhs.m_type = TYPE_EMPTY;
|
|
}
|
|
return *this;
|
|
}
|
|
|
|
void reset()
|
|
{
|
|
if (this->type() == TYPE_STRING)
|
|
delete m_data.s;
|
|
m_type = TYPE_EMPTY;
|
|
}
|
|
~expr() { reset(); }
|
|
|
|
enum Type {
|
|
TYPE_EMPTY = 0,
|
|
TYPE_BOOL,
|
|
TYPE_INT,
|
|
TYPE_DOUBLE,
|
|
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; }
|
|
void set_b(bool v) { this->reset(); this->set_b_lite(v); }
|
|
void set_b_lite(bool v) { assert(this->type() != TYPE_STRING); Data tmp; tmp.b = v; m_data.set(tmp); m_type = TYPE_BOOL; }
|
|
int& i() { return m_data.i; }
|
|
int i() const { return m_data.i; }
|
|
void set_i(int v) { this->reset(); set_i_lite(v); }
|
|
void set_i_lite(int v) { assert(this->type() != TYPE_STRING); Data tmp; tmp.i = v; m_data.set(tmp); m_type = TYPE_INT; }
|
|
int as_i() const { return this->type() == TYPE_INT ? this->i() : int(this->d()); }
|
|
int as_i_rounded() const { return this->type() == TYPE_INT ? this->i() : int(std::round(this->d())); }
|
|
double& d() { return m_data.d; }
|
|
double d() const { return m_data.d; }
|
|
void set_d(double v) { this->reset(); this->set_d_lite(v); }
|
|
void set_d_lite(double v) { assert(this->type() != TYPE_STRING); Data tmp; tmp.d = v; m_data.set(tmp); m_type = TYPE_DOUBLE; }
|
|
double as_d() const { return this->type() == TYPE_DOUBLE ? this->d() : double(this->i()); }
|
|
std::string& s() { return *m_data.s; }
|
|
const std::string& s() const { return *m_data.s; }
|
|
void set_s(const std::string &s) {
|
|
if (this->type() == TYPE_STRING)
|
|
*m_data.s = s;
|
|
else
|
|
this->set_s_take_ownership(new std::string(s));
|
|
}
|
|
void set_s(std::string &&s) {
|
|
if (this->type() == TYPE_STRING)
|
|
*m_data.s = std::move(s);
|
|
else
|
|
this->set_s_take_ownership(new std::string(std::move(s)));
|
|
}
|
|
void set_s(const char *s) {
|
|
if (this->type() == TYPE_STRING)
|
|
*m_data.s = s;
|
|
else
|
|
this->set_s_take_ownership(new std::string(s));
|
|
}
|
|
|
|
std::string to_string() const
|
|
{
|
|
std::string out;
|
|
switch (this->type()) {
|
|
case TYPE_EMPTY:
|
|
// Inside an if / else block to be skipped.
|
|
break;
|
|
case TYPE_BOOL: out = this->b() ? "true" : "false"; break;
|
|
case TYPE_INT: out = std::to_string(this->i()); break;
|
|
case TYPE_DOUBLE:
|
|
#if 0
|
|
// The default converter produces trailing zeros after the decimal point.
|
|
out = std::to_string(data.d);
|
|
#else
|
|
// ostringstream default converter produces no trailing zeros after the decimal point.
|
|
// It seems to be doing what the old boost::to_string() did.
|
|
{
|
|
std::ostringstream ss;
|
|
ss << this->d();
|
|
out = ss.str();
|
|
}
|
|
#endif
|
|
break;
|
|
case TYPE_STRING: out = this->s(); break;
|
|
default: break;
|
|
}
|
|
return out;
|
|
}
|
|
|
|
// Range of input iterators covering this expression.
|
|
// Used for throwing parse exceptions.
|
|
IteratorRange it_range;
|
|
|
|
expr unary_minus(const Iterator start_pos) const
|
|
{
|
|
switch (this->type()) {
|
|
case TYPE_EMPTY:
|
|
// Inside an if / else block to be skipped.
|
|
return expr();
|
|
case TYPE_INT :
|
|
return expr(- this->i(), start_pos, this->it_range.end());
|
|
case TYPE_DOUBLE:
|
|
return expr(- this->d(), start_pos, this->it_range.end());
|
|
default:
|
|
this->throw_exception(L("Cannot apply unary minus operator."));
|
|
}
|
|
assert(false);
|
|
// Suppress compiler warnings.
|
|
return expr();
|
|
}
|
|
|
|
expr unary_integer(const Iterator start_pos) const
|
|
{
|
|
switch (this->type()) {
|
|
case TYPE_EMPTY:
|
|
// Inside an if / else block to be skipped.
|
|
return expr();
|
|
case TYPE_INT:
|
|
return expr(this->i(), start_pos, this->it_range.end());
|
|
case TYPE_DOUBLE:
|
|
return expr(static_cast<int>(this->d()), start_pos, this->it_range.end());
|
|
default:
|
|
this->throw_exception(L("Cannot convert to integer."));
|
|
}
|
|
assert(false);
|
|
// Suppress compiler warnings.
|
|
return expr();
|
|
}
|
|
|
|
expr round(const Iterator start_pos) const
|
|
{
|
|
switch (this->type()) {
|
|
case TYPE_EMPTY:
|
|
// Inside an if / else block to be skipped.
|
|
return expr();
|
|
case TYPE_INT:
|
|
return expr(this->i(), start_pos, this->it_range.end());
|
|
case TYPE_DOUBLE:
|
|
return expr(static_cast<int>(std::round(this->d())), start_pos, this->it_range.end());
|
|
default:
|
|
this->throw_exception(L("Cannot round a non-numeric value."));
|
|
}
|
|
assert(false);
|
|
// Suppress compiler warnings.
|
|
return expr();
|
|
}
|
|
|
|
expr floor(const Iterator start_pos)const
|
|
{
|
|
switch (this->type()) {
|
|
case TYPE_INT:
|
|
return expr(this->i(), start_pos, this->it_range.end());
|
|
case TYPE_DOUBLE:
|
|
return expr(static_cast<int>(std::floor(this->d())), start_pos, this->it_range.end());
|
|
default:
|
|
this->throw_exception(L("Cannot floor a non-numeric value."));
|
|
}
|
|
assert(false);
|
|
// Suppress compiler warnings.
|
|
return expr();
|
|
}
|
|
|
|
expr ceil(const Iterator start_pos)const
|
|
{
|
|
switch (this->type()) {
|
|
case TYPE_INT:
|
|
return expr(this->i(), start_pos, this->it_range.end());
|
|
case TYPE_DOUBLE:
|
|
return expr(static_cast<int>(std::ceil(this->d())), start_pos, this->it_range.end());
|
|
default:
|
|
this->throw_exception(L("Cannot ceil a non-numeric value."));
|
|
}
|
|
assert(false);
|
|
// Suppress compiler warnings.
|
|
return expr();
|
|
}
|
|
|
|
expr unary_not(const Iterator start_pos) const
|
|
{
|
|
switch (this->type()) {
|
|
case TYPE_EMPTY:
|
|
// Inside an if / else block to be skipped.
|
|
return expr();
|
|
case TYPE_BOOL:
|
|
return expr(! this->b(), start_pos, this->it_range.end());
|
|
default:
|
|
this->throw_exception(L("Cannot apply a not operator."));
|
|
}
|
|
assert(false);
|
|
// Suppress compiler warnings.
|
|
return expr();
|
|
}
|
|
|
|
expr &operator+=(const expr &rhs)
|
|
{
|
|
if (this->type() == TYPE_EMPTY) {
|
|
// Inside an if / else block to be skipped.
|
|
} else if (this->type() == TYPE_STRING) {
|
|
// Convert the right hand side to string and append.
|
|
*m_data.s += rhs.to_string();
|
|
} else if (rhs.type() == TYPE_STRING) {
|
|
// Conver the left hand side to string, append rhs.
|
|
this->set_s(this->to_string() + rhs.s());
|
|
} else {
|
|
const char *err_msg = L("Cannot add non-numeric types.");
|
|
this->throw_if_not_numeric(err_msg);
|
|
rhs.throw_if_not_numeric(err_msg);
|
|
if (this->type() == TYPE_DOUBLE || rhs.type() == TYPE_DOUBLE)
|
|
this->set_d_lite(this->as_d() + rhs.as_d());
|
|
else
|
|
m_data.i += rhs.i();
|
|
}
|
|
this->it_range = IteratorRange(this->it_range.begin(), rhs.it_range.end());
|
|
return *this;
|
|
}
|
|
|
|
expr &operator-=(const expr &rhs)
|
|
{
|
|
if (this->type() == TYPE_EMPTY) {
|
|
// Inside an if / else block to be skipped.
|
|
this->reset();
|
|
} else {
|
|
const char *err_msg = L("Cannot subtract non-numeric types.");
|
|
this->throw_if_not_numeric(err_msg);
|
|
rhs.throw_if_not_numeric(err_msg);
|
|
if (this->type() == TYPE_DOUBLE || rhs.type() == TYPE_DOUBLE)
|
|
this->set_d_lite(this->as_d() - rhs.as_d());
|
|
else
|
|
m_data.i -= rhs.i();
|
|
this->it_range = IteratorRange(this->it_range.begin(), rhs.it_range.end());
|
|
}
|
|
return *this;
|
|
}
|
|
|
|
expr &operator*=(const expr &rhs)
|
|
{
|
|
if (this->type() == TYPE_EMPTY) {
|
|
// Inside an if / else block to be skipped.
|
|
this->reset();
|
|
} else {
|
|
const char *err_msg = L("Cannot multiply with non-numeric type.");
|
|
this->throw_if_not_numeric(err_msg);
|
|
rhs.throw_if_not_numeric(err_msg);
|
|
if (this->type() == TYPE_DOUBLE || rhs.type() == TYPE_DOUBLE)
|
|
this->set_d_lite(this->as_d() * rhs.as_d());
|
|
else
|
|
m_data.i *= rhs.i();
|
|
this->it_range = IteratorRange(this->it_range.begin(), rhs.it_range.end());
|
|
}
|
|
return *this;
|
|
}
|
|
|
|
expr &operator/=(const expr &rhs)
|
|
{
|
|
if (this->type() == TYPE_EMPTY) {
|
|
// Inside an if / else block to be skipped.
|
|
this->reset();
|
|
} else {
|
|
this->throw_if_not_numeric(L("Cannot divide a non-numeric type."));
|
|
rhs.throw_if_not_numeric(L("Cannot divide with a non-numeric type."));
|
|
if (rhs.type() == TYPE_INT ? (rhs.i() == 0) : (rhs.d() == 0.))
|
|
rhs.throw_exception(L("Division by zero"));
|
|
if (this->type() == TYPE_DOUBLE || rhs.type() == TYPE_DOUBLE)
|
|
this->set_d_lite(this->as_d() / rhs.as_d());
|
|
else
|
|
m_data.i /= rhs.i();
|
|
this->it_range = IteratorRange(this->it_range.begin(), rhs.it_range.end());
|
|
}
|
|
return *this;
|
|
}
|
|
|
|
expr &operator%=(const expr &rhs)
|
|
{
|
|
if (this->type() == TYPE_EMPTY) {
|
|
// Inside an if / else block to be skipped.
|
|
this->reset();
|
|
} else {
|
|
this->throw_if_not_numeric(L("Cannot divide a non-numeric type."));
|
|
rhs.throw_if_not_numeric(L("Cannot divide with a non-numeric type."));
|
|
if (rhs.type() == TYPE_INT ? (rhs.i() == 0) : (rhs.d() == 0.))
|
|
rhs.throw_exception(L("Division by zero"));
|
|
if (this->type() == TYPE_DOUBLE || rhs.type() == TYPE_DOUBLE)
|
|
this->set_d_lite(std::fmod(this->as_d(), rhs.as_d()));
|
|
else
|
|
m_data.i %= rhs.i();
|
|
this->it_range = IteratorRange(this->it_range.begin(), rhs.it_range.end());
|
|
}
|
|
return *this;
|
|
}
|
|
|
|
static void to_string2(expr &self, std::string &out)
|
|
{
|
|
if (self.type() != TYPE_EMPTY)
|
|
// Not inside an if / else block to be skipped
|
|
out = self.to_string();
|
|
}
|
|
|
|
static void evaluate_boolean(expr &self, bool &out)
|
|
{
|
|
if (self.type() != TYPE_EMPTY) {
|
|
// Not inside an if / else block to be skipped
|
|
if (self.type() != TYPE_BOOL)
|
|
self.throw_exception(L("Not a boolean expression"));
|
|
out = self.b();
|
|
}
|
|
}
|
|
|
|
static void evaluate_boolean_to_string(expr &self, std::string &out)
|
|
{
|
|
assert(self.type() != TYPE_EMPTY);
|
|
if (self.type() != TYPE_BOOL)
|
|
self.throw_exception(L("Not a boolean expression"));
|
|
out = self.b() ? "true" : "false";
|
|
}
|
|
|
|
// Is lhs==rhs? Store the result into lhs.
|
|
static void compare_op(expr &lhs, expr &rhs, char op, bool invert)
|
|
{
|
|
if (lhs.type() == TYPE_EMPTY)
|
|
// Inside an if / else block to be skipped
|
|
return;
|
|
bool value = false;
|
|
if (lhs.numeric_type() && rhs.numeric_type()) {
|
|
// Both types are numeric.
|
|
switch (op) {
|
|
case '=':
|
|
value = (lhs.type() == TYPE_DOUBLE || rhs.type() == TYPE_DOUBLE) ?
|
|
(std::abs(lhs.as_d() - rhs.as_d()) < 1e-8) : (lhs.i() == rhs.i());
|
|
break;
|
|
case '<':
|
|
value = (lhs.type() == TYPE_DOUBLE || rhs.type() == TYPE_DOUBLE) ?
|
|
(lhs.as_d() < rhs.as_d()) : (lhs.i() < rhs.i());
|
|
break;
|
|
case '>':
|
|
default:
|
|
value = (lhs.type() == TYPE_DOUBLE || rhs.type() == TYPE_DOUBLE) ?
|
|
(lhs.as_d() > rhs.as_d()) : (lhs.i() > rhs.i());
|
|
break;
|
|
}
|
|
} else if (lhs.type() == TYPE_BOOL && rhs.type() == TYPE_BOOL) {
|
|
// Both type are bool.
|
|
if (op != '=')
|
|
boost::throw_exception(qi::expectation_failure<Iterator>(
|
|
lhs.it_range.begin(), rhs.it_range.end(), spirit::info(std::string("*") + L("Cannot compare the types."))));
|
|
value = lhs.b() == rhs.b();
|
|
} else if (lhs.type() == TYPE_STRING || rhs.type() == TYPE_STRING) {
|
|
// One type is string, the other could be converted to string.
|
|
value = (op == '=') ? (lhs.to_string() == rhs.to_string()) :
|
|
(op == '<') ? (lhs.to_string() < rhs.to_string()) : (lhs.to_string() > rhs.to_string());
|
|
} else {
|
|
boost::throw_exception(qi::expectation_failure<Iterator>(
|
|
lhs.it_range.begin(), rhs.it_range.end(), spirit::info(std::string("*") + L("Cannot compare the types."))));
|
|
}
|
|
lhs.reset();
|
|
lhs.set_b_lite(invert ? ! value : value);
|
|
}
|
|
// Compare operators, store the result into lhs.
|
|
static void equal (expr &lhs, expr &rhs) { compare_op(lhs, rhs, '=', false); }
|
|
static void not_equal(expr &lhs, expr &rhs) { compare_op(lhs, rhs, '=', true ); }
|
|
static void lower (expr &lhs, expr &rhs) { compare_op(lhs, rhs, '<', false); }
|
|
static void greater (expr &lhs, expr &rhs) { compare_op(lhs, rhs, '>', false); }
|
|
static void leq (expr &lhs, expr &rhs) { compare_op(lhs, rhs, '>', true ); }
|
|
static void geq (expr &lhs, expr &rhs) { compare_op(lhs, rhs, '<', true ); }
|
|
|
|
static void throw_if_not_numeric(const expr ¶m)
|
|
{
|
|
const char *err_msg = L("Not a numeric type.");
|
|
param.throw_if_not_numeric(err_msg);
|
|
}
|
|
|
|
enum Function2ParamsType {
|
|
FUNCTION_MIN,
|
|
FUNCTION_MAX,
|
|
};
|
|
// Store the result into param1.
|
|
static void function_2params(expr ¶m1, expr ¶m2, Function2ParamsType fun)
|
|
{
|
|
if (param1.type() == TYPE_EMPTY)
|
|
// Inside an if / else block to be skipped
|
|
return;
|
|
throw_if_not_numeric(param1);
|
|
throw_if_not_numeric(param2);
|
|
if (param1.type() == TYPE_DOUBLE || param2.type() == TYPE_DOUBLE) {
|
|
double d = 0.;
|
|
switch (fun) {
|
|
case FUNCTION_MIN: d = std::min(param1.as_d(), param2.as_d()); break;
|
|
case FUNCTION_MAX: d = std::max(param1.as_d(), param2.as_d()); break;
|
|
default: param1.throw_exception("Internal error: invalid function");
|
|
}
|
|
param1.set_d_lite(d);
|
|
} else {
|
|
int i = 0;
|
|
switch (fun) {
|
|
case FUNCTION_MIN: i = std::min(param1.as_i(), param2.as_i()); break;
|
|
case FUNCTION_MAX: i = std::max(param1.as_i(), param2.as_i()); break;
|
|
default: param1.throw_exception("Internal error: invalid function");
|
|
}
|
|
param1.set_i_lite(i);
|
|
}
|
|
}
|
|
// Store the result into param1.
|
|
static void min(expr ¶m1, expr ¶m2) { function_2params(param1, param2, FUNCTION_MIN); }
|
|
static void max(expr ¶m1, expr ¶m2) { function_2params(param1, param2, FUNCTION_MAX); }
|
|
|
|
// Store the result into param1.
|
|
static void random(expr ¶m1, expr ¶m2, std::mt19937 &rng)
|
|
{
|
|
if (param1.type() == TYPE_EMPTY)
|
|
// Inside an if / else block to be skipped
|
|
return;
|
|
throw_if_not_numeric(param1);
|
|
throw_if_not_numeric(param2);
|
|
if (param1.type() == TYPE_DOUBLE || param2.type() == TYPE_DOUBLE)
|
|
param1.set_d_lite(std::uniform_real_distribution<>(param1.as_d(), param2.as_d())(rng));
|
|
else
|
|
param1.set_i_lite(std::uniform_int_distribution<>(param1.as_i(), param2.as_i())(rng));
|
|
}
|
|
|
|
// Store the result into param1.
|
|
// param3 is optional
|
|
template<bool leading_zeros>
|
|
static void digits(expr ¶m1, expr ¶m2, expr ¶m3)
|
|
{
|
|
if (param1.type() == TYPE_EMPTY)
|
|
// Inside an if / else block to be skipped
|
|
return;
|
|
throw_if_not_numeric(param1);
|
|
if (param2.type() != TYPE_INT)
|
|
param2.throw_exception(L("digits: second parameter must be integer"));
|
|
bool has_decimals = param3.type() != TYPE_EMPTY;
|
|
if (has_decimals && param3.type() != TYPE_INT)
|
|
param3.throw_exception(L("digits: third parameter must be integer"));
|
|
|
|
char buf[256];
|
|
int ndigits = std::clamp(param2.as_i(), 0, 64);
|
|
if (has_decimals) {
|
|
// Format as double.
|
|
int decimals = std::clamp(param3.as_i(), 0, 64);
|
|
sprintf(buf, leading_zeros ? "%0*.*lf" : "%*.*lf", ndigits, decimals, param1.as_d());
|
|
} else
|
|
// Format as int.
|
|
sprintf(buf, leading_zeros ? "%0*d" : "%*d", ndigits, param1.as_i_rounded());
|
|
param1.set_s(buf);
|
|
}
|
|
|
|
static void regex_op(const expr &lhs, IteratorRange &rhs, char op, expr &out)
|
|
{
|
|
if (lhs.type() == TYPE_EMPTY)
|
|
// Inside an if / else block to be skipped
|
|
return;
|
|
const std::string *subject = nullptr;
|
|
if (lhs.type() == TYPE_STRING) {
|
|
// One type is string, the other could be converted to string.
|
|
subject = &lhs.s();
|
|
} else {
|
|
lhs.throw_exception(L("Left hand side of a regex match must be a string."));
|
|
}
|
|
try {
|
|
std::string pattern(++ rhs.begin(), -- rhs.end());
|
|
bool result = SLIC3R_REGEX_NAMESPACE::regex_match(*subject, SLIC3R_REGEX_NAMESPACE::regex(pattern));
|
|
if (op == '!')
|
|
result = ! result;
|
|
out.set_b(result);
|
|
} catch (SLIC3R_REGEX_NAMESPACE::regex_error &ex) {
|
|
// Syntax error in the regular expression
|
|
boost::throw_exception(qi::expectation_failure<Iterator>(
|
|
rhs.begin(), rhs.end(), spirit::info("*" + Slic3r::format(_u8L("Regular expression compilation failed: %1%"), ex.what()))));
|
|
}
|
|
}
|
|
|
|
static void regex_matches (expr &lhs, IteratorRange &rhs) { return regex_op(lhs, rhs, '=', lhs); }
|
|
static void regex_doesnt_match(expr &lhs, IteratorRange &rhs) { return regex_op(lhs, rhs, '!', lhs); }
|
|
|
|
// Replace every match of the regular expression 'pattern' in the string 'subject' with 'replacement'.
|
|
// The replacement may reference capture groups ($1, $2, ...). Store the result into subject.
|
|
static void regex_replace(expr &subject, IteratorRange &pattern, expr &replacement)
|
|
{
|
|
if (subject.type() == TYPE_EMPTY)
|
|
// Inside an if / else block to be skipped
|
|
return;
|
|
if (subject.type() != TYPE_STRING)
|
|
subject.throw_exception(L("regex_replace() first parameter must be a string."));
|
|
try {
|
|
std::string re(++ pattern.begin(), -- pattern.end());
|
|
std::string result = SLIC3R_REGEX_NAMESPACE::regex_replace(subject.s(), SLIC3R_REGEX_NAMESPACE::regex(re), replacement.to_string());
|
|
subject.set_s(std::move(result));
|
|
} catch (SLIC3R_REGEX_NAMESPACE::regex_error &ex) {
|
|
// Syntax error in the regular expression
|
|
boost::throw_exception(qi::expectation_failure<Iterator>(
|
|
pattern.begin(), pattern.end(), spirit::info("*" + Slic3r::format(_u8L("Regular expression compilation failed: %1%"), ex.what()))));
|
|
}
|
|
}
|
|
|
|
static void one_of_test_init(expr &out) {
|
|
out.set_b(false);
|
|
}
|
|
template<bool RegEx>
|
|
static void one_of_test(const expr &match, const expr &pattern, expr &out) {
|
|
if (match.type() == TYPE_EMPTY) {
|
|
// Inside an if / else block to be skipped
|
|
out.reset();
|
|
return;
|
|
}
|
|
if (! out.b()) {
|
|
if (match.type() != TYPE_STRING)
|
|
match.throw_exception(L("one_of(): First parameter (the string to match against) has to be a string value"));
|
|
if (pattern.type() != TYPE_STRING)
|
|
match.throw_exception(L("one_of(): Pattern has to be a string value"));
|
|
if (RegEx) {
|
|
try {
|
|
out.set_b(SLIC3R_REGEX_NAMESPACE::regex_match(match.s(), SLIC3R_REGEX_NAMESPACE::regex(pattern.s())));
|
|
} catch (SLIC3R_REGEX_NAMESPACE::regex_error &) {
|
|
// Syntax error in the regular expression
|
|
pattern.throw_exception(L("Regular expression compilation failed"));
|
|
}
|
|
} else
|
|
out.set_b(match.s() == pattern.s());
|
|
}
|
|
}
|
|
static void one_of_test_regex(const expr &match, IteratorRange &pattern, expr &out) {
|
|
if (match.type() == TYPE_EMPTY) {
|
|
// Inside an if / else block to be skipped
|
|
out.reset();
|
|
return;
|
|
}
|
|
if (! out.b()) {
|
|
if (match.type() != TYPE_STRING)
|
|
match.throw_exception(L("one_of(): First parameter (the string to match against) has to be a string value"));
|
|
regex_op(match, pattern, '=', out);
|
|
}
|
|
}
|
|
|
|
static void logical_op(expr &lhs, expr &rhs, char op)
|
|
{
|
|
if (lhs.type() == TYPE_EMPTY)
|
|
// Inside an if / else block to be skipped
|
|
return;
|
|
bool value = false;
|
|
if (lhs.type() == TYPE_BOOL && rhs.type() == TYPE_BOOL) {
|
|
value = (op == '|') ? (lhs.b() || rhs.b()) : (lhs.b() && rhs.b());
|
|
} else {
|
|
boost::throw_exception(qi::expectation_failure<Iterator>(
|
|
lhs.it_range.begin(), rhs.it_range.end(), spirit::info(std::string("*") + L("Cannot apply logical operation to non-boolean operands."))));
|
|
}
|
|
lhs.set_b_lite(value);
|
|
}
|
|
static void logical_or (expr &lhs, expr &rhs) { logical_op(lhs, rhs, '|'); }
|
|
static void logical_and(expr &lhs, expr &rhs) { logical_op(lhs, rhs, '&'); }
|
|
|
|
void throw_exception(const char *message) const
|
|
{
|
|
boost::throw_exception(qi::expectation_failure<Iterator>(
|
|
this->it_range.begin(), this->it_range.end(), spirit::info(std::string("*") + message)));
|
|
}
|
|
|
|
void throw_if_not_numeric(const char *message) const
|
|
{
|
|
if (! this->numeric_type())
|
|
this->throw_exception(message);
|
|
}
|
|
|
|
private:
|
|
// This object will take ownership of the parameter string object "s".
|
|
void set_s_take_ownership(std::string* s) { assert(this->type() != TYPE_STRING); Data tmp; tmp.s = s; m_data.set(tmp); m_type = TYPE_STRING; }
|
|
|
|
Type m_type = TYPE_EMPTY;
|
|
|
|
union Data {
|
|
bool b;
|
|
int i;
|
|
double d;
|
|
std::string *s;
|
|
|
|
// Copy the largest member variable through char*, which will alias with all other union members by default.
|
|
void set(const Data &rhs) { memcpy(this, &rhs, sizeof(rhs)); }
|
|
} m_data;
|
|
};
|
|
|
|
std::ostream& operator<<(std::ostream &os, const expr &expression)
|
|
{
|
|
typedef expr Expr;
|
|
os << std::string(expression.it_range.begin(), expression.it_range.end()) << " - ";
|
|
switch (expression.type()) {
|
|
case Expr::TYPE_EMPTY: os << "empty"; break;
|
|
case Expr::TYPE_BOOL: os << "bool (" << expression.b() << ")"; break;
|
|
case Expr::TYPE_INT: os << "int (" << expression.i() << ")"; break;
|
|
case Expr::TYPE_DOUBLE: os << "double (" << expression.d() << ")"; break;
|
|
case Expr::TYPE_STRING: os << "string (" << expression.s() << ")"; break;
|
|
default: os << "unknown";
|
|
};
|
|
return os;
|
|
}
|
|
|
|
struct MyContext : public ConfigOptionResolver {
|
|
// Config provided as a parameter to PlaceholderParser invocation, overriding PlaceholderParser stored config.
|
|
const DynamicConfig *external_config = nullptr;
|
|
// Config stored inside PlaceholderParser.
|
|
const DynamicConfig *config = nullptr;
|
|
// Config provided as a parameter to PlaceholderParser invocation, evaluated after the two configs above.
|
|
const DynamicConfig *config_override = nullptr;
|
|
// Config provided as a parameter to PlaceholderParser invocation, containing variables that will be read out
|
|
// and processed by the PlaceholderParser callee.
|
|
mutable DynamicConfig *config_outputs = nullptr;
|
|
// Local variables, read / write
|
|
mutable DynamicConfig config_local;
|
|
size_t current_extruder_id = 0; // This is filament_id actually
|
|
// 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.
|
|
bool just_boolean_expression = false;
|
|
std::string error_message;
|
|
// Local variables declared in {if} branches that were not taken, see PlaceholderParser::check_inactive_branches.
|
|
mutable std::set<std::string> inactive_local_variables;
|
|
|
|
// Table to translate symbol tag to a human readable error message.
|
|
static std::map<std::string, std::string> tag_to_error_message;
|
|
|
|
size_t get_extruder_id() const {
|
|
if (external_config != nullptr) {
|
|
const ConfigOptionInts * filament_map_opt = external_config->option<ConfigOptionInts>("filament_map");
|
|
if (filament_map_opt && current_extruder_id < filament_map_opt->values.size()) {
|
|
return filament_map_opt->values[current_extruder_id];
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static bool evaluate_full_macro(const MyContext *ctx) { return ! ctx->just_boolean_expression; }
|
|
|
|
// Entering a conditional block.
|
|
static void block_enter(const MyContext *ctx, const bool condition)
|
|
{
|
|
if (ctx->skipping() || ! condition)
|
|
++ ctx->m_depth_suppressed;
|
|
}
|
|
// Exiting a conditional block.
|
|
static void block_exit(const MyContext *ctx, const bool condition, bool ¬_yet_consumed, std::string &data_in, std::string &data_out)
|
|
{
|
|
if (ctx->skipping())
|
|
-- ctx->m_depth_suppressed;
|
|
else if (condition && not_yet_consumed) {
|
|
data_out = std::move(data_in);
|
|
not_yet_consumed = false;
|
|
}
|
|
}
|
|
static void block_exit_ternary(const MyContext* ctx, const bool condition, expr &data_in, expr &data_out)
|
|
{
|
|
if (ctx->skipping())
|
|
-- ctx->m_depth_suppressed;
|
|
else if (condition)
|
|
data_out = std::move(data_in);
|
|
}
|
|
// Inside a block, which is conditionally suppressed?
|
|
bool skipping() const { return m_depth_suppressed > 0; }
|
|
// Are variable names resolved inside the suppressed blocks too?
|
|
bool check_inactive_names() const { return PlaceholderParser::check_inactive_branches && ! just_boolean_expression; }
|
|
|
|
const ConfigOption* optptr(const t_config_option_key &opt_key) const override
|
|
{
|
|
const ConfigOption *opt = nullptr;
|
|
if (config_override != nullptr)
|
|
opt = config_override->option(opt_key);
|
|
if (opt == nullptr)
|
|
opt = config->option(opt_key);
|
|
if (opt == nullptr && external_config != nullptr)
|
|
opt = external_config->option(opt_key);
|
|
return opt;
|
|
}
|
|
|
|
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 {
|
|
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, std::unique_ptr<ConfigOption> &&opt, bool global_variable) {
|
|
assert(opt);
|
|
if (global_variable) {
|
|
assert(this->context_data != nullptr && this->context_data->global_config);
|
|
this->context_data->global_config->set_key_value(opt_key, opt.release());
|
|
} else
|
|
this->config_local.set_key_value(opt_key, opt.release());
|
|
}
|
|
|
|
static void legacy_variable_expansion(const MyContext *ctx, IteratorRange &opt_key, std::string &output)
|
|
{
|
|
if (ctx->skipping() && ! ctx->check_inactive_names())
|
|
return;
|
|
|
|
std::string opt_key_str(opt_key.begin(), opt_key.end());
|
|
const ConfigOption *opt = ctx->resolve_symbol(opt_key_str);
|
|
size_t idx = ctx->current_extruder_id;
|
|
if (opt == nullptr) {
|
|
// Check whether this is a legacy vector indexing.
|
|
idx = opt_key_str.rfind('_');
|
|
if (idx != std::string::npos) {
|
|
opt = ctx->resolve_symbol(opt_key_str.substr(0, idx));
|
|
if (opt != nullptr) {
|
|
if (! opt->is_vector())
|
|
ctx->throw_exception(L("Trying to index a scalar variable"), opt_key);
|
|
char *endptr = nullptr;
|
|
idx = strtol(opt_key_str.c_str() + idx + 1, &endptr, 10);
|
|
if (endptr == nullptr || *endptr != 0)
|
|
ctx->throw_exception(L("Invalid vector index"), IteratorRange(opt_key.begin() + idx + 1, opt_key.end()));
|
|
}
|
|
}
|
|
}
|
|
if (opt == nullptr)
|
|
ctx->throw_exception(ctx->skipping() ? L("Variable does not exist (in an inactive branch)") : L("Variable does not exist"), opt_key);
|
|
if (ctx->skipping())
|
|
return;
|
|
if (opt->is_scalar()) {
|
|
if (opt->is_nil())
|
|
ctx->throw_exception(L("Trying to reference an undefined (nil) optional variable"), opt_key);
|
|
output = opt->serialize();
|
|
} else {
|
|
const ConfigOptionVectorBase *vec = static_cast<const ConfigOptionVectorBase*>(opt);
|
|
if (vec->empty())
|
|
ctx->throw_exception(L("Indexing an empty vector variable"), opt_key);
|
|
if (idx >= vec->size())
|
|
idx = 0;
|
|
if (vec->is_nil(idx))
|
|
ctx->throw_exception(L("Trying to reference an undefined (nil) element of vector of optional values"), opt_key);
|
|
output = vec->vserialize()[idx];
|
|
}
|
|
}
|
|
|
|
static void legacy_variable_expansion2(
|
|
const MyContext *ctx,
|
|
IteratorRange &opt_key,
|
|
IteratorRange &opt_vector_index,
|
|
std::string &output)
|
|
{
|
|
if (ctx->skipping() && ! ctx->check_inactive_names())
|
|
return;
|
|
|
|
const char *not_found = ctx->skipping() ? L("Variable does not exist (in an inactive branch)") : L("Variable does not exist");
|
|
std::string opt_key_str(opt_key.begin(), opt_key.end());
|
|
const ConfigOption *opt = ctx->resolve_symbol(opt_key_str);
|
|
if (opt == nullptr) {
|
|
// Check whether the opt_key ends with '_'.
|
|
if (opt_key_str.back() == '_') {
|
|
opt_key_str.resize(opt_key_str.size() - 1);
|
|
opt = ctx->resolve_symbol(opt_key_str);
|
|
}
|
|
if (opt == nullptr)
|
|
ctx->throw_exception(not_found, opt_key);
|
|
}
|
|
if (! opt->is_vector())
|
|
ctx->throw_exception(L("Trying to index a scalar variable"), opt_key);
|
|
const ConfigOption *opt_index = ctx->resolve_symbol(std::string(opt_vector_index.begin(), opt_vector_index.end()));
|
|
if (opt_index == nullptr)
|
|
ctx->throw_exception(not_found, opt_key);
|
|
if (opt_index->type() != coInt)
|
|
ctx->throw_exception(L("Indexing variable has to be integer"), opt_key);
|
|
if (ctx->skipping())
|
|
return;
|
|
const ConfigOptionVectorBase *vec = static_cast<const ConfigOptionVectorBase*>(opt);
|
|
if (vec->empty())
|
|
ctx->throw_exception(L("Indexing an empty vector variable"), opt_key);
|
|
int idx = opt_index->getInt();
|
|
if (idx < 0)
|
|
ctx->throw_exception(L("Negative vector index"), opt_key);
|
|
if (idx >= (int)vec->size())
|
|
idx = 0;
|
|
if (vec->is_nil(idx))
|
|
ctx->throw_exception(L("Trying to reference an undefined (nil) element of vector of optional values"), opt_key);
|
|
output = vec->vserialize()[idx];
|
|
}
|
|
|
|
static void resolve_variable(
|
|
const MyContext *ctx,
|
|
IteratorRange &opt_key,
|
|
OptWithPos &output)
|
|
{
|
|
if (! ctx->skipping()) {
|
|
const std::string key{ opt_key.begin(), opt_key.end() };
|
|
const ConfigOption *opt = ctx->resolve_symbol(key);
|
|
if (opt == nullptr) {
|
|
opt = ctx->resolve_output_symbol(key);
|
|
if (opt == nullptr)
|
|
ctx->throw_exception(L("Not a variable name"), opt_key);
|
|
output.writable = true;
|
|
}
|
|
output.opt = opt;
|
|
} else if (ctx->check_inactive_names()) {
|
|
// Only check the name. Back tracking may resolve the same identifier twice, so there are no side effects.
|
|
const std::string key{ opt_key.begin(), opt_key.end() };
|
|
if (ctx->resolve_symbol(key) == nullptr && ctx->resolve_output_symbol(key) == nullptr &&
|
|
ctx->inactive_local_variables.count(key) == 0 &&
|
|
(ctx->context_data == nullptr || ctx->context_data->inactive_global_variables.count(key) == 0))
|
|
ctx->throw_exception(L("Not a variable name (in an inactive branch)"), opt_key);
|
|
}
|
|
output.it_range = opt_key;
|
|
}
|
|
|
|
static void store_variable_index(
|
|
const MyContext *ctx,
|
|
OptWithPos &opt,
|
|
int index,
|
|
Iterator it_end,
|
|
OptWithPos &output)
|
|
{
|
|
if (! ctx->skipping()) {
|
|
if (index < 0)
|
|
index = 0; // Orca: fallback to first element if index < 0, this matches the behavior of BambuStudio
|
|
if (!opt.opt->is_vector())
|
|
index = -1; // Orca: ignore the index if variable is scalar, this matches the behavior of BambuStudio
|
|
output = opt;
|
|
output.index = index;
|
|
} else
|
|
output = opt;
|
|
output.it_range.end() = it_end;
|
|
}
|
|
|
|
// Evaluating a scalar variable into expr,
|
|
// all possible ConfigOption types are supported.
|
|
static void scalar_variable_to_expr(const MyContext *ctx, OptWithPos &opt, expr &output)
|
|
{
|
|
if (ctx->skipping())
|
|
return;
|
|
|
|
assert(opt.opt->is_scalar());
|
|
|
|
if (opt.opt->is_nil())
|
|
ctx->throw_exception(L("Trying to reference an undefined (nil) optional variable"), opt.it_range);
|
|
|
|
switch (opt.opt->type()) {
|
|
case coFloat: output.set_d(opt.opt->getFloat()); break;
|
|
case coInt: output.set_i(opt.opt->getInt()); break;
|
|
case coString: output.set_s(static_cast<const ConfigOptionString*>(opt.opt)->value); break;
|
|
case coPercent: output.set_d(opt.opt->getFloat()); break;
|
|
case coEnum:
|
|
case coPoint: output.set_s(opt.opt->serialize()); break;
|
|
case coBool: output.set_b(opt.opt->getBool()); break;
|
|
case coFloatOrPercent:
|
|
{
|
|
std::string opt_key(opt.it_range.begin(), opt.it_range.end());
|
|
if (boost::ends_with(opt_key, "line_width")) {
|
|
// Line width supports defaults and a complex graph of dependencies.
|
|
output.set_d(Flow::extrusion_width(opt_key, *ctx, static_cast<unsigned int>(ctx->current_extruder_id)));
|
|
} else if (! static_cast<const ConfigOptionFloatOrPercent*>(opt.opt)->percent) {
|
|
// Not a percent, just return the value.
|
|
output.set_d(opt.opt->getFloat());
|
|
} else {
|
|
// Resolve dependencies using the "ratio_over" link to a parent value.
|
|
const ConfigOptionDef *opt_def = print_config_def.get(opt_key);
|
|
assert(opt_def != nullptr);
|
|
double v = opt.opt->getFloat() * 0.01; // percent to ratio
|
|
for (;;) {
|
|
const ConfigOption *opt_parent = opt_def->ratio_over.empty() ? nullptr : ctx->resolve_symbol(opt_def->ratio_over);
|
|
if (opt_parent == nullptr)
|
|
ctx->throw_exception(L("FloatOrPercent variable failed to resolve the \"ratio_over\" dependencies"), opt.it_range);
|
|
if (boost::ends_with(opt_def->ratio_over, "line_width")) {
|
|
// Line width supports defaults and a complex graph of dependencies.
|
|
assert(opt_parent->type() == coFloatOrPercent);
|
|
v *= Flow::extrusion_width(opt_def->ratio_over, static_cast<const ConfigOptionFloatOrPercent*>(opt_parent), *ctx, static_cast<unsigned int>(ctx->current_extruder_id));
|
|
break;
|
|
}
|
|
if (opt_parent->type() == coFloat || opt_parent->type() == coFloatOrPercent) {
|
|
v *= opt_parent->getFloat();
|
|
if (opt_parent->type() == coFloat || ! static_cast<const ConfigOptionFloatOrPercent*>(opt_parent)->percent)
|
|
break;
|
|
v *= 0.01; // percent to ratio
|
|
}
|
|
// Continue one level up in the "ratio_over" hierarchy.
|
|
opt_def = print_config_def.get(opt_def->ratio_over);
|
|
assert(opt_def != nullptr);
|
|
}
|
|
output.set_d(v);
|
|
}
|
|
break;
|
|
}
|
|
default:
|
|
ctx->throw_exception(L("Unsupported scalar variable type"), opt.it_range);
|
|
}
|
|
}
|
|
|
|
// Evaluating one element of a vector variable.
|
|
// all possible ConfigOption types are supported.
|
|
static void vector_element_to_expr(const MyContext *ctx, OptWithPos &opt, expr &output)
|
|
{
|
|
if (ctx->skipping())
|
|
return;
|
|
|
|
assert(opt.opt->is_vector());
|
|
const ConfigOptionVectorBase* vec = static_cast<const ConfigOptionVectorBase*>(opt.opt);
|
|
if (vec->empty())
|
|
ctx->throw_exception(L("Indexing an empty vector variable"), opt.it_range);
|
|
|
|
// Helper to resolve a FloatOrPercent value (handles ratio_over chain for percent values).
|
|
// elem_index: the element index used to access this vector element, so that
|
|
// parent vectors (via ratio_over) use the same index rather than the current extruder.
|
|
auto resolve_float_or_percent = [ctx, &opt, &output](const FloatOrPercent &fop, size_t elem_index) {
|
|
std::string opt_key(opt.it_range.begin(), opt.it_range.end());
|
|
if (boost::ends_with(opt_key, "line_width")) {
|
|
// Line width supports defaults and a complex graph of dependencies.
|
|
output.set_d(Flow::extrusion_width(opt_key, *ctx, static_cast<unsigned int>(ctx->current_extruder_id)));
|
|
} else if (! fop.percent) {
|
|
// Not a percent, just return the value.
|
|
output.set_d(fop.value);
|
|
} else {
|
|
// Resolve dependencies using the "ratio_over" link to a parent value.
|
|
const ConfigOptionDef *opt_def = print_config_def.get(opt_key);
|
|
assert(opt_def != nullptr);
|
|
double v = fop.value * 0.01; // percent to ratio
|
|
for (;;) {
|
|
const ConfigOption *opt_parent = opt_def->ratio_over.empty() ? nullptr : ctx->resolve_symbol(opt_def->ratio_over);
|
|
if (opt_parent == nullptr)
|
|
ctx->throw_exception(L("FloatOrPercent variable failed to resolve the \"ratio_over\" dependencies"), opt.it_range);
|
|
if (boost::ends_with(opt_def->ratio_over, "line_width")) {
|
|
// Line width supports defaults and a complex graph of dependencies.
|
|
assert(opt_parent->type() == coFloatOrPercent);
|
|
v *= Flow::extrusion_width(opt_def->ratio_over, static_cast<const ConfigOptionFloatOrPercent*>(opt_parent), *ctx, static_cast<unsigned int>(ctx->current_extruder_id));
|
|
break;
|
|
}
|
|
if (opt_parent->type() == coFloat || opt_parent->type() == coFloatOrPercent) {
|
|
v *= opt_parent->getFloat();
|
|
if (opt_parent->type() == coFloat || ! static_cast<const ConfigOptionFloatOrPercent*>(opt_parent)->percent)
|
|
break;
|
|
v *= 0.01; // percent to ratio
|
|
} else if (opt_parent->type() == coFloats) {
|
|
// Vector parent: extract the value for the current extruder.
|
|
const ConfigOptionFloatsNullable *parent_nullable = dynamic_cast<const ConfigOptionFloatsNullable *>(opt_parent);
|
|
if (parent_nullable) {
|
|
v *= (parent_nullable->size() == 1) ? parent_nullable->get_at(0) : parent_nullable->get_at(elem_index);
|
|
} else {
|
|
const ConfigOptionFloats *parent_vec = static_cast<const ConfigOptionFloats *>(opt_parent);
|
|
v *= (parent_vec->size() == 1) ? parent_vec->get_at(0) : parent_vec->get_at(elem_index);
|
|
}
|
|
break;
|
|
} else if (opt_parent->type() == coFloatsOrPercents) {
|
|
// Vector parent with percent support: extract the FloatOrPercent for the current extruder.
|
|
const ConfigOptionFloatsOrPercentsNullable *parent_nullable = dynamic_cast<const ConfigOptionFloatsOrPercentsNullable *>(opt_parent);
|
|
if (parent_nullable) {
|
|
const FloatOrPercent &parent_fop = (parent_nullable->size() == 1) ? parent_nullable->get_at(0) : parent_nullable->get_at(elem_index);
|
|
if (! parent_fop.percent) {
|
|
v *= parent_fop.value;
|
|
break;
|
|
}
|
|
v *= parent_fop.value * 0.01; // percent to ratio
|
|
} else {
|
|
const ConfigOptionFloatsOrPercents *parent_vec = static_cast<const ConfigOptionFloatsOrPercents *>(opt_parent);
|
|
const FloatOrPercent &parent_fop = (parent_vec->size() == 1) ? parent_vec->get_at(0) : parent_vec->get_at(elem_index);
|
|
if (! parent_fop.percent) {
|
|
v *= parent_fop.value;
|
|
break;
|
|
}
|
|
v *= parent_fop.value * 0.01; // percent to ratio
|
|
}
|
|
}
|
|
// Continue one level up in the "ratio_over" hierarchy.
|
|
opt_def = print_config_def.get(opt_def->ratio_over);
|
|
assert(opt_def != nullptr);
|
|
}
|
|
output.set_d(v);
|
|
}
|
|
};
|
|
|
|
if (!opt.has_index()) {
|
|
// Allow omitting extruder id when referencing vectors
|
|
switch (opt.opt->type()) {
|
|
case coFloats: {
|
|
const ConfigOptionFloatsNullable* opt_floatsnullable = dynamic_cast<const ConfigOptionFloatsNullable *>(opt.opt);
|
|
if (opt_floatsnullable) {
|
|
if (opt_floatsnullable->size() == 1) { // old version
|
|
output.set_d(opt_floatsnullable->get_at(0));
|
|
} else {
|
|
output.set_d(opt_floatsnullable->get_at(ctx->get_extruder_id()));
|
|
}
|
|
} else {
|
|
const ConfigOptionFloats* opt_floats = static_cast<const ConfigOptionFloats*>(opt.opt);
|
|
if (opt_floats->size() == 1) { // old version
|
|
output.set_d(opt_floats->get_at(0));
|
|
} else {
|
|
output.set_d(opt_floats->get_at(ctx->get_extruder_id()));
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case coFloatsOrPercents: {
|
|
const ConfigOptionFloatsOrPercentsNullable *opt_vec_nullable = dynamic_cast<const ConfigOptionFloatsOrPercentsNullable *>(opt.opt);
|
|
if (opt_vec_nullable) {
|
|
size_t elem_index = (opt_vec_nullable->size() == 1) ? 0 : ctx->get_extruder_id();
|
|
resolve_float_or_percent(opt_vec_nullable->get_at(elem_index), elem_index);
|
|
} else {
|
|
const ConfigOptionFloatsOrPercents *opt_vec = static_cast<const ConfigOptionFloatsOrPercents *>(opt.opt);
|
|
size_t elem_index = (opt_vec->size() == 1) ? 0 : ctx->get_extruder_id();
|
|
resolve_float_or_percent(opt_vec->get_at(elem_index), elem_index);
|
|
}
|
|
break;
|
|
}
|
|
default: ctx->throw_exception(L("Referencing a vector variable when scalar is expected"), opt.it_range);
|
|
}
|
|
} else {
|
|
size_t idx = (opt.index < 0) ? 0 : (opt.index >= int(vec->size())) ? 0 : size_t(opt.index);
|
|
if (vec->is_nil(idx))
|
|
ctx->throw_exception(L("Trying to reference an undefined (nil) element of vector of optional values"), opt.it_range);
|
|
switch (opt.opt->type()) {
|
|
case coFloats: output.set_d(static_cast<const ConfigOptionFloats*>(opt.opt)->values[idx]); break;
|
|
case coInts: output.set_i(static_cast<const ConfigOptionInts*>(opt.opt)->values[idx]); break;
|
|
case coStrings: output.set_s(static_cast<const ConfigOptionStrings*>(opt.opt)->values[idx]); break;
|
|
case coPercents: output.set_d(static_cast<const ConfigOptionPercents*>(opt.opt)->values[idx]); break;
|
|
case coPoints: output.set_s(to_string(static_cast<const ConfigOptionPoints*>(opt.opt)->values[idx])); break;
|
|
case coBools: output.set_b(static_cast<const ConfigOptionBools*>(opt.opt)->values[idx] != 0); break;
|
|
case coEnums: output.set_i(static_cast<const ConfigOptionInts *>(opt.opt)->values[idx]); break;
|
|
case coFloatsOrPercents: {
|
|
const ConfigOptionFloatsOrPercentsNullable *opt_vec_nullable = dynamic_cast<const ConfigOptionFloatsOrPercentsNullable *>(opt.opt);
|
|
if (opt_vec_nullable) {
|
|
resolve_float_or_percent(opt_vec_nullable->values[idx], idx);
|
|
} else {
|
|
resolve_float_or_percent(static_cast<const ConfigOptionFloatsOrPercents *>(opt.opt)->values[idx], idx);
|
|
}
|
|
break;
|
|
}
|
|
default:
|
|
ctx->throw_exception(L("Unsupported vector variable type"), opt.it_range);
|
|
}
|
|
}
|
|
}
|
|
|
|
static void check_writable(const MyContext *ctx, OptWithPos &opt) {
|
|
if (! opt.writable)
|
|
ctx->throw_exception(L("Cannot modify a read-only variable"), opt.it_range);
|
|
}
|
|
|
|
static void check_numeric(const expr ¶m) {
|
|
if (! param.numeric_type())
|
|
param.throw_exception(L("Right side is not a numeric expression"));
|
|
};
|
|
|
|
static size_t evaluate_count(const expr &expr_count) {
|
|
if (expr_count.type() != expr::TYPE_INT)
|
|
expr_count.throw_exception(L("Expected number of elements to fill a vector with."));
|
|
int count = expr_count.i();
|
|
if (count < 0)
|
|
expr_count.throw_exception(L("Negative number of elements specified."));
|
|
return size_t(count);
|
|
};
|
|
|
|
static void scalar_variable_assign_scalar(const MyContext *ctx, OptWithPos &lhs, const expr &rhs)
|
|
{
|
|
assert(! ctx->skipping());
|
|
assert(lhs.opt->is_scalar());
|
|
check_writable(ctx, lhs);
|
|
ConfigOption *wropt = const_cast<ConfigOption*>(lhs.opt);
|
|
switch (wropt->type()) {
|
|
case coFloat:
|
|
check_numeric(rhs);
|
|
static_cast<ConfigOptionFloat*>(wropt)->value = rhs.as_d();
|
|
break;
|
|
case coInt:
|
|
check_numeric(rhs);
|
|
static_cast<ConfigOptionInt*>(wropt)->value = rhs.as_i();
|
|
break;
|
|
case coString:
|
|
static_cast<ConfigOptionString*>(wropt)->value = rhs.to_string();
|
|
break;
|
|
case coPercent:
|
|
check_numeric(rhs);
|
|
static_cast<ConfigOptionPercent*>(wropt)->value = rhs.as_d();
|
|
break;
|
|
case coBool:
|
|
if (rhs.type() != expr::TYPE_BOOL)
|
|
ctx->throw_exception(L("Right side is not a boolean expression"), rhs.it_range);
|
|
static_cast<ConfigOptionBool*>(wropt)->value = rhs.b();
|
|
break;
|
|
default:
|
|
ctx->throw_exception(L("Unsupported output scalar variable type"), lhs.it_range);
|
|
}
|
|
}
|
|
|
|
static void vector_variable_element_assign_scalar(const MyContext *ctx, OptWithPos &lhs, const expr &rhs)
|
|
{
|
|
assert(! ctx->skipping());
|
|
assert(lhs.opt->is_vector());
|
|
check_writable(ctx, lhs);
|
|
if (! lhs.has_index())
|
|
ctx->throw_exception(L("Referencing an output vector variable when scalar is expected"), lhs.it_range);
|
|
ConfigOptionVectorBase *vec = const_cast<ConfigOptionVectorBase*>(static_cast<const ConfigOptionVectorBase*>(lhs.opt));
|
|
if (vec->empty())
|
|
ctx->throw_exception(L("Indexing an empty vector variable"), lhs.it_range);
|
|
if (lhs.index >= int(vec->size()))
|
|
ctx->throw_exception(L("Index out of range"), lhs.it_range);
|
|
switch (lhs.opt->type()) {
|
|
case coFloats:
|
|
check_numeric(rhs);
|
|
static_cast<ConfigOptionFloats*>(vec)->values[lhs.index] = rhs.as_d();
|
|
break;
|
|
case coInts:
|
|
check_numeric(rhs);
|
|
static_cast<ConfigOptionInts*>(vec)->values[lhs.index] = rhs.as_i();
|
|
break;
|
|
case coStrings:
|
|
static_cast<ConfigOptionStrings*>(vec)->values[lhs.index] = rhs.to_string();
|
|
break;
|
|
case coPercents:
|
|
check_numeric(rhs);
|
|
static_cast<ConfigOptionPercents*>(vec)->values[lhs.index] = rhs.as_d();
|
|
break;
|
|
case coBools:
|
|
if (rhs.type() != expr::TYPE_BOOL)
|
|
ctx->throw_exception(L("Right side is not a boolean expression"), rhs.it_range);
|
|
static_cast<ConfigOptionBools*>(vec)->values[lhs.index] = rhs.b();
|
|
break;
|
|
default:
|
|
ctx->throw_exception(L("Unsupported output vector variable type"), lhs.it_range);
|
|
}
|
|
}
|
|
|
|
static void vector_variable_assign_expr_with_count(const MyContext *ctx, OptWithPos &lhs, const expr &rhs_count, const expr &rhs_value)
|
|
{
|
|
assert(! ctx->skipping());
|
|
size_t count = evaluate_count(rhs_count);
|
|
auto *opt = const_cast<ConfigOption*>(lhs.opt);
|
|
switch (lhs.opt->type()) {
|
|
case coFloats:
|
|
check_numeric(rhs_value);
|
|
static_cast<ConfigOptionFloats*>(opt)->values.assign(count, rhs_value.as_d());
|
|
break;
|
|
case coInts:
|
|
check_numeric(rhs_value);
|
|
static_cast<ConfigOptionInts*>(opt)->values.assign(count, rhs_value.as_i());
|
|
break;
|
|
case coStrings:
|
|
static_cast<ConfigOptionStrings*>(opt)->values.assign(count, rhs_value.to_string());
|
|
break;
|
|
case coBools:
|
|
if (rhs_value.type() != expr::TYPE_BOOL)
|
|
rhs_value.throw_exception(L("Right side is not a boolean expression"));
|
|
static_cast<ConfigOptionBools*>(opt)->values.assign(count, rhs_value.b());
|
|
break;
|
|
default: assert(false);
|
|
}
|
|
}
|
|
|
|
static void variable_value(const MyContext *ctx, OptWithPos &opt, expr &output)
|
|
{
|
|
if (! ctx->skipping()) {
|
|
if (opt.opt->is_vector())
|
|
vector_element_to_expr(ctx, opt, output);
|
|
else
|
|
scalar_variable_to_expr(ctx, opt, output);
|
|
}
|
|
output.it_range = opt.it_range;
|
|
}
|
|
|
|
// Return a boolean value, true if the scalar variable referenced by "opt" is nullable and it has a nil value.
|
|
// Return a boolean value, true if an element of a vector variable referenced by "opt[index]" is nullable and it has a nil value.
|
|
static void is_nil_test(const MyContext *ctx, OptWithPos &opt, expr &output)
|
|
{
|
|
if (ctx->skipping()) {
|
|
} else if (opt.opt->is_vector()) {
|
|
if (! opt.has_index())
|
|
ctx->throw_exception(L("Referencing a vector variable when scalar is expected"), opt.it_range);
|
|
const ConfigOptionVectorBase *vec = static_cast<const ConfigOptionVectorBase*>(opt.opt);
|
|
if (vec->empty())
|
|
ctx->throw_exception(L("Indexing an empty vector variable"), opt.it_range);
|
|
output.set_b(static_cast<const ConfigOptionVectorBase*>(opt.opt)->is_nil(opt.index >= int(vec->size()) ? 0 : size_t(opt.index)));
|
|
} else {
|
|
assert(opt.opt->is_scalar());
|
|
output.set_b(opt.opt->is_nil());
|
|
}
|
|
output.it_range = opt.it_range;
|
|
}
|
|
|
|
// Reference to an existing symbol, or a name of a new symbol.
|
|
struct NewOldVariable {
|
|
std::string name;
|
|
IteratorRange it_range;
|
|
ConfigOption *opt{ nullptr };
|
|
};
|
|
static void new_old_variable(
|
|
const MyContext *ctx,
|
|
bool global_variable,
|
|
const IteratorRange &it_range,
|
|
NewOldVariable &out)
|
|
{
|
|
if (! ctx->skipping()) {
|
|
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(L("Symbol is already defined in read-only system dictionary"), it_range);
|
|
if (ctx->config_outputs && ctx->config_outputs->optptr(key))
|
|
ctx->throw_exception(L("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(L("Global variables are not available in this context"), it_range);
|
|
if (ctx->config_local.optptr(key))
|
|
ctx->throw_exception(L("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(L("Variable name already defined in global scope"), it_range);
|
|
out.opt = ctx->config_local.optptr(key);
|
|
}
|
|
out.name = std::move(key);
|
|
} else if (ctx->check_inactive_names()) {
|
|
// Declared in a branch that is not taken: the name still counts as defined for the names that follow.
|
|
std::string key(it_range.begin(), it_range.end());
|
|
if (global_variable && ctx->context_data != nullptr)
|
|
ctx->context_data->inactive_global_variables.insert(std::move(key));
|
|
else
|
|
ctx->inactive_local_variables.insert(std::move(key));
|
|
}
|
|
out.it_range = it_range;
|
|
}
|
|
|
|
// Decoding a scalar variable symbol "opt", assigning it a value of "param".
|
|
static void scalar_variable_assign_scalar_expression(const MyContext *ctx, OptWithPos &opt, const expr ¶m)
|
|
{
|
|
if (! ctx->skipping()) {
|
|
check_writable(ctx, opt);
|
|
if (opt.opt->is_vector())
|
|
vector_variable_element_assign_scalar(ctx, opt, param);
|
|
else
|
|
scalar_variable_assign_scalar(ctx, opt, param);
|
|
}
|
|
}
|
|
|
|
static void scalar_variable_new_from_scalar_expression(
|
|
const MyContext *ctx,
|
|
bool global_variable,
|
|
NewOldVariable &lhs,
|
|
const expr &rhs)
|
|
{
|
|
if (ctx->skipping()) {
|
|
} else if (lhs.opt) {
|
|
if (lhs.opt->is_vector())
|
|
rhs.throw_exception(L("Cannot assign a scalar value to a vector variable."));
|
|
OptWithPos lhs_opt{ lhs.opt, lhs.it_range, true };
|
|
scalar_variable_assign_scalar(ctx, lhs_opt, rhs);
|
|
} else {
|
|
std::unique_ptr<ConfigOption> opt_new;
|
|
switch (rhs.type()) {
|
|
case expr::TYPE_BOOL: opt_new = std::make_unique<ConfigOptionBool>(rhs.b()); break;
|
|
case expr::TYPE_INT: opt_new = std::make_unique<ConfigOptionInt>(rhs.i()); break;
|
|
case expr::TYPE_DOUBLE: opt_new = std::make_unique<ConfigOptionFloat>(rhs.d()); break;
|
|
case expr::TYPE_STRING: opt_new = std::make_unique<ConfigOptionString>(rhs.s()); break;
|
|
default: assert(false);
|
|
}
|
|
const_cast<MyContext*>(ctx)->store_new_variable(lhs.name, std::move(opt_new), global_variable);
|
|
}
|
|
}
|
|
|
|
static void vector_variable_new_from_array(
|
|
const MyContext *ctx,
|
|
bool global_variable,
|
|
NewOldVariable &lhs,
|
|
const expr &rhs_count,
|
|
const expr &rhs_value)
|
|
{
|
|
if (ctx->skipping()) {
|
|
} else if (lhs.opt) {
|
|
if (lhs.opt->is_scalar())
|
|
rhs_value.throw_exception(L("Cannot assign a vector value to a scalar variable."));
|
|
OptWithPos lhs_opt{ lhs.opt, lhs.it_range, true };
|
|
vector_variable_assign_expr_with_count(ctx, lhs_opt, rhs_count, rhs_value);
|
|
} else {
|
|
size_t count = evaluate_count(rhs_count);
|
|
std::unique_ptr<ConfigOption> opt_new;
|
|
switch (rhs_value.type()) {
|
|
case expr::TYPE_BOOL: opt_new = std::make_unique<ConfigOptionBools>(count, rhs_value.b()); break;
|
|
case expr::TYPE_INT: opt_new = std::make_unique<ConfigOptionInts>(count, rhs_value.i()); break;
|
|
case expr::TYPE_DOUBLE: opt_new = std::make_unique<ConfigOptionFloats>(count, rhs_value.d()); break;
|
|
case expr::TYPE_STRING: opt_new = std::make_unique<ConfigOptionStrings>(count, rhs_value.s()); break;
|
|
default: assert(false);
|
|
}
|
|
const_cast<MyContext*>(ctx)->store_new_variable(lhs.name, std::move(opt_new), global_variable);
|
|
}
|
|
}
|
|
|
|
static void vector_variable_assign_array(
|
|
const MyContext *ctx,
|
|
OptWithPos &lhs,
|
|
const expr &rhs_count,
|
|
const expr &rhs_value)
|
|
{
|
|
if (! ctx->skipping()) {
|
|
check_writable(ctx, lhs);
|
|
if (lhs.opt->is_scalar())
|
|
rhs_value.throw_exception(L("Cannot assign a vector value to a scalar variable."));
|
|
vector_variable_assign_expr_with_count(ctx, lhs, rhs_count, rhs_value);
|
|
}
|
|
}
|
|
|
|
template<typename ConfigOptionType, typename RightValueEvaluate>
|
|
static void fill_vector_from_initializer_list(ConfigOption *opt, const std::vector<expr> &il, RightValueEvaluate rv_eval) {
|
|
auto& out = static_cast<ConfigOptionType*>(opt)->values;
|
|
out.clear();
|
|
out.reserve(il.size());
|
|
for (const expr& i : il)
|
|
out.emplace_back(rv_eval(i));
|
|
}
|
|
|
|
static void vector_variable_assign_initializer_list(const MyContext *ctx, OptWithPos &lhs, const std::vector<expr> &il)
|
|
{
|
|
if (ctx->skipping())
|
|
return;
|
|
|
|
check_writable(ctx, lhs);
|
|
|
|
if (lhs.opt->is_scalar()) {
|
|
if (il.size() == 1)
|
|
// scalar_var = ( scalar )
|
|
scalar_variable_assign_scalar_expression(ctx, lhs, il.front());
|
|
else
|
|
// scalar_var = ()
|
|
// or
|
|
// scalar_var = ( scalar, scalar, ... )
|
|
ctx->throw_exception(L("Cannot assign a vector value to a scalar variable."), lhs.it_range);
|
|
}
|
|
|
|
auto check_numeric_vector = [](const std::vector<expr> &il) {
|
|
for (auto &i : il)
|
|
if (! i.numeric_type())
|
|
i.throw_exception(L("Right side is not a numeric expression"));
|
|
};
|
|
|
|
ConfigOption *opt = const_cast<ConfigOption*>(lhs.opt);
|
|
switch (lhs.opt->type()) {
|
|
case coFloats:
|
|
check_numeric_vector(il);
|
|
fill_vector_from_initializer_list<ConfigOptionFloats>(opt, il, [](auto &v){ return v.as_d(); });
|
|
break;
|
|
case coInts:
|
|
check_numeric_vector(il);
|
|
fill_vector_from_initializer_list<ConfigOptionInts>(opt, il, [](auto &v){ return v.as_i(); });
|
|
break;
|
|
case coStrings:
|
|
fill_vector_from_initializer_list<ConfigOptionStrings>(opt, il, [](auto &v){ return v.to_string(); });
|
|
break;
|
|
case coBools:
|
|
for (auto &i : il)
|
|
if (i.type() != expr::TYPE_BOOL)
|
|
i.throw_exception(L("Right side is not a boolean expression"));
|
|
fill_vector_from_initializer_list<ConfigOptionBools>(opt, il, [](auto &v){ return v.b(); });
|
|
break;
|
|
default: assert(false);
|
|
}
|
|
}
|
|
|
|
static void vector_variable_new_from_initializer_list(
|
|
const MyContext *ctx,
|
|
bool global_variable,
|
|
NewOldVariable &lhs,
|
|
const std::vector<expr> &il)
|
|
{
|
|
if (ctx->skipping())
|
|
return;
|
|
|
|
if (lhs.opt) {
|
|
// Assign to an existing vector variable.
|
|
OptWithPos lhs_opt{ lhs.opt, lhs.it_range, true };
|
|
vector_variable_assign_initializer_list(ctx, lhs_opt, il);
|
|
} else {
|
|
if (il.empty())
|
|
ctx->throw_exception(L("Cannot create vector variable from an empty initializer list, because its type cannot be deduced."), lhs.it_range);
|
|
// Allocate a new vector variable.
|
|
// 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);
|
|
}
|
|
std::unique_ptr<ConfigOption> opt_new;
|
|
if (num_string > 0)
|
|
// Convert everything to strings.
|
|
opt_new = std::make_unique<ConfigOptionStrings>();
|
|
else if (num_bool > 0) {
|
|
if (num_double + num_int > 0)
|
|
ctx->throw_exception(L("Right side is not valid: Mixing numeric and boolean types."), IteratorRange{ il.front().it_range.begin(), il.back().it_range.end() });
|
|
opt_new = std::make_unique<ConfigOptionBools>();
|
|
} else {
|
|
// Output is numeric.
|
|
if (num_double == 0)
|
|
opt_new = std::make_unique<ConfigOptionInts>();
|
|
else
|
|
opt_new = std::make_unique<ConfigOptionFloats>();
|
|
}
|
|
OptWithPos lhs_opt{ opt_new.get(), lhs.it_range, true };
|
|
vector_variable_assign_initializer_list(ctx, lhs_opt, il);
|
|
const_cast<MyContext*>(ctx)->store_new_variable(lhs.name, std::move(opt_new), global_variable);
|
|
}
|
|
}
|
|
|
|
static bool is_vector_variable_reference(const OptWithPos &var) {
|
|
return ! var.empty() && ! var.has_index() && var.opt->is_vector();
|
|
}
|
|
|
|
// Called when checking whether the NewOldVariable could be assigned a vectir right hand side.
|
|
static bool could_be_vector_variable_reference(const NewOldVariable &var) {
|
|
return var.opt == nullptr || var.opt->is_vector();
|
|
}
|
|
|
|
static void copy_vector_variable_to_vector_variable(const MyContext *ctx, OptWithPos &lhs, const OptWithPos &rhs)
|
|
{
|
|
if (ctx->skipping())
|
|
return;
|
|
|
|
check_writable(ctx, lhs);
|
|
assert(lhs.opt->is_vector());
|
|
if (rhs.has_index() || ! rhs.opt->is_vector())
|
|
ctx->throw_exception(L("Cannot assign scalar to a vector"), lhs.it_range);
|
|
if (rhs.opt->is_nil())
|
|
ctx->throw_exception(L("Some elements of the right hand side vector variable of optional values are undefined (nil)"), rhs.it_range);
|
|
if (lhs.opt->type() != rhs.opt->type()) {
|
|
// Vector types are not compatible.
|
|
switch (lhs.opt->type()) {
|
|
case coFloats:
|
|
ctx->throw_exception(L("Left hand side is a float vector, while the right hand side is not."), lhs.it_range);
|
|
case coInts:
|
|
ctx->throw_exception(L("Left hand side is an int vector, while the right hand side is not."), lhs.it_range);
|
|
case coStrings:
|
|
ctx->throw_exception(L("Left hand side is a string vector, while the right hand side is not."), lhs.it_range);
|
|
case coBools:
|
|
ctx->throw_exception(L("Left hand side is a bool vector, while the right hand side is not."), lhs.it_range);
|
|
default:
|
|
ctx->throw_exception(L("Left hand side / right hand side vectors are not compatible."), lhs.it_range);
|
|
}
|
|
}
|
|
const_cast<ConfigOption*>(lhs.opt)->set(rhs.opt);
|
|
}
|
|
|
|
static bool vector_variable_new_from_copy(
|
|
const MyContext *ctx,
|
|
bool global_variable,
|
|
NewOldVariable &lhs,
|
|
const OptWithPos &rhs)
|
|
{
|
|
if (ctx->skipping())
|
|
// Skipping, let conditional_expression parse the whole right hand side, which may continue after the variable reference.
|
|
return false;
|
|
|
|
if (lhs.opt) {
|
|
assert(lhs.opt->is_vector());
|
|
OptWithPos lhs_opt{ lhs.opt, lhs.it_range, true };
|
|
copy_vector_variable_to_vector_variable(ctx, lhs_opt, rhs);
|
|
} else {
|
|
if (rhs.has_index() || ! rhs.opt->is_vector())
|
|
// Stop parsing, let the other rules resolve this case.
|
|
return false;
|
|
if (rhs.opt->is_nil())
|
|
ctx->throw_exception(L("Some elements of the right hand side vector variable of optional values are undefined (nil)"), rhs.it_range);
|
|
// Clone the vector variable.
|
|
std::unique_ptr<ConfigOption> opt_new;
|
|
if (one_of(rhs.opt->type(), { coFloats, coInts, coStrings, coBools }))
|
|
opt_new = std::unique_ptr<ConfigOption>(rhs.opt->clone());
|
|
else if (rhs.opt->type() == coPercents)
|
|
opt_new = std::make_unique<ConfigOptionFloats>(static_cast<const ConfigOptionPercents*>(rhs.opt)->values);
|
|
else
|
|
ctx->throw_exception(L("Duplicating this type of vector variable is not supported"), rhs.it_range);
|
|
const_cast<MyContext*>(ctx)->store_new_variable(lhs.name, std::move(opt_new), global_variable);
|
|
}
|
|
// Continue parsing.
|
|
return true;
|
|
}
|
|
|
|
static void initializer_list_append(std::vector<expr> &list, expr ¶m)
|
|
{
|
|
if (param.type() != expr::TYPE_EMPTY)
|
|
// not skipping
|
|
list.emplace_back(std::move(param));
|
|
}
|
|
|
|
static void is_vector_empty(const MyContext *ctx, OptWithPos &opt, expr &out)
|
|
{
|
|
if (! ctx->skipping()) {
|
|
if (opt.has_index() || ! opt.opt->is_vector())
|
|
ctx->throw_exception(L("parameter of empty() is not a vector variable"), opt.it_range);
|
|
out.set_b(static_cast<const ConfigOptionVectorBase*>(opt.opt)->size() == 0);
|
|
}
|
|
out.it_range = opt.it_range;
|
|
}
|
|
|
|
static void vector_size(const MyContext *ctx, OptWithPos &opt, expr &out)
|
|
{
|
|
if (! ctx->skipping()) {
|
|
if (opt.has_index() || ! opt.opt->is_vector())
|
|
ctx->throw_exception(L("parameter of size() is not a vector variable"), opt.it_range);
|
|
out.set_i(int(static_cast<const ConfigOptionVectorBase*>(opt.opt)->size()));
|
|
}
|
|
out.it_range = opt.it_range;
|
|
}
|
|
|
|
// Verify that the expression returns an integer, which may be used
|
|
// to address a vector.
|
|
static void evaluate_index(expr &expr_index, int &output)
|
|
{
|
|
if (expr_index.type() != expr::TYPE_EMPTY) {
|
|
if (expr_index.type() != expr::TYPE_INT)
|
|
expr_index.throw_exception(L("Non-integer index is not allowed to address a vector variable."));
|
|
output = expr_index.i();
|
|
}
|
|
}
|
|
|
|
static void random(const MyContext *ctx, expr ¶m1, expr ¶m2)
|
|
{
|
|
if (ctx->skipping())
|
|
return;
|
|
|
|
if (ctx->context_data == nullptr)
|
|
ctx->throw_exception(L("Random number generator not available in this context."),
|
|
IteratorRange(param1.it_range.begin(), param2.it_range.end()));
|
|
expr::random(param1, param2, ctx->context_data->rng);
|
|
}
|
|
|
|
static void filament_change(const MyContext* ctx, expr& param)
|
|
{
|
|
MyContext *context = const_cast<MyContext *>(ctx);
|
|
context->current_extruder_id = param.as_i();
|
|
}
|
|
|
|
static void throw_exception(const std::string &msg, const IteratorRange &it_range)
|
|
{
|
|
// An asterix is added to the start of the string to differentiate the boost::spirit::info::tag content
|
|
// between the grammer terminal / non-terminal symbol name and a free-form error message.
|
|
boost::throw_exception(qi::expectation_failure(it_range.begin(), it_range.end(), spirit::info(std::string("*") + msg)));
|
|
}
|
|
|
|
static void process_error_message(const MyContext *context, const boost::spirit::info &info, const Iterator &it_begin, const Iterator &it_end, const Iterator &it_error)
|
|
{
|
|
std::string &msg = const_cast<MyContext*>(context)->error_message;
|
|
std::string first(it_begin, it_error);
|
|
std::string last(it_error, it_end);
|
|
auto first_pos = first.rfind('\n');
|
|
auto last_pos = last.find('\n');
|
|
int line_nr = 1;
|
|
if (first_pos == std::string::npos)
|
|
first_pos = 0;
|
|
else {
|
|
// Calculate the current line number.
|
|
for (size_t i = 0; i <= first_pos; ++ i)
|
|
if (first[i] == '\n')
|
|
++ line_nr;
|
|
++ first_pos;
|
|
}
|
|
auto error_line = std::string(first, first_pos) + std::string(last, 0, last_pos);
|
|
// Position of the it_error from the start of its line.
|
|
auto error_pos = (it_error - it_begin) - first_pos;
|
|
if (! info.tag.empty() && info.tag.front() == '*') {
|
|
// The gat contains an explanatory string.
|
|
// TRN Custom G-code error. %1% is the line number, %2% the reason.
|
|
msg += Slic3r::format(_u8L("Parsing error at line %1%: %2%"), line_nr, Slic3r::I18N::translate(info.tag.substr(1)));
|
|
} else {
|
|
auto it = tag_to_error_message.find(info.tag);
|
|
if (it == tag_to_error_message.end()) {
|
|
// A generic error report based on the nonterminal or terminal symbol name.
|
|
// TRN Custom G-code error. %1% is the line number, %2% the name of a grammar rule.
|
|
msg += Slic3r::format(_u8L("Parsing error at line %1%. Expecting tag %2%"), line_nr, info.tag);
|
|
} else {
|
|
// Use the human readable error message.
|
|
// TRN Custom G-code error. %1% is the line number, %2% the reason.
|
|
msg += Slic3r::format(_u8L("Parsing error at line %1%. %2%"), line_nr, Slic3r::I18N::translate(it->second));
|
|
}
|
|
}
|
|
msg += '\n';
|
|
// This hack removes all non-UTF8 characters from the source line, so that the upstream wxWidgets conversions
|
|
// from UTF8 to UTF16 don't bail out.
|
|
msg += boost::nowide::narrow(boost::nowide::widen(error_line));
|
|
msg += '\n';
|
|
// The error dialog (MsgDialog.cpp) renders this excerpt monospaced. It recognizes a source
|
|
// line directly above a caret line of spaces and a single '^'.
|
|
for (size_t i = 0; i < error_pos; ++ i)
|
|
msg += ' ';
|
|
msg += "^\n";
|
|
}
|
|
|
|
private:
|
|
// For skipping execution of inactive conditional branches.
|
|
mutable int m_depth_suppressed{ 0 };
|
|
};
|
|
|
|
struct InterpolateTableContext {
|
|
struct Item {
|
|
double x;
|
|
IteratorRange it_range_x;
|
|
double y;
|
|
};
|
|
std::vector<Item> table;
|
|
|
|
static void init(const expr &x) {
|
|
if (x.type() != expr::TYPE_EMPTY) {
|
|
if (!x.numeric_type())
|
|
x.throw_exception(L("Interpolation value must be a number."));
|
|
}
|
|
}
|
|
static void add_pair(const expr &x, const expr &y, InterpolateTableContext &table) {
|
|
if (x.type() != expr::TYPE_EMPTY) {
|
|
if (! x.numeric_type())
|
|
x.throw_exception(L("X value of a table point must be a number."));
|
|
if (! y.numeric_type())
|
|
y.throw_exception(L("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) {
|
|
if (expr_x.type() == expr::TYPE_EMPTY)
|
|
return;
|
|
|
|
// Check whether the table X values are sorted.
|
|
double x = expr_x.as_d();
|
|
assert(! std::isnan(x));
|
|
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(std::string("*") + L("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 (double x0 = table.table.front().x; x > x0 - EPSILON && x < x0)
|
|
out.set_d(table.table.front().y);
|
|
else if (double x1 = table.table.back().x; x > x1 && x < x1 + EPSILON)
|
|
out.set_d(table.table.back().y);
|
|
else
|
|
// The value is really outside the table range.
|
|
expr_x.throw_exception(L("Interpolation value is outside the table range"));
|
|
}
|
|
}
|
|
};
|
|
|
|
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<std::string, std::string> MyContext::tag_to_error_message = {
|
|
{ "eoi", L("Unknown syntax error") },
|
|
{ "start", L("Unknown syntax error") },
|
|
{ "text", L("Invalid text.") },
|
|
{ "text_block", L("Invalid text block.") },
|
|
{ "macro", L("Invalid macro.") },
|
|
{ "repeat", L("Unknown syntax error") },
|
|
{ "if_else_output", L("Not an {if}{else}{endif} macro.") },
|
|
{ "switch_output", L("Not a {switch} macro.") },
|
|
{ "legacy_variable_expansion", L("Expecting a legacy variable expansion format") },
|
|
{ "identifier", L("Expecting an identifier.") },
|
|
{ "conditional_expression", L("Expecting a conditional expression.") },
|
|
{ "logical_or_expression", L("Expecting a boolean expression.") },
|
|
{ "logical_and_expression", L("Expecting a boolean expression.") },
|
|
{ "equality_expression", L("Expecting an expression.") },
|
|
{ "bool_expr_eval", L("Expecting a boolean expression.")},
|
|
{ "relational_expression", L("Expecting an expression.") },
|
|
{ "additive_expression", L("Expecting an expression.") },
|
|
{ "multiplicative_expression", L("Expecting an expression.") },
|
|
{ "unary_expression", L("Expecting an expression.") },
|
|
{ "optional_parameter", L("Expecting a closing brace or an optional parameter.") },
|
|
{ "one_of_list", L("Expecting a list of string patterns (simple text or regexp)") },
|
|
{ "variable_reference", L("Expecting a variable reference.")},
|
|
{ "variable", L("Expecting a variable name.")},
|
|
{ "regular_expression", L("Expecting a regular expression.")}
|
|
};
|
|
|
|
// For debugging the boost::spirit parsers. Print out the string enclosed in it_range.
|
|
std::ostream& operator<<(std::ostream& os, const IteratorRange &it_range)
|
|
{
|
|
os << std::string(it_range.begin(), it_range.end());
|
|
return os;
|
|
}
|
|
|
|
// Disable parsing int numbers (without decimals) and Inf/NaN symbols by the double parser.
|
|
struct strict_real_policies_without_nan_inf : public qi::strict_real_policies<double>
|
|
{
|
|
template <typename It, typename Attr> static bool parse_nan(It&, It const&, Attr&) { return false; }
|
|
template <typename It, typename Attr> static bool parse_inf(It&, It const&, Attr&) { return false; }
|
|
};
|
|
|
|
// This parser is to be used inside a raw[] directive to accept a single valid UTF-8 character.
|
|
// If an invalid UTF-8 sequence is encountered, a qi::expectation_failure is thrown.
|
|
struct utf8_char_parser : qi::primitive_parser<utf8_char_parser>
|
|
{
|
|
// Define the attribute type exposed by this parser component
|
|
template <typename Context, typename Iterator>
|
|
struct attribute
|
|
{
|
|
typedef wchar_t type;
|
|
};
|
|
|
|
// This function is called during the actual parsing process to skip whitespaces.
|
|
// Also it throws if it encounters valid or invalid UTF-8 sequence.
|
|
template <typename Iterator, typename Context , typename Skipper, typename Attribute>
|
|
bool parse(Iterator &first, Iterator const &last, Context &context, Skipper const &skipper, Attribute& attr) const
|
|
{
|
|
// The skipper shall always be empty, any white space will be accepted.
|
|
// skip_over(first, last, skipper);
|
|
if (first == last)
|
|
return false;
|
|
// Iterator over the UTF-8 sequence.
|
|
auto it = first;
|
|
// Read the first byte of the UTF-8 sequence.
|
|
unsigned char c = static_cast<boost::uint8_t>(*it ++);
|
|
unsigned int cnt = 0;
|
|
// UTF-8 sequence must not start with a continuation character:
|
|
if ((c & 0xC0) == 0x80)
|
|
goto err;
|
|
// Skip high surrogate first if there is one.
|
|
// If the most significant bit with a zero in it is in position
|
|
// 8-N then there are N bytes in this UTF-8 sequence:
|
|
{
|
|
unsigned char mask = 0x80u;
|
|
unsigned int result = 0;
|
|
while (c & mask) {
|
|
++ result;
|
|
mask >>= 1;
|
|
}
|
|
cnt = (result == 0) ? 1 : ((result > 4) ? 4 : result);
|
|
}
|
|
// Since we haven't read in a value, we need to validate the code points:
|
|
for (-- cnt; cnt > 0; -- cnt) {
|
|
if (it == last)
|
|
goto err;
|
|
c = static_cast<boost::uint8_t>(*it ++);
|
|
// We must have a continuation byte:
|
|
if (cnt > 1 && (c & 0xC0) != 0x80)
|
|
goto err;
|
|
}
|
|
first = it;
|
|
return true;
|
|
err:
|
|
MyContext::throw_exception(L("Invalid utf8 sequence"), IteratorRange(first, last));
|
|
return false;
|
|
}
|
|
|
|
// This function is called during error handling to create a human readable string for the error context.
|
|
template <typename Context>
|
|
spirit::info what(Context&) const
|
|
{
|
|
return spirit::info("unicode_char");
|
|
}
|
|
};
|
|
|
|
// This parser is to be used inside a raw[] directive to accept a single valid UTF-8 character.
|
|
// If an invalid UTF-8 sequence is encountered, a qi::expectation_failure is thrown.
|
|
struct ascii_char_skipper_parser : public utf8_char_parser
|
|
{
|
|
// This function is called during the actual parsing process
|
|
template <typename Iterator, typename Context, typename Skipper, typename Attribute>
|
|
bool parse(Iterator &first, Iterator const &last, Context &context, Skipper const &skipper, Attribute &attr) const
|
|
{
|
|
Iterator it = first;
|
|
// Let the UTF-8 parser throw if it encounters an invalid UTF-8 sequence.
|
|
if (! utf8_char_parser::parse(it, last, context, skipper, attr))
|
|
return false;
|
|
char c = *first;
|
|
if (it - first > 1 || c < 0)
|
|
MyContext::throw_exception(L("Non-ASCII7 characters are only allowed inside text blocks and string literals, not inside code blocks."), IteratorRange(first, it));
|
|
if (c == '\r' || c == '\n' || c == '\t' || c == ' ') {
|
|
// Skip the whitespaces
|
|
++ first;
|
|
return true;
|
|
} else
|
|
// Stop skipping, let this 7bit ASCII character be processed.
|
|
return false;
|
|
}
|
|
|
|
// This function is called during error handling to create a human readable string for the error context.
|
|
template <typename Context>
|
|
spirit::info what(Context&) const
|
|
{
|
|
return spirit::info("ASCII7_char");
|
|
}
|
|
};
|
|
|
|
struct FactorActions {
|
|
static void set_start_pos(Iterator &start_pos, expr &out)
|
|
{ out.it_range = IteratorRange(start_pos, start_pos); }
|
|
static void int_(const MyContext *ctx, int &value, Iterator &end_pos, expr &out) {
|
|
if (ctx->skipping()) {
|
|
out.reset();
|
|
out.it_range.end() = end_pos;
|
|
} else
|
|
out = expr(value, out.it_range.begin(), end_pos);
|
|
}
|
|
static void double_(const MyContext *ctx, double &value, Iterator &end_pos, expr &out) {
|
|
if (ctx->skipping()) {
|
|
out.reset();
|
|
out.it_range.end() = end_pos;
|
|
} else
|
|
out = expr(value, out.it_range.begin(), end_pos);
|
|
}
|
|
static void bool_(const MyContext *ctx, bool &value, Iterator &end_pos, expr &out) {
|
|
if (ctx->skipping()) {
|
|
out.reset();
|
|
out.it_range.end() = end_pos;
|
|
} else
|
|
out = expr(value, out.it_range.begin(), end_pos);
|
|
}
|
|
static void string_(const MyContext *ctx, IteratorRange &it_range, expr &out) {
|
|
if (ctx->skipping()) {
|
|
out.reset();
|
|
out.it_range = it_range;
|
|
} else {
|
|
// Unescape the string, UTF-8 safe.
|
|
std::string s;
|
|
auto begin = std::next(it_range.begin());
|
|
auto end = std::prev(it_range.end());
|
|
assert(begin <= end);
|
|
{
|
|
// 1) Get the size of the string after unescaping.
|
|
size_t len = 0;
|
|
for (auto it = begin; it != end;) {
|
|
if (*it == '\\') {
|
|
if (++ it == end ||
|
|
(*it != 'r' && *it != 'n' && *it != '"' && *it != '\\'))
|
|
ctx->throw_exception(L("Invalid escape sequence"), {std::prev(it), std::next(it) });
|
|
++ len;
|
|
++ it;
|
|
} else {
|
|
size_t n = get_utf8_sequence_length(&*it, end - it);
|
|
len += n;
|
|
it += n;
|
|
}
|
|
}
|
|
// and reserve the string.
|
|
s.reserve(len);
|
|
}
|
|
// 2) Copy & unescape the string.
|
|
for (auto it = begin; it != end;) {
|
|
if (*it == '\\') {
|
|
char c = *(++ it);
|
|
if (c == 'r')
|
|
c = '\r';
|
|
else if (c == 'n')
|
|
c = '\n';
|
|
s += c;
|
|
++ it;
|
|
} else {
|
|
size_t n = get_utf8_sequence_length(&*it, end - it);
|
|
s.append(&*it, n);
|
|
it += n;
|
|
}
|
|
}
|
|
out = expr(std::move(s), it_range.begin(), it_range.end());
|
|
}
|
|
}
|
|
static void expr_(expr &value, Iterator &end_pos, expr &out)
|
|
{ auto begin_pos = out.it_range.begin(); out = expr(std::move(value), begin_pos, end_pos); }
|
|
static void minus_(expr &value, expr &out)
|
|
{ out = value.unary_minus(out.it_range.begin()); }
|
|
static void not_(expr &value, expr &out)
|
|
{ out = value.unary_not(out.it_range.begin()); }
|
|
static void to_int(expr &value, expr &out)
|
|
{ out = value.unary_integer(out.it_range.begin()); }
|
|
static void round(expr &value, expr &out)
|
|
{ out = value.round(out.it_range.begin()); }
|
|
static void floor(expr &value, expr &out)
|
|
{ out = value.floor(out.it_range.begin()); }
|
|
static void ceil(expr &value, expr &out)
|
|
{ out = value.ceil(out.it_range.begin());}
|
|
// For indicating "no optional parameter".
|
|
static void noexpr(expr &out) { out.reset(); }
|
|
};
|
|
|
|
using skipper = ascii_char_skipper_parser;
|
|
|
|
///////////////////////////////////////////////////////////////////////////
|
|
// Our macro_processor grammar
|
|
///////////////////////////////////////////////////////////////////////////
|
|
// Inspired by the C grammar rules https://www.lysator.liu.se/c/ANSI-C-grammar-y.html
|
|
struct macro_processor : qi::grammar<Iterator, std::string(const MyContext*), qi::locals<bool>, skipper>
|
|
{
|
|
macro_processor() : macro_processor::base_type(start)
|
|
{
|
|
qi::alpha_type alpha;
|
|
qi::alnum_type alnum;
|
|
qi::eps_type eps;
|
|
qi::raw_type raw;
|
|
qi::lit_type lit;
|
|
qi::lexeme_type lexeme;
|
|
qi::no_skip_type no_skip;
|
|
qi::real_parser<double, strict_real_policies_without_nan_inf> strict_double;
|
|
spirit_encoding::char_type char_;
|
|
utf8_char_parser utf8char;
|
|
spirit::bool_type bool_;
|
|
spirit::int_type int_;
|
|
spirit::double_type double_;
|
|
spirit_encoding::string_type string;
|
|
spirit::eoi_type eoi;
|
|
spirit::repository::qi::iter_pos_type iter_pos;
|
|
auto kw = spirit::repository::qi::distinct(qi::copy(alnum | '_'));
|
|
|
|
qi::_val_type _val;
|
|
qi::_1_type _1;
|
|
qi::_2_type _2;
|
|
qi::_3_type _3;
|
|
qi::_4_type _4;
|
|
qi::_a_type _a;
|
|
qi::_b_type _b;
|
|
qi::_r1_type _r1;
|
|
qi::_r2_type _r2;
|
|
|
|
// Starting symbol of the grammer.
|
|
// The leading eps is required by the "expectation point" operator ">".
|
|
// Without it, some of the errors would not trigger the error handler.
|
|
// Also the start symbol switches between the "full macro syntax" and a "boolean expression only",
|
|
// depending on the context->just_boolean_expression flag. This way a single static expression parser
|
|
// could serve both purposes.
|
|
start =
|
|
( (eps(px::bind(&MyContext::evaluate_full_macro, _r1)) > text_block(_r1) [_val=_1])
|
|
| conditional_expression(_r1) [ px::bind(&expr::evaluate_boolean_to_string, _1, _val) ]
|
|
) > eoi;
|
|
start.name("start");
|
|
qi::on_error<qi::fail>(start, px::bind(&MyContext::process_error_message, _r1, _4, _1, _2, _3));
|
|
|
|
text_block = *(
|
|
text [_val+=_1]
|
|
// 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('{') >> (macros(_r1)[_val += _1] > '}') | '}')
|
|
| (lit('[') > legacy_variable_expansion(_r1) [_val+=_1] > ']')
|
|
);
|
|
text_block.name("text_block");
|
|
|
|
// Free-form text up to a first brace, including spaces and newlines.
|
|
// The free-form text will be inserted into the processed text without a modification.
|
|
text = no_skip[raw[+(utf8char - char_('[') - char_('{'))]];
|
|
text.name("text");
|
|
|
|
// New style of macro expansion.
|
|
// The macro expansion may contain numeric or string expressions, ifs and cases.
|
|
macros =
|
|
+(block(_r1)[_val += _1] | (statement(_r1) > (+lit(';') | &lit('}')))[_val += _1] | +lit(';'));
|
|
macros.name("macro");
|
|
// if_macros and else_macros only differ by the look-ahead ending condition, which is to not have to repeat the last semicolon
|
|
// at the end of the block.
|
|
if_macros = kw["then"] > *(block(_r1)[_val += _1] | (statement(_r1) > (+lit(';') | &(kw["elsif"] | kw["else"] | kw["endif"])))[_val += _1] | +lit(';'));
|
|
if_macros.name("if_macros");
|
|
else_macros = *(block(_r1)[_val += _1] | (statement(_r1) > (+lit(';') | &kw["endif"]))[_val += _1] | +lit(';'));
|
|
else_macros.name("else_macros");
|
|
|
|
// Blocks do not require a separating semicolon.
|
|
block =
|
|
(kw["if"] > if_else_output(_r1)[_val = _1])
|
|
// (kw["switch"] ...
|
|
;
|
|
block.name("block");
|
|
|
|
// Statements require a separating semicolon.
|
|
statement =
|
|
(assignment_statement(_r1) [_val = _1])
|
|
| (new_variable_statement(_r1)[_val = _1])
|
|
| (conditional_expression(_r1)[px::bind(&expr::to_string2, _1, _val)])
|
|
;
|
|
|
|
// An if expression enclosed in {} (the outmost {} are already parsed by the caller).
|
|
// Also }{ could be replaced with ; to simplify writing of pure code.
|
|
if_else_output =
|
|
eps[_a=true] >
|
|
(bool_expr_eval(_r1)[px::bind(&MyContext::block_enter, _r1, _1)] > (if_text_block(_r1) | if_macros(_r1)))
|
|
[px::bind(&MyContext::block_exit, _r1, _1, _a, _2, _val)] >
|
|
*((kw["elsif"] > bool_expr_eval(_r1)[px::bind(&MyContext::block_enter, _r1, _1 && _a)] > (if_text_block(_r1) | if_macros(_r1)))
|
|
[px::bind(&MyContext::block_exit, _r1, _1, _a, _2, _val)]) >
|
|
-(kw["else"] > eps[px::bind(&MyContext::block_enter, _r1, _a)] > (if_text_block(_r1) | else_macros(_r1)))
|
|
[px::bind(&MyContext::block_exit, _r1, _a, _a, _1, _val)] >
|
|
kw["endif"];
|
|
if_else_output.name("if_else_output");
|
|
if_text_block = (lit('}') > text_block(_r1) > '{');
|
|
if_text_block.name("if_text_block");
|
|
|
|
// A switch expression enclosed in {} (the outmost {} are already parsed by the caller).
|
|
/*
|
|
switch_output =
|
|
eps[_b=true] >
|
|
omit[expr(_r1)[_a=_1]] > '}' > text_block(_r1)[px::bind(&expr::set_if_equal, _a, _b, _1, _val)] > '{' >
|
|
*("elsif" > omit[bool_expr_eval(_r1)[_a=_1]] > '}' > text_block(_r1)[px::bind(&expr::set_if, _a, _b, _1, _val)]) >>
|
|
-("else" > '}' >> text_block(_r1)[px::bind(&expr::set_if, _b, _b, _1, _val)]) >
|
|
"endif";
|
|
*/
|
|
|
|
// Legacy variable expansion of the original Slic3r, in the form of [scalar_variable] or [vector_variable_index].
|
|
legacy_variable_expansion =
|
|
(identifier >> &lit(']'))
|
|
[ px::bind(&MyContext::legacy_variable_expansion, _r1, _1, _val) ]
|
|
| (identifier > lit('[') > identifier > ']')
|
|
[ px::bind(&MyContext::legacy_variable_expansion2, _r1, _1, _2, _val) ]
|
|
;
|
|
legacy_variable_expansion.name("legacy_variable_expansion");
|
|
|
|
identifier =
|
|
! kw[keywords] >>
|
|
raw[lexeme[(alpha | '_') >> *(alnum | '_')]];
|
|
identifier.name("identifier");
|
|
|
|
conditional_expression =
|
|
logical_or_expression(_r1) [_val = _1]
|
|
>> -('?' > eps[px::bind(&expr::evaluate_boolean, _val, _a)] >
|
|
eps[px::bind(&MyContext::block_enter, _r1, _a)] > conditional_expression(_r1)[px::bind(&MyContext::block_exit_ternary, _r1, _a, _1, _val)]
|
|
> ':' >
|
|
eps[px::bind(&MyContext::block_enter, _r1, ! _a)] > conditional_expression(_r1)[px::bind(&MyContext::block_exit_ternary, _r1, ! _a, _1, _val)]);
|
|
conditional_expression.name("conditional_expression");
|
|
|
|
logical_or_expression =
|
|
logical_and_expression(_r1) [_val = _1]
|
|
>> *( ((kw["or"] | "||") > logical_and_expression(_r1) ) [px::bind(&expr::logical_or, _val, _1)] );
|
|
logical_or_expression.name("logical_or_expression");
|
|
|
|
logical_and_expression =
|
|
equality_expression(_r1) [_val = _1]
|
|
>> *( ((kw["and"] | "&&") > equality_expression(_r1) ) [px::bind(&expr::logical_and, _val, _1)] );
|
|
logical_and_expression.name("logical_and_expression");
|
|
|
|
equality_expression =
|
|
relational_expression(_r1) [_val = _1]
|
|
>> *( ("==" > relational_expression(_r1) ) [px::bind(&expr::equal, _val, _1)]
|
|
| ("!=" > relational_expression(_r1) ) [px::bind(&expr::not_equal, _val, _1)]
|
|
| ("<>" > relational_expression(_r1) ) [px::bind(&expr::not_equal, _val, _1)]
|
|
| ("=~" > regular_expression ) [px::bind(&expr::regex_matches, _val, _1)]
|
|
| ("!~" > regular_expression ) [px::bind(&expr::regex_doesnt_match, _val, _1)]
|
|
);
|
|
equality_expression.name("bool expression");
|
|
|
|
// Evaluate a boolean expression stored as expr into a boolean value.
|
|
// Throw if the equality_expression does not produce a expr of boolean type.
|
|
bool_expr_eval = conditional_expression(_r1) [ px::bind(&expr::evaluate_boolean, _1, _val) ];
|
|
bool_expr_eval.name("bool_expr_eval");
|
|
|
|
relational_expression =
|
|
additive_expression(_r1) [_val = _1]
|
|
>> *( ("<=" > additive_expression(_r1) ) [px::bind(&expr::leq, _val, _1)]
|
|
| (">=" > additive_expression(_r1) ) [px::bind(&expr::geq, _val, _1)]
|
|
| (lit('<') > additive_expression(_r1) ) [px::bind(&expr::lower, _val, _1)]
|
|
| (lit('>') > additive_expression(_r1) ) [px::bind(&expr::greater, _val, _1)]
|
|
);
|
|
relational_expression.name("relational_expression");
|
|
|
|
additive_expression =
|
|
multiplicative_expression(_r1) [_val = _1]
|
|
>> *( (lit('+') > multiplicative_expression(_r1) ) [_val += _1]
|
|
| (lit('-') > multiplicative_expression(_r1) ) [_val -= _1]
|
|
);
|
|
additive_expression.name("additive_expression");
|
|
|
|
multiplicative_expression =
|
|
unary_expression(_r1) [_val = _1]
|
|
>> *( (lit('*') > unary_expression(_r1) ) [_val *= _1]
|
|
| (lit('/') > unary_expression(_r1) ) [_val /= _1]
|
|
| (lit('%') > unary_expression(_r1) ) [_val %= _1]
|
|
);
|
|
multiplicative_expression.name("multiplicative_expression");
|
|
|
|
assignment_statement =
|
|
(variable_reference(_r1)[_a = _1] >> '=') >
|
|
( // Consumes also '(' conditional_expression ')', that means enclosing an expression into braces makes it a single value vector initializer.
|
|
initializer_list(_r1)[px::bind(&MyContext::vector_variable_assign_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.
|
|
| eps(px::bind(&MyContext::is_vector_variable_reference, _a)) >>
|
|
variable_reference(_r1)[px::bind(&MyContext::copy_vector_variable_to_vector_variable, _r1, _a, _1)]
|
|
// Would NOT consume '(' conditional_expression ')' because such value was consumed with the expression above.
|
|
| conditional_expression(_r1)
|
|
[px::bind(&MyContext::scalar_variable_assign_scalar_expression, _r1, _a, _1)]
|
|
| (kw["repeat"] > "(" > additive_expression(_r1) > "," > conditional_expression(_r1) > ")")
|
|
[px::bind(&MyContext::vector_variable_assign_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.
|
|
initializer_list(_r1)[px::bind(&MyContext::vector_variable_new_from_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.
|
|
| eps(px::bind(&MyContext::could_be_vector_variable_reference, _b)) >>
|
|
variable_reference(_r1)[qi::_pass = px::bind(&MyContext::vector_variable_new_from_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::scalar_variable_new_from_scalar_expression, _r1, _a, _b, _1)]
|
|
| (kw["repeat"] > "(" > additive_expression(_r1) > "," > conditional_expression(_r1) > ")")
|
|
[px::bind(&MyContext::vector_variable_new_from_array, _r1, _a, _b, _1, _2)]
|
|
);
|
|
initializer_list = lit('(') >
|
|
( lit(')') |
|
|
( conditional_expression(_r1)[px::bind(&MyContext::initializer_list_append, _val, _1)] >
|
|
*(lit(',') > conditional_expression(_r1)[px::bind(&MyContext::initializer_list_append, _val, _1)]) >
|
|
lit(')')
|
|
)
|
|
);
|
|
|
|
unary_expression = iter_pos[px::bind(&FactorActions::set_start_pos, _1, _val)] >> (
|
|
variable_reference(_r1) [px::bind(&MyContext::variable_value, _r1, _1, _val)]
|
|
| (lit('(') > conditional_expression(_r1) > ')' > iter_pos) [ px::bind(&FactorActions::expr_, _1, _2, _val) ]
|
|
| (lit('-') > unary_expression(_r1) ) [ px::bind(&FactorActions::minus_, _1, _val) ]
|
|
| (lit('+') > unary_expression(_r1) > iter_pos) [ px::bind(&FactorActions::expr_, _1, _2, _val) ]
|
|
| ((kw["not"] | '!') > unary_expression(_r1) > iter_pos) [ px::bind(&FactorActions::not_, _1, _val) ]
|
|
| (kw["min"] > '(' > conditional_expression(_r1) [_val = _1] > ',' > conditional_expression(_r1) > ')')
|
|
[ px::bind(&expr::min, _val, _2) ]
|
|
| (kw["max"] > '(' > conditional_expression(_r1) [_val = _1] > ',' > conditional_expression(_r1) > ')')
|
|
[ px::bind(&expr::max, _val, _2) ]
|
|
| (kw["random"] > '(' > conditional_expression(_r1) [_val = _1] > ',' > conditional_expression(_r1) > ')')
|
|
[ px::bind(&MyContext::random, _r1, _val, _2) ]
|
|
| (kw["filament_change"] > '(' > conditional_expression(_r1) > ')') [ px::bind(&MyContext::filament_change, _r1, _1) ]
|
|
| (kw["digits"] > '(' > conditional_expression(_r1) [_val = _1] > ',' > conditional_expression(_r1) > optional_parameter(_r1))
|
|
[ px::bind(&expr::digits<false>, _val, _2, _3) ]
|
|
| (kw["zdigits"] > '(' > conditional_expression(_r1) [_val = _1] > ',' > conditional_expression(_r1) > optional_parameter(_r1))
|
|
[ px::bind(&expr::digits<true>, _val, _2, _3) ]
|
|
| (kw["regex_replace"] > '(' > conditional_expression(_r1) [_val = _1] > ',' > regular_expression > ',' > conditional_expression(_r1) > ')')
|
|
[ px::bind(&expr::regex_replace, _val, _2, _3) ]
|
|
| (kw["int"] > '(' > conditional_expression(_r1) > ')') [ px::bind(&FactorActions::to_int, _1, _val) ]
|
|
| (kw["round"] > '(' > conditional_expression(_r1) > ')') [ px::bind(&FactorActions::round, _1, _val) ]
|
|
| (kw["ceil"] > '(' > conditional_expression(_r1) > ')') [ px::bind(&FactorActions::ceil, _1, _val) ]
|
|
| (kw["floor"] > '(' > conditional_expression(_r1) > ')') [ px::bind(&FactorActions::floor, _1, _val) ]
|
|
| (kw["is_nil"] > '(' > variable_reference(_r1) > ')') [px::bind(&MyContext::is_nil_test, _r1, _1, _val)]
|
|
| (kw["one_of"] > '(' > one_of(_r1) > ')') [ _val = _1 ]
|
|
| (kw["empty"] > '(' > variable_reference(_r1) > ')') [px::bind(&MyContext::is_vector_empty, _r1, _1, _val)]
|
|
| (kw["size"] > '(' > variable_reference(_r1) > ')') [px::bind(&MyContext::vector_size, _r1, _1, _val)]
|
|
| (kw["interpolate_table"] > '(' > interpolate_table(_r1) > ')') [ _val = _1 ]
|
|
| (strict_double > iter_pos) [ px::bind(&FactorActions::double_, _r1, _1, _2, _val) ]
|
|
| (int_ > iter_pos) [ px::bind(&FactorActions::int_, _r1, _1, _2, _val) ]
|
|
| (kw[bool_] > iter_pos) [ px::bind(&FactorActions::bool_, _r1, _1, _2, _val) ]
|
|
| raw[lexeme['"' > *((utf8char - char_('\\') - char_('"')) | ('\\' > char_)) > '"']]
|
|
[ px::bind(&FactorActions::string_, _r1, _1, _val) ]
|
|
);
|
|
unary_expression.name("unary_expression");
|
|
|
|
one_of = (unary_expression(_r1)[_a = _1] > one_of_list(_r1, _a))[_val = _2];
|
|
one_of.name("one_of");
|
|
one_of_list =
|
|
eps[px::bind(&expr::one_of_test_init, _val)] >
|
|
( ( ',' > *(
|
|
(
|
|
unary_expression(_r1)[px::bind(&expr::one_of_test<false>, _r2, _1, _val)]
|
|
| (lit('~') > unary_expression(_r1))[px::bind(&expr::one_of_test<true>, _r2, _1, _val)]
|
|
| regular_expression[px::bind(&expr::one_of_test_regex, _r2, _1, _val)]
|
|
) >> -lit(','))
|
|
)
|
|
| 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 ]
|
|
);
|
|
optional_parameter.name("optional_parameter");
|
|
|
|
variable_reference =
|
|
variable(_r1)[_a=_1] >>
|
|
(
|
|
('[' > additive_expression(_r1)[px::bind(&MyContext::evaluate_index, _1, _b)] > ']' > iter_pos)
|
|
[px::bind(&MyContext::store_variable_index, _r1, _a, _b, _2, _val)]
|
|
| eps[_val=_a]
|
|
);
|
|
variable_reference.name("variable reference");
|
|
|
|
variable = identifier[ px::bind(&MyContext::resolve_variable, _r1, _1, _val) ];
|
|
variable.name("variable name");
|
|
|
|
regular_expression = raw[lexeme['/' > *((utf8char - char_('\\') - char_('/')) | ('\\' > char_)) > '/']];
|
|
regular_expression.name("regular_expression");
|
|
|
|
keywords.add
|
|
("and")
|
|
("digits")
|
|
("zdigits")
|
|
("empty")
|
|
("if")
|
|
("int")
|
|
("is_nil")
|
|
("local")
|
|
//("inf")
|
|
("else")
|
|
("elsif")
|
|
("endif")
|
|
("false")
|
|
("global")
|
|
("interpolate_table")
|
|
("min")
|
|
("max")
|
|
("random")
|
|
("regex_replace")
|
|
("filament_change")
|
|
("repeat")
|
|
("round")
|
|
("floor")
|
|
("ceil")
|
|
("not")
|
|
("one_of")
|
|
("or")
|
|
("size")
|
|
("true");
|
|
|
|
if (0) {
|
|
debug(start);
|
|
debug(text);
|
|
debug(text_block);
|
|
debug(macros);
|
|
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);
|
|
debug(equality_expression);
|
|
debug(bool_expr_eval);
|
|
debug(relational_expression);
|
|
debug(additive_expression);
|
|
debug(multiplicative_expression);
|
|
debug(unary_expression);
|
|
debug(one_of);
|
|
debug(one_of_list);
|
|
debug(optional_parameter);
|
|
debug(variable_reference);
|
|
debug(variable);
|
|
debug(regular_expression);
|
|
}
|
|
}
|
|
|
|
// Generic expression over expr.
|
|
typedef qi::rule<Iterator, expr(const MyContext*), skipper> RuleExpression;
|
|
|
|
// The start of the grammar.
|
|
qi::rule<Iterator, std::string(const MyContext*), qi::locals<bool>, skipper> start;
|
|
// A free-form text.
|
|
qi::rule<Iterator, std::string(), skipper> text;
|
|
// A free-form text, possibly empty, possibly containing macro expansions.
|
|
qi::rule<Iterator, std::string(const MyContext*), skipper> text_block;
|
|
// Statements enclosed in curely braces {}
|
|
qi::rule<Iterator, std::string(const MyContext*), skipper> block, statement, macros, if_text_block, if_macros, else_macros;
|
|
// Legacy variable expansion of the original Slic3r, in the form of [scalar_variable] or [vector_variable_index].
|
|
qi::rule<Iterator, std::string(const MyContext*), skipper> legacy_variable_expansion;
|
|
// Parsed identifier name.
|
|
qi::rule<Iterator, IteratorRange(), skipper> identifier;
|
|
// Ternary operator (?:) over logical_or_expression.
|
|
qi::rule<Iterator, expr(const MyContext*), qi::locals<bool>, skipper> conditional_expression;
|
|
// Logical or over logical_and_expressions.
|
|
RuleExpression logical_or_expression;
|
|
// Logical and over relational_expressions.
|
|
RuleExpression logical_and_expression;
|
|
// <, >, <=, >=
|
|
RuleExpression relational_expression;
|
|
// Math expression consisting of +- operators over multiplicative_expressions.
|
|
RuleExpression additive_expression;
|
|
// Boolean expressions over expressions.
|
|
RuleExpression equality_expression;
|
|
// Math expression consisting of */ operators over factors.
|
|
RuleExpression multiplicative_expression;
|
|
// Number literals, functions, braced expressions, variable references, variable indexing references.
|
|
RuleExpression unary_expression;
|
|
// Accepting an optional parameter.
|
|
RuleExpression optional_parameter;
|
|
// Rule to capture a regular expression enclosed in //.
|
|
qi::rule<Iterator, IteratorRange(), skipper> regular_expression;
|
|
// Evaluate boolean expression into bool.
|
|
qi::rule<Iterator, bool(const MyContext*), skipper> bool_expr_eval;
|
|
// Reference of a scalar variable, or reference to a field of a vector variable.
|
|
qi::rule<Iterator, OptWithPos(const MyContext*), qi::locals<OptWithPos, int>, skipper> variable_reference;
|
|
// Rule to translate an identifier to a ConfigOption, or to fail.
|
|
qi::rule<Iterator, OptWithPos(const MyContext*), skipper> variable;
|
|
// Evaluating whether a nullable variable is nil.
|
|
qi::rule<Iterator, expr(const MyContext*), skipper> is_nil_test;
|
|
// Evaluating "one of" list of patterns.
|
|
qi::rule<Iterator, expr(const MyContext*), qi::locals<expr>, skipper> one_of;
|
|
qi::rule<Iterator, expr(const MyContext*, const expr ¶m), skipper> one_of_list;
|
|
// Evaluating the "interpolate_table" expression.
|
|
qi::rule<Iterator, expr(const MyContext*), qi::locals<expr>, skipper> interpolate_table;
|
|
qi::rule<Iterator, InterpolateTableContext(const MyContext*, const expr ¶m), skipper> interpolate_table_list;
|
|
|
|
qi::rule<Iterator, std::string(const MyContext*), qi::locals<bool>, skipper> if_else_output;
|
|
qi::rule<Iterator, std::string(const MyContext*), qi::locals<OptWithPos>, skipper> assignment_statement;
|
|
// Allocating new local or global variables.
|
|
qi::rule<Iterator, std::string(const MyContext*), qi::locals<bool, MyContext::NewOldVariable>, skipper> new_variable_statement;
|
|
qi::rule<Iterator, std::vector<expr>(const MyContext*), skipper> initializer_list;
|
|
|
|
qi::symbols<char> keywords;
|
|
};
|
|
}
|
|
|
|
static const client::macro_processor g_macro_processor_instance;
|
|
|
|
static std::string process_macro(const std::string &templ, client::MyContext &context)
|
|
{
|
|
std::string output;
|
|
phrase_parse(templ.begin(), templ.end(), g_macro_processor_instance(&context), client::skipper{}, output);
|
|
if (! context.error_message.empty()) {
|
|
if (context.error_message.back() != '\n' && context.error_message.back() != '\r')
|
|
context.error_message += '\n';
|
|
throw Slic3r::PlaceholderParserError(context.error_message);
|
|
}
|
|
return output;
|
|
}
|
|
|
|
std::string PlaceholderParser::process(const std::string &templ, unsigned int current_extruder_id, const DynamicConfig *config_override, DynamicConfig *config_outputs, ContextData *context_data) const
|
|
{
|
|
client::MyContext context;
|
|
context.external_config = this->external_config();
|
|
context.config = &this->config();
|
|
context.config_override = config_override;
|
|
context.config_outputs = config_outputs;
|
|
context.current_extruder_id = current_extruder_id;
|
|
context.context_data = context_data;
|
|
return process_macro(templ, context);
|
|
}
|
|
|
|
// Evaluate a boolean expression using the full expressive power of the PlaceholderParser boolean expression syntax.
|
|
// Throws Slic3r::RuntimeError on syntax or runtime error.
|
|
bool PlaceholderParser::evaluate_boolean_expression(const std::string &templ, const DynamicConfig &config, const DynamicConfig *config_override)
|
|
{
|
|
client::MyContext context;
|
|
context.config = &config;
|
|
context.config_override = config_override;
|
|
// Let the macro processor parse just a boolean expression, not the full macro language.
|
|
context.just_boolean_expression = true;
|
|
return process_macro(templ, context) == "true";
|
|
}
|
|
|
|
}
|