Merge branch 'master' of https://github.com/prusa3d/PrusaSlicer into et_labels

This commit is contained in:
Enrico Turri 2020-02-05 15:27:20 +01:00
commit 5c4b481e35
30 changed files with 846 additions and 270 deletions

View File

@ -591,7 +591,7 @@ bool CLI::setup(int argc, char **argv)
// Initialize with defaults.
for (const t_optiondef_map *options : { &cli_actions_config_def.options, &cli_transform_config_def.options, &cli_misc_config_def.options })
for (const std::pair<t_config_option_key, ConfigOptionDef> &optdef : *options)
m_config.optptr(optdef.first, true);
m_config.option(optdef.first, true);
set_data_dir(m_config.opt_string("datadir"));

View File

@ -757,6 +757,12 @@ ConfigOption* DynamicConfig::optptr(const t_config_option_key &opt_key, bool cre
return opt;
}
const ConfigOption* DynamicConfig::optptr(const t_config_option_key &opt_key) const
{
auto it = options.find(opt_key);
return (it == options.end()) ? nullptr : it->second.get();
}
void DynamicConfig::read_cli(const std::vector<std::string> &tokens, t_config_option_keys* extra, t_config_option_keys* keys)
{
std::vector<const char*> args;

View File

@ -1494,8 +1494,49 @@ protected:
ConfigOptionDef* add_nullable(const t_config_option_key &opt_key, ConfigOptionType type);
};
// A pure interface to resolving ConfigOptions.
// This pure interface is useful as a base of ConfigBase, also it may be overriden to combine
// various config sources.
class ConfigOptionResolver
{
public:
ConfigOptionResolver() {}
virtual ~ConfigOptionResolver() {}
// Find a ConfigOption instance for a given name.
virtual const ConfigOption* optptr(const t_config_option_key &opt_key) const = 0;
bool has(const t_config_option_key &opt_key) const { return this->optptr(opt_key) != nullptr; }
const ConfigOption* option(const t_config_option_key &opt_key) const { return this->optptr(opt_key); }
template<typename TYPE>
const TYPE* option(const t_config_option_key& opt_key) const
{
const ConfigOption* opt = this->optptr(opt_key);
return (opt == nullptr || opt->type() != TYPE::static_type()) ? nullptr : static_cast<const TYPE*>(opt);
}
const ConfigOption* option_throw(const t_config_option_key& opt_key) const
{
const ConfigOption* opt = this->optptr(opt_key);
if (opt == nullptr)
throw UnknownOptionException(opt_key);
return opt;
}
template<typename TYPE>
const TYPE* option_throw(const t_config_option_key& opt_key) const
{
const ConfigOption* opt = this->option_throw(opt_key);
if (opt->type() != TYPE::static_type())
throw BadOptionTypeException("Conversion to a wrong type");
return static_cast<TYPE*>(opt);
}
};
// An abstract configuration store.
class ConfigBase
class ConfigBase : public ConfigOptionResolver
{
public:
// Definition of configuration values for the purpose of GUI presentation, editing, value mapping and config file handling.
@ -1503,7 +1544,7 @@ public:
// but it carries the defaults of the configuration values.
ConfigBase() {}
virtual ~ConfigBase() {}
~ConfigBase() override {}
// Virtual overridables:
public:
@ -1513,6 +1554,7 @@ public:
virtual ConfigOption* optptr(const t_config_option_key &opt_key, bool create = false) = 0;
// Collect names of all configuration values maintained by this configuration store.
virtual t_config_option_keys keys() const = 0;
protected:
// Verify whether the opt_key has not been obsoleted or renamed.
// Both opt_key and value may be modified by handle_legacy().
@ -1521,12 +1563,10 @@ protected:
virtual void handle_legacy(t_config_option_key &/*opt_key*/, std::string &/*value*/) const {}
public:
using ConfigOptionResolver::option;
using ConfigOptionResolver::option_throw;
// Non-virtual methods:
bool has(const t_config_option_key &opt_key) const { return this->option(opt_key) != nullptr; }
const ConfigOption* option(const t_config_option_key &opt_key) const
{ return const_cast<ConfigBase*>(this)->option(opt_key, false); }
ConfigOption* option(const t_config_option_key &opt_key, bool create = false)
{ return this->optptr(opt_key, create); }
@ -1537,10 +1577,6 @@ public:
return (opt == nullptr || opt->type() != TYPE::static_type()) ? nullptr : static_cast<TYPE*>(opt);
}
template<typename TYPE>
const TYPE* option(const t_config_option_key &opt_key) const
{ return const_cast<ConfigBase*>(this)->option<TYPE>(opt_key, false); }
ConfigOption* option_throw(const t_config_option_key &opt_key, bool create = false)
{
ConfigOption *opt = this->optptr(opt_key, create);
@ -1549,9 +1585,6 @@ public:
return opt;
}
const ConfigOption* option_throw(const t_config_option_key &opt_key) const
{ return const_cast<ConfigBase*>(this)->option_throw(opt_key, false); }
template<typename TYPE>
TYPE* option_throw(const t_config_option_key &opt_key, bool create = false)
{
@ -1561,10 +1594,6 @@ public:
return static_cast<TYPE*>(opt);
}
template<typename TYPE>
const TYPE* option_throw(const t_config_option_key &opt_key) const
{ return const_cast<ConfigBase*>(this)->option_throw<TYPE>(opt_key, false); }
// Apply all keys of other ConfigBase defined by this->def() to this ConfigBase.
// An UnknownOptionException is thrown in case some option keys of other are not defined by this->def(),
// or this ConfigBase is of a StaticConfig type and it does not support some of the keys, and ignore_nonexistent is not set.
@ -1735,6 +1764,8 @@ public:
{ return dynamic_cast<T*>(this->option(opt_key, create)); }
template<class T> const T* opt(const t_config_option_key &opt_key) const
{ return dynamic_cast<const T*>(this->option(opt_key)); }
// Overrides ConfigResolver::optptr().
const ConfigOption* optptr(const t_config_option_key &opt_key) const override;
// Overrides ConfigBase::optptr(). Find ando/or create a ConfigOption instance for a given name.
ConfigOption* optptr(const t_config_option_key &opt_key, bool create = false) override;
// Overrides ConfigBase::keys(). Collect names of all configuration values maintained by this configuration store.

View File

@ -1,12 +1,18 @@
#include "Flow.hpp"
#include "I18N.hpp"
#include "Print.hpp"
#include <cmath>
#include <assert.h>
#include <boost/algorithm/string/predicate.hpp>
// Mark string for localization and translate.
#define L(s) Slic3r::I18N::translate(s)
namespace Slic3r {
// This static method returns a sane extrusion width default.
static inline float auto_extrusion_width(FlowRole role, float nozzle_diameter, float height)
float Flow::auto_extrusion_width(FlowRole role, float nozzle_diameter)
{
switch (role) {
case frSupportMaterial:
@ -22,6 +28,92 @@ static inline float auto_extrusion_width(FlowRole role, float nozzle_diameter, f
}
}
// Used by the Flow::extrusion_width() funtion to provide hints to the user on default extrusion width values,
// and to provide reasonable values to the PlaceholderParser.
static inline FlowRole opt_key_to_flow_role(const std::string &opt_key)
{
if (opt_key == "perimeter_extrusion_width" ||
// or all the defaults:
opt_key == "extrusion_width" || opt_key == "first_layer_extrusion_width")
return frPerimeter;
else if (opt_key == "external_perimeter_extrusion_width")
return frExternalPerimeter;
else if (opt_key == "infill_extrusion_width")
return frInfill;
else if (opt_key == "solid_infill_extrusion_width")
return frSolidInfill;
else if (opt_key == "top_infill_extrusion_width")
return frTopSolidInfill;
else if (opt_key == "support_material_extrusion_width")
return frSupportMaterial;
else
throw std::runtime_error("opt_key_to_flow_role: invalid argument");
};
static inline void throw_on_missing_variable(const std::string &opt_key, const char *dependent_opt_key)
{
throw std::runtime_error((boost::format(L("Cannot calculate extrusion width for %1%: Variable \"%2%\" not accessible.")) % opt_key % dependent_opt_key).str());
}
// Used to provide hints to the user on default extrusion width values, and to provide reasonable values to the PlaceholderParser.
double Flow::extrusion_width(const std::string& opt_key, const ConfigOptionFloatOrPercent* opt, const ConfigOptionResolver& config, const unsigned int first_printing_extruder)
{
assert(opt != nullptr);
bool first_layer = boost::starts_with(opt_key, "first_layer_");
#if 0
// This is the logic used for skit / brim, but not for the rest of the 1st layer.
if (opt->value == 0. && first_layer) {
// The "first_layer_extrusion_width" was set to zero, try a substitute.
opt = config.option<ConfigOptionFloatOrPercent>("perimeter_extrusion_width");
if (opt == nullptr)
throw_on_missing_variable(opt_key, "perimeter_extrusion_width");
}
#endif
if (opt->value == 0.) {
// The role specific extrusion width value was set to zero, try the role non-specific extrusion width.
opt = config.option<ConfigOptionFloatOrPercent>("extrusion_width");
if (opt == nullptr)
throw_on_missing_variable(opt_key, "extrusion_width");
// Use the "layer_height" instead of "first_layer_height".
first_layer = false;
}
if (opt->percent) {
auto opt_key_layer_height = first_layer ? "first_layer_height" : "layer_height";
auto opt_layer_height = config.option(opt_key_layer_height);
if (opt_layer_height == nullptr)
throw_on_missing_variable(opt_key, opt_key_layer_height);
double layer_height = opt_layer_height->getFloat();
if (first_layer && static_cast<const ConfigOptionFloatOrPercent*>(opt_layer_height)->percent) {
// first_layer_height depends on layer_height.
opt_layer_height = config.option("layer_height");
if (opt_layer_height == nullptr)
throw_on_missing_variable(opt_key, "layer_height");
layer_height *= 0.01 * opt_layer_height->getFloat();
}
return opt->get_abs_value(layer_height);
}
if (opt->value == 0.) {
// If user left option to 0, calculate a sane default width.
auto opt_nozzle_diameters = config.option<ConfigOptionFloats>("nozzle_diameter");
if (opt_nozzle_diameters == nullptr)
throw_on_missing_variable(opt_key, "nozzle_diameter");
return auto_extrusion_width(opt_key_to_flow_role(opt_key), float(opt_nozzle_diameters->get_at(first_printing_extruder)));
}
return opt->value;
}
// Used to provide hints to the user on default extrusion width values, and to provide reasonable values to the PlaceholderParser.
double Flow::extrusion_width(const std::string& opt_key, const ConfigOptionResolver &config, const unsigned int first_printing_extruder)
{
return extrusion_width(opt_key, config.option<ConfigOptionFloatOrPercent>(opt_key), config, first_printing_extruder);
}
// This constructor builds a Flow object from an extrusion width config setting
// and other context properties.
Flow Flow::new_from_config_width(FlowRole role, const ConfigOptionFloatOrPercent &width, float nozzle_diameter, float height, float bridge_flow_ratio)
@ -39,7 +131,7 @@ Flow Flow::new_from_config_width(FlowRole role, const ConfigOptionFloatOrPercent
sqrt(bridge_flow_ratio) * nozzle_diameter;
} else if (! width.percent && width.value == 0.) {
// If user left option to 0, calculate a sane default width.
w = auto_extrusion_width(role, nozzle_diameter, height);
w = auto_extrusion_width(role, nozzle_diameter);
} else {
// If user set a manual value, use it.
w = float(width.get_abs_value(height));

View File

@ -64,6 +64,16 @@ public:
// This method is used exclusively to calculate new flow of 100% infill, where the extrusion width was allowed to scale
// to fit a region with integer number of lines.
static Flow new_from_spacing(float spacing, float nozzle_diameter, float height, bool bridge);
// Sane extrusion width defautl based on nozzle diameter.
// The defaults were derived from manual Prusa MK3 profiles.
static float auto_extrusion_width(FlowRole role, float nozzle_diameter);
// Extrusion width from full config, taking into account the defaults (when set to zero) and ratios (percentages).
// Precise value depends on layer index (1st layer vs. other layers vs. variable layer height),
// on active extruder etc. Therefore the value calculated by this function shall be used as a hint only.
static double extrusion_width(const std::string &opt_key, const ConfigOptionFloatOrPercent *opt, const ConfigOptionResolver &config, const unsigned int first_printing_extruder = 0);
static double extrusion_width(const std::string &opt_key, const ConfigOptionResolver &config, const unsigned int first_printing_extruder = 0);
};
extern Flow support_material_flow(const PrintObject *object, float layer_height = 0.f);

View File

@ -1,4 +1,5 @@
#include "PlaceholderParser.hpp"
#include "Flow.hpp"
#include <cstring>
#include <ctime>
#include <iomanip>
@ -99,11 +100,7 @@ static inline bool opts_equal(const DynamicConfig &config_old, const DynamicConf
const ConfigOption *opt_old = config_old.option(opt_key);
const ConfigOption *opt_new = config_new.option(opt_key);
assert(opt_new != nullptr);
if (opt_old == nullptr)
return false;
return (opt_new->type() == coFloatOrPercent) ?
dynamic_cast<const ConfigOptionFloat*>(opt_old)->value == config_new.get_abs_value(opt_key) :
*opt_new == *opt_old;
return opt_old != nullptr && *opt_new == *opt_old;
}
std::vector<std::string> PlaceholderParser::config_diff(const DynamicPrintConfig &rhs)
@ -126,14 +123,7 @@ 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)) {
// Store a copy of the config option.
// Convert FloatOrPercent values to floats first.
//FIXME there are some ratio_over chains, which end with empty ratio_with.
// For example, XXX_extrusion_width parameters are not handled by get_abs_value correctly.
const ConfigOption *opt_rhs = rhs.option(opt_key);
this->set(opt_key, (opt_rhs->type() == coFloatOrPercent) ?
new ConfigOptionFloat(rhs.get_abs_value(opt_key)) :
opt_rhs->clone());
this->set(opt_key, rhs.option(opt_key)->clone());
modified = true;
}
}
@ -142,16 +132,8 @@ bool PlaceholderParser::apply_config(const DynamicPrintConfig &rhs)
void PlaceholderParser::apply_only(const DynamicPrintConfig &rhs, const std::vector<std::string> &keys)
{
for (const t_config_option_key &opt_key : keys) {
// Store a copy of the config option.
// Convert FloatOrPercent values to floats first.
//FIXME there are some ratio_over chains, which end with empty ratio_with.
// For example, XXX_extrusion_width parameters are not handled by get_abs_value correctly.
const ConfigOption *opt_rhs = rhs.option(opt_key);
this->set(opt_key, (opt_rhs->type() == coFloatOrPercent) ?
new ConfigOptionFloat(rhs.get_abs_value(opt_key)) :
opt_rhs->clone());
}
for (const t_config_option_key &opt_key : keys)
this->set(opt_key, rhs.option(opt_key)->clone());
}
void PlaceholderParser::apply_config(DynamicPrintConfig &&rhs)
@ -635,7 +617,7 @@ namespace client
return os;
}
struct MyContext {
struct MyContext : public ConfigOptionResolver {
const DynamicConfig *external_config = nullptr;
const DynamicConfig *config = nullptr;
const DynamicConfig *config_override = nullptr;
@ -650,7 +632,7 @@ namespace client
static void evaluate_full_macro(const MyContext *ctx, bool &result) { result = ! ctx->just_boolean_expression; }
const ConfigOption* resolve_symbol(const std::string &opt_key) const
const ConfigOption* optptr(const t_config_option_key &opt_key) const override
{
const ConfigOption *opt = nullptr;
if (config_override != nullptr)
@ -662,6 +644,8 @@ namespace client
return opt;
}
const ConfigOption* resolve_symbol(const std::string &opt_key) const { return this->optptr(opt_key); }
template <typename Iterator>
static void legacy_variable_expansion(
const MyContext *ctx,
@ -758,7 +742,43 @@ namespace client
case coPoint: output.set_s(opt.opt->serialize()); break;
case coBool: output.set_b(opt.opt->getBool()); break;
case coFloatOrPercent:
ctx->throw_exception("FloatOrPercent variables are not supported", opt.it_range);
{
std::string opt_key(opt.it_range.begin(), opt.it_range.end());
if (boost::ends_with(opt_key, "extrusion_width")) {
// Extrusion 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("FloatOrPercent variable failed to resolve the \"ratio_over\" dependencies", opt.it_range);
if (boost::ends_with(opt_def->ratio_over, "extrusion_width")) {
// Extrusion 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("Unknown scalar variable type", opt.it_range);
}

View File

@ -24,8 +24,7 @@
#include <boost/format.hpp>
#include <boost/log/trivial.hpp>
//! macro used to mark string used at localization,
//! return same string
// Mark string for localization and translate.
#define L(s) Slic3r::I18N::translate(s)
namespace Slic3r {
@ -495,7 +494,7 @@ static bool custom_per_printz_gcodes_tool_changes_differ(const std::vector<Custo
{
auto it_a = va.begin();
auto it_b = vb.begin();
while (it_a != va.end() && it_b != vb.end()) {
while (it_a != va.end() || it_b != vb.end()) {
if (it_a != va.end() && it_a->gcode != ToolChangeCode) {
// Skip any CustomGCode items, which are not tool changes.
++ it_a;
@ -527,7 +526,6 @@ void Print::config_diffs(
const DynamicPrintConfig &new_full_config,
t_config_option_keys &print_diff, t_config_option_keys &object_diff, t_config_option_keys &region_diff,
t_config_option_keys &full_config_diff,
DynamicPrintConfig &placeholder_parser_overrides,
DynamicPrintConfig &filament_overrides) const
{
// Collect changes to print config, account for overrides of extruder retract values by filament presets.
@ -563,19 +561,11 @@ void Print::config_diffs(
object_diff = m_default_object_config.diff(new_full_config);
region_diff = m_default_region_config.diff(new_full_config);
// Prepare for storing of the full print config into new_full_config to be exported into the G-code and to be used by the PlaceholderParser.
// As the PlaceholderParser does not interpret the FloatOrPercent values itself, these values are stored into the PlaceholderParser converted to floats.
for (const t_config_option_key &opt_key : new_full_config.keys()) {
const ConfigOption *opt_old = m_full_print_config.option(opt_key);
const ConfigOption *opt_new = new_full_config.option(opt_key);
if (opt_old == nullptr || *opt_new != *opt_old)
full_config_diff.emplace_back(opt_key);
if (opt_new->type() == coFloatOrPercent) {
// The m_placeholder_parser is never modified by the background processing, GCode.cpp/hpp makes a copy.
const ConfigOption *opt_old_pp = this->placeholder_parser().config().option(opt_key);
double new_value = new_full_config.get_abs_value(opt_key);
if (opt_old_pp == nullptr || static_cast<const ConfigOptionFloat*>(opt_old_pp)->value != new_value)
placeholder_parser_overrides.set_key_value(opt_key, new ConfigOptionFloat(new_value));
}
}
}
@ -593,8 +583,8 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
// Find modified keys of the various configs. Resolve overrides extruder retract values by filament profiles.
t_config_option_keys print_diff, object_diff, region_diff, full_config_diff;
DynamicPrintConfig placeholder_parser_overrides, filament_overrides;
this->config_diffs(new_full_config, print_diff, object_diff, region_diff, full_config_diff, placeholder_parser_overrides, filament_overrides);
DynamicPrintConfig filament_overrides;
this->config_diffs(new_full_config, print_diff, object_diff, region_diff, full_config_diff, filament_overrides);
// Do not use the ApplyStatus as we will use the max function when updating apply_status.
unsigned int apply_status = APPLY_STATUS_UNCHANGED;
@ -614,9 +604,8 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
// which should be stopped if print_diff is not empty.
size_t num_extruders = m_config.nozzle_diameter.size();
bool num_extruders_changed = false;
if (! full_config_diff.empty() || ! placeholder_parser_overrides.empty()) {
if (! full_config_diff.empty()) {
update_apply_status(this->invalidate_step(psGCodeExport));
m_placeholder_parser.apply_config(std::move(placeholder_parser_overrides));
// Set the profile aliases for the PrintBase::output_filename()
m_placeholder_parser.set("print_preset", new_full_config.option("print_settings_id")->clone());
m_placeholder_parser.set("filament_preset", new_full_config.option("filament_settings_id")->clone());

View File

@ -435,7 +435,6 @@ private:
const DynamicPrintConfig &new_full_config,
t_config_option_keys &print_diff, t_config_option_keys &object_diff, t_config_option_keys &region_diff,
t_config_option_keys &full_config_diff,
DynamicPrintConfig &placeholder_parser_overrides,
DynamicPrintConfig &filament_overrides) const;
bool invalidate_state_by_config_options(const std::vector<t_config_option_key> &opt_keys);

View File

@ -718,8 +718,9 @@ void PrintConfigDef::init_fff_params()
def->gui_type = "f_enum_open";
def->gui_flags = "show_value";
def->enum_values.push_back("PLA");
def->enum_values.push_back("ABS");
def->enum_values.push_back("PET");
def->enum_values.push_back("ABS");
def->enum_values.push_back("ASA");
def->enum_values.push_back("FLEX");
def->enum_values.push_back("HIPS");
def->enum_values.push_back("EDGE");

View File

@ -46,12 +46,6 @@ enum SeamPosition {
spRandom, spNearest, spAligned, spRear
};
/*
enum FilamentType {
ftPLA, ftABS, ftPET, ftHIPS, ftFLEX, ftSCAFF, ftEDGE, ftNGEN, ftPVA
};
*/
enum SLAMaterial {
slamTough,
slamFlex,
@ -149,24 +143,6 @@ template<> inline const t_config_enum_values& ConfigOptionEnum<SeamPosition>::ge
return keys_map;
}
/*
template<> inline const t_config_enum_values& ConfigOptionEnum<FilamentType>::get_enum_values() {
static t_config_enum_values keys_map;
if (keys_map.empty()) {
keys_map["PLA"] = ftPLA;
keys_map["ABS"] = ftABS;
keys_map["PET"] = ftPET;
keys_map["HIPS"] = ftHIPS;
keys_map["FLEX"] = ftFLEX;
keys_map["SCAFF"] = ftSCAFF;
keys_map["EDGE"] = ftEDGE;
keys_map["NGEN"] = ftNGEN;
keys_map["PVA"] = ftPVA;
}
return keys_map;
}
*/
template<> inline const t_config_enum_values& ConfigOptionEnum<SLADisplayOrientation>::get_enum_values() {
static const t_config_enum_values keys_map = {
{ "landscape", sladoLandscape},
@ -354,6 +330,9 @@ protected:
#define STATIC_PRINT_CONFIG_CACHE_BASE(CLASS_NAME) \
public: \
/* Overrides ConfigBase::optptr(). Find ando/or create a ConfigOption instance for a given name. */ \
const ConfigOption* optptr(const t_config_option_key &opt_key) const override \
{ return s_cache_##CLASS_NAME.optptr(opt_key, this); } \
/* Overrides ConfigBase::optptr(). Find ando/or create a ConfigOption instance for a given name. */ \
ConfigOption* optptr(const t_config_option_key &opt_key, bool create = false) override \
{ return s_cache_##CLASS_NAME.optptr(opt_key, this); } \
/* Overrides ConfigBase::keys(). Collect names of all configuration values maintained by this configuration store. */ \

View File

@ -143,7 +143,7 @@ void SLAPrint::Steps::drill_holes(SLAPrintObject &po)
holes_mesh.merge(sla::to_triangle_mesh(holept.to_mesh()));
holes_mesh.require_shared_vertices();
MeshBoolean::cgal::self_union(holes_mesh); //FIXME-fix and use the cgal version
MeshBoolean::self_union(holes_mesh);
try {
MeshBoolean::cgal::minus(hollowed_mesh, holes_mesh);

View File

@ -66,6 +66,10 @@ bool Version::is_current_slic3r_supported() const
return this->is_slic3r_supported(Slic3r::SEMVER);
}
bool Version::is_current_slic3r_downgrade() const
{
return Slic3r::SEMVER < min_slic3r_version;
}
#if 0
//TODO: This test should be moved to a unit test, once we have C++ unit tests in place.
static int version_test()

View File

@ -29,6 +29,7 @@ struct Version
bool is_slic3r_supported(const Semver &slicer_version) const;
bool is_current_slic3r_supported() const;
bool is_current_slic3r_downgrade() const;
};
// Index of vendor specific config bundle versions and Slic3r compatibilities.

View File

@ -2001,6 +2001,10 @@ ConfigWizard::ConfigWizard(wxWindow *parent)
p->page_msla = new PagePrinters(this, _(L("Prusa MSLA Technology Printers")), "Prusa MSLA", *vendor_prusa, 0, T_SLA);
p->add_page(p->page_msla);
// Pages for 3rd party vendors
p->create_3rdparty_pages(); // Needs to be done _before_ creating PageVendors
p->add_page(p->page_vendors = new PageVendors(this));
p->any_sla_selected = p->check_sla_selected();
p->any_fff_selected = p->check_fff_selected();
@ -2022,10 +2026,6 @@ ConfigWizard::ConfigWizard(wxWindow *parent)
p->add_page(p->page_diams = new PageDiameters(this));
p->add_page(p->page_temps = new PageTemperatures(this));
// Pages for 3rd party vendors
p->create_3rdparty_pages(); // Needs to be done _before_ creating PageVendors
p->add_page(p->page_vendors = new PageVendors(this));
p->load_pages();
p->index->go_to(size_t{0});

View File

@ -603,21 +603,24 @@ std::string Control::get_color_for_color_change_tick(std::set<TickCode>::const_i
return "";
}
void Control::draw_colored_band(wxDC& dc)
wxRect Control::get_colored_band_rect()
{
if (!m_is_enabled_tick_manipulation)
return;
int height, width;
get_size(&width, &height);
const wxCoord mid = is_horizontal() ? 0.5 * height : 0.5 * width;
wxRect main_band = is_horizontal() ?
return is_horizontal() ?
wxRect(SLIDER_MARGIN, lround(mid - 0.375 * m_thumb_size.y),
width - 2 * SLIDER_MARGIN + 1, lround(0.75 * m_thumb_size.y)) :
wxRect(lround(mid - 0.375 * m_thumb_size.x), SLIDER_MARGIN,
lround(0.75 * m_thumb_size.x), height - 2 * SLIDER_MARGIN + 1);
}
void Control::draw_colored_band(wxDC& dc)
{
if (!m_is_enabled_tick_manipulation)
return;
auto draw_band = [](wxDC& dc, const wxColour& clr, const wxRect& band_rc)
{
@ -626,6 +629,8 @@ void Control::draw_colored_band(wxDC& dc)
dc.DrawRectangle(band_rc);
};
wxRect main_band = get_colored_band_rect();
// don't color a band for MultiExtruder mode
if (m_ticks.empty() || m_mode == t_mode::MultiExtruder)
{
@ -875,28 +880,24 @@ void Control::correct_higher_value()
m_lower_value = m_higher_value;
}
wxString Control::get_tooltip(IconFocus icon_focus)
wxString Control::get_tooltip(FocusItem focused_item, int tick/*=-1*/)
{
wxString tooltip(wxEmptyString);
if (m_is_one_layer_icon_focesed)
tooltip = _(L("One layer mode"));
if (focused_item == fiNone)
return "";
if (focused_item == fiOneLayerIcon)
return _(L("One layer mode"));
if (focused_item == fiRevertIcon)
return _(L("Discard all custom changes"));
if (focused_item == fiCogIcon)
return _(L("Set extruder sequence for whole print"));
if (focused_item == fiColorBand)
return m_mode != t_mode::SingleExtruder ? "" :
_(L("For edit current color use right mouse button click on colored band"));
if (icon_focus == ifRevert)
tooltip = _(L("Discard all custom changes"));
if (icon_focus == ifCog)
tooltip = _(L("Set extruder sequence for whole print"));
else if (m_is_action_icon_focesed)
{
const int tick = m_selection == ssLower ? m_lower_value : m_higher_value;
wxString tooltip;
const auto tick_code_it = m_ticks.ticks.find(TickCode{tick});
/* Note: just on OSX!!!
* Right click event causes a little scrolling.
* So, as a workaround we use Ctrl+LeftMouseClick instead of RightMouseClick
* Show this information in tooltip
* */
if (tick_code_it == m_ticks.ticks.end()) // tick doesn't exist
if (tick_code_it == m_ticks.ticks.end() && focused_item == fiActionIcon) // tick doesn't exist
{
// Show mode as a first string of tooltop
tooltip = " " + _(L("Slider(print) mode")) + ": ";
@ -905,16 +906,27 @@ wxString Control::get_tooltip(IconFocus icon_focus)
CustomGCode::MultiExtruderMode );
tooltip += "\n\n";
/* Note: just on OSX!!!
* Right click event causes a little scrolling.
* So, as a workaround we use Ctrl+LeftMouseClick instead of RightMouseClick
* Show this information in tooltip
* */
// Show list of actions with new tick
tooltip += ( m_mode == t_mode::MultiAsSingle ?
_(L("For add change extruder use left mouse button click")) :
m_mode == t_mode::SingleExtruder ?
_(L("For add color change use left mouse button click "
"if you want to use colors from default color list, "
"or Shift + left mouse button click if you want to select a color")) :
_(L("For add color change use left mouse button click")) ) + " " +
_(L("OR pres \"+\" key")) + "\n" + (
is_osx ?
_(L("For add another code use Ctrl + Left mouse button click")) :
_(L("For add another code use Ctrl + left mouse button click")) :
_(L("For add another code use right mouse button click")) );
}
else // tick exists
if (tick_code_it != m_ticks.ticks.end()) // tick exists
{
// Show custom Gcode as a first string of tooltop
tooltip = " ";
@ -948,14 +960,31 @@ wxString Control::get_tooltip(IconFocus icon_focus)
"Check your choice to avoid redundant color changes."));
// Show list of actions with existing tick
if (focused_item == fiActionIcon)
tooltip += "\n\n" + _(L("For Delete tick use left mouse button click OR pres \"-\" key")) + "\n" + (
is_osx ?
_(L("For Edit tick use Ctrl + Left mouse button click")) :
_(L("For Edit tick use right mouse button click")) );
}
return tooltip;
}
return tooltip;
int Control::get_edited_tick_for_position(const wxPoint pos, const std::string& gcode /*= ColorChangeCode*/)
{
if (m_ticks.empty())
return -1;
int tick = get_value_from_position(pos);
auto it = std::lower_bound(m_ticks.ticks.begin(), m_ticks.ticks.end(), TickCode{ tick });
while (it != m_ticks.ticks.begin()) {
--it;
if (it->gcode == gcode)
return it->tick;
}
return -1;
}
void Control::OnMotion(wxMouseEvent& event)
@ -966,14 +995,29 @@ void Control::OnMotion(wxMouseEvent& event)
const wxPoint pos = event.GetLogicalPosition(dc);
m_is_one_layer_icon_focesed = is_point_in_rect(pos, m_rect_one_layer_icon);
IconFocus icon_focus = ifNone;
FocusItem focused_item = fiNone;
int tick = -1;
if (!m_is_left_down && !m_is_one_layer) {
m_is_action_icon_focesed = is_point_in_rect(pos, m_rect_tick_action);
if (!m_ticks.empty() && is_point_in_rect(pos, m_rect_revert_icon))
icon_focus = ifRevert;
if (m_is_one_layer_icon_focesed)
focused_item = fiOneLayerIcon;
else if (m_is_action_icon_focesed) {
focused_item = fiActionIcon;
tick = m_selection == ssLower ? m_lower_value : m_higher_value;
}
else if (!m_ticks.empty() && is_point_in_rect(pos, m_rect_revert_icon))
focused_item = fiRevertIcon;
else if (is_point_in_rect(pos, m_rect_cog_icon))
icon_focus = ifCog;
focused_item = fiCogIcon;
else if (m_mode == t_mode::SingleExtruder && is_point_in_rect(pos, get_colored_band_rect()) &&
get_edited_tick_for_position(pos) >= 0 )
focused_item = fiColorBand;
else {
focused_item = fiTick;
tick = get_tick_near_point(pos);
}
}
else if (m_is_left_down || m_is_right_down) {
if (m_selection == ssLower) {
@ -994,7 +1038,7 @@ void Control::OnMotion(wxMouseEvent& event)
event.Skip();
// Set tooltips with information for each icon
this->SetToolTip(get_tooltip(icon_focus));
this->SetToolTip(get_tooltip(focused_item, tick));
if (action)
{
@ -1227,7 +1271,7 @@ void Control::OnRightDown(wxMouseEvent& event)
const wxClientDC dc(this);
wxPoint pos = event.GetLogicalPosition(dc);
if (is_point_in_rect(pos, m_rect_tick_action) && m_is_enabled_tick_manipulation)
if (m_is_enabled_tick_manipulation && is_point_in_rect(pos, m_rect_tick_action))
{
const int tick = m_selection == ssLower ? m_lower_value : m_higher_value;
if (m_ticks.ticks.find(TickCode{ tick }) == m_ticks.ticks.end()) // if on this Z doesn't exist tick
@ -1239,6 +1283,13 @@ void Control::OnRightDown(wxMouseEvent& event)
return;
}
if (m_is_enabled_tick_manipulation && m_mode == t_mode::SingleExtruder &&
is_point_in_rect(pos, get_colored_band_rect()))
{
m_force_color_edit = true;
return;
}
detect_selected_slider(event.GetLogicalPosition(dc));
if (!m_selection)
return;
@ -1409,6 +1460,17 @@ void Control::OnRightUp(wxMouseEvent& event)
m_show_edit_menu = false;
}
else if (m_force_color_edit)
{
const wxClientDC dc(this);
wxPoint pos = event.GetLogicalPosition(dc);
int edited_tick = get_edited_tick_for_position(pos);
if (edited_tick >= 0)
edit_tick(edited_tick);
m_force_color_edit = false;
}
Refresh();
Update();
@ -1572,9 +1634,10 @@ void Control::delete_current_tick()
post_ticks_changed_event(code);
}
void Control::edit_tick()
void Control::edit_tick(int tick/* = -1*/)
{
const int tick = m_selection == ssLower ? m_lower_value : m_higher_value;
if (tick < 0)
tick = m_selection == ssLower ? m_lower_value : m_higher_value;
const std::set<TickCode>::iterator it = m_ticks.ticks.find(TickCode{ tick });
if (it == m_ticks.ticks.end() ||
@ -1896,9 +1959,11 @@ ConflictType TickCodeInfo::is_conflict_tick(const TickCode& tick, t_mode out_mod
if (it == ticks.begin())
return tick.extruder == std::max<int>(only_extruder, 1) ? ctMeaninglessToolChange : ctNone;
while (it != ticks.begin()) {
--it;
if (it->gcode == ToolChangeCode && tick.extruder == it->extruder)
return ctMeaninglessToolChange;
if (it->gcode == ToolChangeCode)
return tick.extruder == it->extruder ? ctMeaninglessToolChange : ctNone;
}
}
return ctNone;

View File

@ -33,10 +33,14 @@ enum SelectedSlider {
ssHigher
};
enum IconFocus {
ifNone,
ifRevert,
ifCog
enum FocusItem {
fiNone,
fiRevertIcon,
fiOneLayerIcon,
fiCogIcon,
fiColorBand,
fiActionIcon,
fiTick
};
enum ConflictType
@ -221,7 +225,7 @@ public:
void add_current_tick(bool call_from_keyboard = false);
// delete current tick, when press "-"
void delete_current_tick();
void edit_tick();
void edit_tick(int tick = -1);
void edit_extruder_sequence();
ExtrudersSequence m_extruders_sequence;
@ -259,14 +263,17 @@ private:
wxString get_label(const SelectedSlider& selection) const;
void get_lower_and_higher_position(int& lower_pos, int& higher_pos);
int get_value_from_position(const wxCoord x, const wxCoord y);
int get_value_from_position(const wxPoint pos) { return get_value_from_position(pos.x, pos.y); }
wxCoord get_position_from_value(const int value);
wxSize get_size();
void get_size(int *w, int *h);
double get_double_value(const SelectedSlider& selection);
wxString get_tooltip(IconFocus icon_focus);
wxString get_tooltip(FocusItem focused_item, int tick = -1);
int get_edited_tick_for_position(wxPoint pos, const std::string& gcode = ColorChangeCode);
std::string get_color_for_tool_change_tick(std::set<TickCode>::const_iterator it) const;
std::string get_color_for_color_change_tick(std::set<TickCode>::const_iterator it) const;
wxRect get_colored_band_rect();
// Get active extruders for tick.
// Means one current extruder for not existing tick OR
@ -312,6 +319,7 @@ private:
bool m_force_mode_apply = true;
bool m_force_add_tick = false;
bool m_force_delete_tick = false;
bool m_force_color_edit = false;
t_mode m_mode = t_mode::SingleExtruder;
int m_only_extruder = -1;

View File

@ -690,7 +690,7 @@ void GLCanvas3D::WarningTexture::activate(WarningTexture::Warning warning, bool
case ObjectOutside : text = L("An object outside the print area was detected"); break;
case ToolpathOutside : text = L("A toolpath outside the print area was detected"); break;
case SlaSupportsOutside : text = L("SLA supports outside the print area were detected"); break;
case SomethingNotShown : text = L("Some objects are not visible when editing supports"); break;
case SomethingNotShown : text = L("Some objects are not visible"); break;
case ObjectClashed: {
text = L("An object outside the print area was detected\n"
"Resolve the current problem to continue slicing");
@ -1384,6 +1384,7 @@ wxDEFINE_EVENT(EVT_GLCANVAS_INCREASE_INSTANCES, Event<int>);
wxDEFINE_EVENT(EVT_GLCANVAS_INSTANCE_MOVED, SimpleEvent);
wxDEFINE_EVENT(EVT_GLCANVAS_INSTANCE_ROTATED, SimpleEvent);
wxDEFINE_EVENT(EVT_GLCANVAS_INSTANCE_SCALED, SimpleEvent);
wxDEFINE_EVENT(EVT_GLCANVAS_FORCE_UPDATE, SimpleEvent);
wxDEFINE_EVENT(EVT_GLCANVAS_WIPETOWER_MOVED, Vec3dEvent);
wxDEFINE_EVENT(EVT_GLCANVAS_WIPETOWER_ROTATED, Vec3dEvent);
wxDEFINE_EVENT(EVT_GLCANVAS_ENABLE_ACTION_BUTTONS, Event<bool>);
@ -2849,8 +2850,127 @@ void GLCanvas3D::on_char(wxKeyEvent& evt)
}
}
class TranslationProcessor
{
using UpAction = std::function<void(void)>;
using DownAction = std::function<void(const Vec3d&, bool, bool)>;
UpAction m_up_action{ nullptr };
DownAction m_down_action{ nullptr };
bool m_running{ false };
Vec3d m_direction{ Vec3d::UnitX() };
public:
TranslationProcessor(UpAction up_action, DownAction down_action)
: m_up_action(up_action), m_down_action(down_action)
{
}
void process(wxKeyEvent& evt)
{
const int keyCode = evt.GetKeyCode();
wxEventType type = evt.GetEventType();
if (type == wxEVT_KEY_UP) {
switch (keyCode)
{
case WXK_NUMPAD_LEFT: case WXK_LEFT:
case WXK_NUMPAD_RIGHT: case WXK_RIGHT:
case WXK_NUMPAD_UP: case WXK_UP:
case WXK_NUMPAD_DOWN: case WXK_DOWN:
{
m_running = false;
m_up_action();
break;
}
default: { break; }
}
}
else if (type == wxEVT_KEY_DOWN) {
bool apply = false;
switch (keyCode)
{
case WXK_SHIFT:
{
if (m_running)
apply = true;
break;
}
case WXK_NUMPAD_LEFT:
case WXK_LEFT:
{
m_direction = -Vec3d::UnitX();
apply = true;
break;
}
case WXK_NUMPAD_RIGHT:
case WXK_RIGHT:
{
m_direction = Vec3d::UnitX();
apply = true;
break;
}
case WXK_NUMPAD_UP:
case WXK_UP:
{
m_direction = Vec3d::UnitY();
apply = true;
break;
}
case WXK_NUMPAD_DOWN:
case WXK_DOWN:
{
m_direction = -Vec3d::UnitY();
apply = true;
break;
}
default: { break; }
}
if (apply) {
m_running = true;
m_down_action(m_direction, evt.ShiftDown(), evt.CmdDown());
}
}
}
};
void GLCanvas3D::on_key(wxKeyEvent& evt)
{
static TranslationProcessor translationProcessor(
[this]() {
do_move(L("Gizmo-Move"));
m_gizmos.update_data();
wxGetApp().obj_manipul()->set_dirty();
// Let the plater know that the dragging finished, so a delayed refresh
// of the scene with the background processing data should be performed.
post_event(SimpleEvent(EVT_GLCANVAS_MOUSE_DRAGGING_FINISHED));
// updates camera target constraints
refresh_camera_scene_box();
m_dirty = true;
},
[this](const Vec3d& direction, bool slow, bool camera_space) {
m_selection.start_dragging();
double multiplier = slow ? 1.0 : 10.0;
Vec3d displacement;
if (camera_space)
{
Eigen::Matrix<double, 3, 3, Eigen::DontAlign> inv_view_3x3 = m_camera.get_view_matrix().inverse().matrix().block(0, 0, 3, 3);
displacement = multiplier * (inv_view_3x3 * direction);
displacement(2) = 0.0;
}
else
displacement = multiplier * direction;
m_selection.translate(displacement);
m_dirty = true;
}
);
const int keyCode = evt.GetKeyCode();
auto imgui = wxGetApp().imgui();
@ -2869,6 +2989,8 @@ void GLCanvas3D::on_key(wxKeyEvent& evt)
}
else if (keyCode == WXK_SHIFT)
{
translationProcessor.process(evt);
if (m_picking_enabled && m_rectangle_selection.is_dragging())
{
_update_selection_from_hover();
@ -2892,26 +3014,10 @@ void GLCanvas3D::on_key(wxKeyEvent& evt)
else if (keyCode == WXK_CONTROL)
m_dirty = true;
else if (m_gizmos.is_enabled() && !m_selection.is_empty()) {
translationProcessor.process(evt);
switch (keyCode)
{
case WXK_NUMPAD_LEFT: case WXK_LEFT:
case WXK_NUMPAD_RIGHT: case WXK_RIGHT:
case WXK_NUMPAD_UP: case WXK_UP:
case WXK_NUMPAD_DOWN: case WXK_DOWN:
{
do_move(L("Gizmo-Move"));
m_gizmos.update_data();
wxGetApp().obj_manipul()->set_dirty();
// Let the plater know that the dragging finished, so a delayed refresh
// of the scene with the background processing data should be performed.
post_event(SimpleEvent(EVT_GLCANVAS_MOUSE_DRAGGING_FINISHED));
// updates camera target constraints
refresh_camera_scene_box();
m_dirty = true;
break;
}
case WXK_NUMPAD_PAGEUP: case WXK_PAGEUP:
case WXK_NUMPAD_PAGEDOWN: case WXK_PAGEDOWN:
{
@ -2936,6 +3042,8 @@ void GLCanvas3D::on_key(wxKeyEvent& evt)
m_tab_down = keyCode == WXK_TAB && !evt.HasAnyModifiers();
if (keyCode == WXK_SHIFT)
{
translationProcessor.process(evt);
if (m_picking_enabled && (m_gizmos.get_current_type() != GLGizmosManager::SlaSupports))
{
m_mouse.ignore_left_up = false;
@ -2954,12 +3062,6 @@ void GLCanvas3D::on_key(wxKeyEvent& evt)
m_dirty = true;
else if (m_gizmos.is_enabled() && !m_selection.is_empty())
{
auto do_move = [this](const Vec3d& displacement) {
m_selection.start_dragging();
m_selection.translate(displacement);
m_dirty = true;
// wxGetApp().obj_manipul()->set_dirty();
};
auto do_rotate = [this](double angle_z_rad) {
m_selection.start_dragging();
m_selection.rotate(Vec3d(0.0, 0.0, angle_z_rad), TransformationType(TransformationType::World_Relative_Joint));
@ -2967,12 +3069,10 @@ void GLCanvas3D::on_key(wxKeyEvent& evt)
// wxGetApp().obj_manipul()->set_dirty();
};
translationProcessor.process(evt);
switch (keyCode)
{
case WXK_NUMPAD_LEFT: case WXK_LEFT: { do_move(-Vec3d::UnitX()); break; }
case WXK_NUMPAD_RIGHT: case WXK_RIGHT: { do_move(Vec3d::UnitX()); break; }
case WXK_NUMPAD_UP: case WXK_UP: { do_move(Vec3d::UnitY()); break; }
case WXK_NUMPAD_DOWN: case WXK_DOWN: { do_move(-Vec3d::UnitY()); break; }
case WXK_NUMPAD_PAGEUP: case WXK_PAGEUP: { do_rotate(0.25 * M_PI); break; }
case WXK_NUMPAD_PAGEDOWN: case WXK_PAGEDOWN: { do_rotate(-0.25 * M_PI); break; }
default: { break; }

View File

@ -94,6 +94,7 @@ wxDECLARE_EVENT(EVT_GLCANVAS_SELECT_ALL, SimpleEvent);
wxDECLARE_EVENT(EVT_GLCANVAS_QUESTION_MARK, SimpleEvent);
wxDECLARE_EVENT(EVT_GLCANVAS_INCREASE_INSTANCES, Event<int>); // data: +1 => increase, -1 => decrease
wxDECLARE_EVENT(EVT_GLCANVAS_INSTANCE_MOVED, SimpleEvent);
wxDECLARE_EVENT(EVT_GLCANVAS_FORCE_UPDATE, SimpleEvent);
wxDECLARE_EVENT(EVT_GLCANVAS_WIPETOWER_MOVED, Vec3dEvent);
wxDECLARE_EVENT(EVT_GLCANVAS_INSTANCE_ROTATED, SimpleEvent);
wxDECLARE_EVENT(EVT_GLCANVAS_INSTANCE_SCALED, SimpleEvent);

View File

@ -284,28 +284,17 @@ bool GUI_App::on_init_inner()
// to popup a modal dialog on start without screwing combo boxes.
// This is ugly but I honestly found no better way to do it.
// Neither wxShowEvent nor wxWindowCreateEvent work reliably.
static bool once = true;
if (once) {
once = false;
PresetUpdater::UpdateResult updater_result;
try {
updater_result = preset_updater->config_update(app_config->orig_version());
if (updater_result == PresetUpdater::R_INCOMPAT_EXIT) {
mainframe->Close();
} else if (updater_result == PresetUpdater::R_INCOMPAT_CONFIGURED) {
app_conf_exists = true;
}
} catch (const std::exception &ex) {
show_error(nullptr, from_u8(ex.what()));
}
check_updates(false);
CallAfter([this] {
config_wizard_startup();
preset_updater->slic3r_update_notify();
preset_updater->sync(preset_bundle);
});
}
});
@ -810,7 +799,7 @@ void GUI_App::add_config_menu(wxMenuBar *menu)
local_menu->Append(config_id_base + ConfigMenuWizard, config_wizard_name + dots, config_wizard_tooltip);
local_menu->Append(config_id_base + ConfigMenuSnapshots, _(L("&Configuration Snapshots")) + dots, _(L("Inspect / activate configuration snapshots")));
local_menu->Append(config_id_base + ConfigMenuTakeSnapshot, _(L("Take Configuration &Snapshot")), _(L("Capture a configuration snapshot")));
// local_menu->Append(config_id_base + ConfigMenuUpdate, _(L("Check for updates")), _(L("Check for configuration updates")));
local_menu->Append(config_id_base + ConfigMenuUpdate, _(L("Check for updates")), _(L("Check for configuration updates")));
local_menu->AppendSeparator();
local_menu->Append(config_id_base + ConfigMenuPreferences, _(L("&Preferences")) + dots +
#ifdef __APPLE__
@ -841,6 +830,9 @@ void GUI_App::add_config_menu(wxMenuBar *menu)
case ConfigMenuWizard:
run_wizard(ConfigWizard::RR_USER);
break;
case ConfigMenuUpdate:
check_updates(true);
break;
case ConfigMenuTakeSnapshot:
// Take a configuration snapshot.
if (check_unsaved_changes()) {
@ -1230,6 +1222,30 @@ bool GUI_App::config_wizard_startup()
return false;
}
void GUI_App::check_updates(const bool verbose)
{
PresetUpdater::UpdateResult updater_result;
try {
updater_result = preset_updater->config_update(app_config->orig_version());
if (updater_result == PresetUpdater::R_INCOMPAT_EXIT) {
mainframe->Close();
}
else if (updater_result == PresetUpdater::R_INCOMPAT_CONFIGURED) {
app_conf_exists = true;
}
else if(verbose && updater_result == PresetUpdater::R_NOOP)
{
MsgNoUpdates dlg;
dlg.ShowModal();
}
}
catch (const std::exception & ex) {
show_error(nullptr, from_u8(ex.what()));
}
}
// static method accepting a wxWindow object as first parameter
// void warning_catcher{
// my($self, $message_dialog) = @_;

View File

@ -200,6 +200,7 @@ private:
bool select_language();
bool config_wizard_startup();
void check_updates(const bool verbose);
#ifdef __WXMSW__
void associate_3mf_files();

View File

@ -358,12 +358,14 @@ bool CommonGizmosData::update_from_backend(GLCanvas3D& canvas, ModelObject* mode
m_backend_mesh_transformed = po->get_mesh_to_print();
m_backend_mesh_transformed.transform(canvas.sla_print()->sla_trafo(*m_model_object).inverse());
m_mesh = &m_backend_mesh_transformed;
m_has_drilled_mesh = true;
}
}
if (! m_mesh) {
m_mesh = &m_model_object->volumes.front()->mesh();
m_backend_mesh_transformed.clear();
m_has_drilled_mesh = false;
}
m_model_object_id = m_model_object->id();

View File

@ -227,10 +227,13 @@ public:
m_clipping_plane_distance = m_clipping_plane_distance_stash;
}
bool has_drilled_mesh() const { return m_has_drilled_mesh; }
private:
const TriangleMesh* m_old_mesh;
TriangleMesh m_backend_mesh_transformed;
float m_clipping_plane_distance_stash = 0.f;
bool m_has_drilled_mesh = false;
};
} // namespace GUI

View File

@ -330,6 +330,7 @@ bool GLGizmoHollow::unproject_on_mesh(const Vec2d& mouse_pos, std::pair<Vec3f, V
{
if (! m_c->m_mesh_raycaster)
return false;
// if the gizmo doesn't have the V, F structures for igl, calculate them first:
// !!! is it really necessary?
//m_c->update_from_backend(m_parent, m_c->m_model_object);
@ -344,6 +345,19 @@ bool GLGizmoHollow::unproject_on_mesh(const Vec2d& mouse_pos, std::pair<Vec3f, V
Vec3f hit;
Vec3f normal;
if (m_c->m_mesh_raycaster->unproject_on_mesh(mouse_pos, trafo.get_matrix(), camera, hit, normal, m_c->m_clipping_plane.get())) {
// User is about to manipulate a hole. If the gizmo currently shows drilled mesh,
// invalidate slaposDrillHoles so it returns to normal. To do this, hackishly
// add a hole, force SLAPrint::apply call that will invalidate the step because
// of it and then remove the hole again.
if (m_c->has_drilled_mesh()) {
m_c->m_model_object->sla_drain_holes.push_back(sla::DrainHole());
m_selected.push_back(false);
m_parent.post_event(SimpleEvent(EVT_GLCANVAS_FORCE_UPDATE));
wxGetApp().CallAfter([this] { m_c->m_model_object->sla_drain_holes.pop_back(); m_selected.pop_back(); });
return false;
}
// Return both the point and the facet normal.
pos_and_normal = std::make_pair(hit, normal);
return true;
@ -513,6 +527,8 @@ void GLGizmoHollow::delete_selected_points()
}
}
m_parent.post_event(SimpleEvent(EVT_GLCANVAS_FORCE_UPDATE));
select_point(NoPoints);
}
@ -652,6 +668,7 @@ void GLGizmoHollow::on_render_input_window(float x, float y, float bottom_limit)
bool first_run = true; // This is a hack to redraw the button when all points are removed,
// so it is not delayed until the background process finishes.
RENDER_AGAIN:
const float approx_height = m_imgui->scaled(20.0f);
y = std::min(y, bottom_limit - approx_height);
@ -678,6 +695,8 @@ RENDER_AGAIN:
if (m_imgui->button(m_desc["preview"]))
hollow_mesh();
bool config_changed = false;
ImGui::Separator();
{
@ -686,6 +705,7 @@ RENDER_AGAIN:
if (m_imgui->checkbox(m_desc["enable"], m_enable_hollowing)) {
m_c->m_model_object->config.opt<ConfigOptionBool>("hollowing_enable", true)->value = m_enable_hollowing;
wxGetApp().obj_list()->update_and_show_object_settings_item();
config_changed = true;
}
}
@ -761,8 +781,10 @@ RENDER_AGAIN:
m_c->m_model_object->config.opt<ConfigOptionFloat>("hollowing_min_thickness", true)->value = offset;
m_c->m_model_object->config.opt<ConfigOptionFloat>("hollowing_quality", true)->value = quality;
m_c->m_model_object->config.opt<ConfigOptionFloat>("hollowing_closing_distance", true)->value = closing_d;
if (slider_released)
if (slider_released) {
wxGetApp().obj_list()->update_and_show_object_settings_item();
config_changed = true;
}
}
m_imgui->disabled_end();
@ -887,6 +909,9 @@ RENDER_AGAIN:
if (force_refresh)
m_parent.set_as_dirty();
if (config_changed)
m_parent.post_event(SimpleEvent(EVT_GLCANVAS_FORCE_UPDATE));
}
bool GLGizmoHollow::on_is_activable() const

View File

@ -335,7 +335,7 @@ void GLGizmoSlaSupports::render_points(const Selection& selection, bool picking)
}
// Now render the drain holes:
/*if (! m_c->m_cavity_mesh) {
if (! m_c->has_drilled_mesh()) {
render_color[0] = 0.7f;
render_color[1] = 0.7f;
render_color[2] = 0.7f;
@ -370,7 +370,7 @@ void GLGizmoSlaSupports::render_points(const Selection& selection, bool picking)
glFrontFace(GL_CCW);
glsafe(::glPopMatrix());
}
}*/
}
if (!picking)
glsafe(::glDisable(GL_LIGHTING));
@ -414,14 +414,14 @@ bool GLGizmoSlaSupports::unproject_on_mesh(const Vec2d& mouse_pos, std::pair<Vec
// In case the hollowed and drilled mesh is available, we can allow
// placing points in holes, because they should never end up
// on surface that's been drilled away.
/*if (! m_c->m_cavity_mesh) {
if (! m_c->has_drilled_mesh()) {
for (const sla::DrainHole& hole : m_c->m_model_object->sla_drain_holes) {
if (hole.is_inside(hit)) {
in_hole = true;
break;
}
}
}*/
}
if (! in_hole) {
// Return both the point and the facet normal.
pos_and_normal = std::make_pair(hit, normal);
@ -1291,6 +1291,8 @@ bool GLGizmoSlaSupports::unsaved_changes() const
void GLGizmoSlaSupports::update_clipping_plane(bool keep_normal) const
{
if (! m_c->m_model_object)
return;
Vec3d normal = (keep_normal && m_c->m_clipping_plane->get_normal() != Vec3d::Zero() ?
m_c->m_clipping_plane->get_normal() : -m_parent.get_camera().get_dir_forward());

View File

@ -2067,6 +2067,7 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame)
view3D_canvas->Bind(EVT_GLCANVAS_INCREASE_INSTANCES, [this](Event<int> &evt)
{ if (evt.data == 1) this->q->increase_instances(); else if (this->can_decrease_instances()) this->q->decrease_instances(); });
view3D_canvas->Bind(EVT_GLCANVAS_INSTANCE_MOVED, [this](SimpleEvent&) { update(); });
view3D_canvas->Bind(EVT_GLCANVAS_FORCE_UPDATE, [this](SimpleEvent&) { update(); });
view3D_canvas->Bind(EVT_GLCANVAS_WIPETOWER_MOVED, &priv::on_wipetower_moved, this);
view3D_canvas->Bind(EVT_GLCANVAS_WIPETOWER_ROTATED, &priv::on_wipetower_rotated, this);
view3D_canvas->Bind(EVT_GLCANVAS_INSTANCE_ROTATED, [this](SimpleEvent&) { update(); });
@ -5384,6 +5385,13 @@ void Plater::on_config_change(const DynamicPrintConfig &config)
this->p->schedule_background_process();
}
void Plater::set_bed_shape() const
{
p->set_bed_shape(p->config->option<ConfigOptionPoints>("bed_shape")->values,
p->config->option<ConfigOptionString>("bed_custom_texture")->value,
p->config->option<ConfigOptionString>("bed_custom_model")->value);
}
void Plater::force_filament_colors_update()
{
bool update_scheduled = false;

View File

@ -289,6 +289,8 @@ public:
const Mouse3DController& get_mouse3d_controller() const;
Mouse3DController& get_mouse3d_controller();
void set_bed_shape() const;
// ROII wrapper for suppressing the Undo / Redo snapshot to be taken.
class SuppressSnapshots
{

View File

@ -142,6 +142,71 @@ MsgUpdateConfig::MsgUpdateConfig(const std::vector<Update> &updates) :
MsgUpdateConfig::~MsgUpdateConfig() {}
//MsgUpdateForced
MsgUpdateForced::MsgUpdateForced(const std::vector<Update>& updates) :
MsgDialog(nullptr, wxString::Format(_(L("%s incompatibility")), SLIC3R_APP_NAME), _(L("Configuration update is necessary to install")), wxID_NONE)
{
auto* text = new wxStaticText(this, wxID_ANY, wxString::Format(_(L(
"%s will now start updates. Otherwise it won't be able to start.\n\n"
"Note that a full configuration snapshot will be created first. It can then be restored at any time "
"should there be a problem with the new version.\n\n"
"Updated configuration bundles:"
)), SLIC3R_APP_NAME));
logo->SetBitmap(create_scaled_bitmap("PrusaSlicer_192px_grayscale.png", this, 192));
text->Wrap(CONTENT_WIDTH * wxGetApp().em_unit());
content_sizer->Add(text);
content_sizer->AddSpacer(VERT_SPACING);
const auto lang_code = wxGetApp().current_language_code_safe().ToStdString();
auto* versions = new wxBoxSizer(wxVERTICAL);
for (const auto& update : updates) {
auto* flex = new wxFlexGridSizer(2, 0, VERT_SPACING);
auto* text_vendor = new wxStaticText(this, wxID_ANY, update.vendor);
text_vendor->SetFont(boldfont);
flex->Add(text_vendor);
flex->Add(new wxStaticText(this, wxID_ANY, update.version.to_string()));
if (!update.comment.empty()) {
flex->Add(new wxStaticText(this, wxID_ANY, _(L("Comment:"))), 0, wxALIGN_RIGHT);
auto* update_comment = new wxStaticText(this, wxID_ANY, from_u8(update.comment));
update_comment->Wrap(CONTENT_WIDTH * wxGetApp().em_unit());
flex->Add(update_comment);
}
versions->Add(flex);
if (!update.changelog_url.empty() && update.version.prerelease() == nullptr) {
auto* line = new wxBoxSizer(wxHORIZONTAL);
auto changelog_url = (boost::format(update.changelog_url) % lang_code).str();
line->AddSpacer(3 * VERT_SPACING);
line->Add(new wxHyperlinkCtrl(this, wxID_ANY, _(L("Open changelog page")), changelog_url));
versions->Add(line);
}
}
content_sizer->Add(versions);
content_sizer->AddSpacer(2 * VERT_SPACING);
auto* btn_exit = new wxButton(this, wxID_EXIT, wxString::Format(_(L("Exit %s")), SLIC3R_APP_NAME));
btn_sizer->Add(btn_exit);
btn_sizer->AddSpacer(HORIZ_SPACING);
auto* btn_ok = new wxButton(this, wxID_OK);
btn_sizer->Add(btn_ok);
btn_ok->SetFocus();
auto exiter = [this](const wxCommandEvent& evt) { this->EndModal(evt.GetId()); };
btn_exit->Bind(wxEVT_BUTTON, exiter);
btn_ok->Bind(wxEVT_BUTTON, exiter);
Fit();
}
MsgUpdateForced::~MsgUpdateForced() {}
// MsgDataIncompatible
@ -236,5 +301,28 @@ MsgDataLegacy::MsgDataLegacy() :
MsgDataLegacy::~MsgDataLegacy() {}
// MsgNoUpdate
MsgNoUpdates::MsgNoUpdates() :
MsgDialog(nullptr, _(L("Configuration updates")), _(L("No updates aviable")))
{
auto* text = new wxStaticText(this, wxID_ANY, wxString::Format(
_(L(
"%s has no configuration updates aviable."
)),
SLIC3R_APP_NAME, ConfigWizard::name()
));
text->Wrap(CONTENT_WIDTH * wxGetApp().em_unit());
content_sizer->Add(text);
content_sizer->AddSpacer(VERT_SPACING);
logo->SetBitmap(create_scaled_bitmap("PrusaSlicer_192px_grayscale.png", this, 192));
Fit();
}
MsgNoUpdates::~MsgNoUpdates() {}
}
}

View File

@ -62,6 +62,33 @@ public:
~MsgUpdateConfig();
};
// Informs about currently installed bundles not being compatible with the running Slic3r. Asks about action.
class MsgUpdateForced : public MsgDialog
{
public:
struct Update
{
std::string vendor;
Semver version;
std::string comment;
std::string changelog_url;
Update(std::string vendor, Semver version, std::string comment, std::string changelog_url)
: vendor(std::move(vendor))
, version(std::move(version))
, comment(std::move(comment))
, changelog_url(std::move(changelog_url))
{}
};
MsgUpdateForced(const std::vector<Update>& updates);
MsgUpdateForced(MsgUpdateForced&&) = delete;
MsgUpdateForced(const MsgUpdateForced&) = delete;
MsgUpdateForced& operator=(MsgUpdateForced&&) = delete;
MsgUpdateForced& operator=(const MsgUpdateForced&) = delete;
~MsgUpdateForced();
};
// Informs about currently installed bundles not being compatible with the running Slic3r. Asks about action.
class MsgDataIncompatible : public MsgDialog
{
@ -87,6 +114,17 @@ public:
~MsgDataLegacy();
};
// Informs about absence of bundles requiring update.
class MsgNoUpdates : public MsgDialog
{
public:
MsgNoUpdates();
MsgNoUpdates(MsgNoUpdates&&) = delete;
MsgNoUpdates(const MsgNoUpdates&) = delete;
MsgNoUpdates& operator=(MsgNoUpdates&&) = delete;
MsgNoUpdates& operator=(const MsgNoUpdates&) = delete;
~MsgNoUpdates();
};
}
}

View File

@ -73,13 +73,16 @@ struct Update
std::string vendor;
std::string changelog_url;
bool forced_update;
Update() {}
Update(fs::path &&source, fs::path &&target, const Version &version, std::string vendor, std::string changelog_url)
Update(fs::path &&source, fs::path &&target, const Version &version, std::string vendor, std::string changelog_url, bool forced = false)
: source(std::move(source))
, target(std::move(target))
, version(version)
, vendor(std::move(vendor))
, changelog_url(std::move(changelog_url))
, forced_update(forced)
{}
void install() const
@ -297,6 +300,12 @@ void PresetUpdater::priv::sync_config(const VendorMap vendors)
const auto idx_url = vendor.config_update_url + "/" + INDEX_FILENAME;
const std::string idx_path = (cache_path / (vendor.id + ".idx")).string();
const std::string idx_path_temp = idx_path + "-update";
//check if idx_url is leading to our site
if (! boost::starts_with(idx_url, "http://files.prusa3d.com/wp-content/uploads/repository/"))
{
BOOST_LOG_TRIVIAL(warning) << "unsafe url path for vendor \"" << vendor.name << "\" rejected: " << idx_url;
continue;
}
if (!get_file(idx_url, idx_path_temp)) { continue; }
if (cancel) { return; }
@ -418,12 +427,17 @@ Updates PresetUpdater::priv::get_config_updates(const Semver &old_slic3r_version
continue;
}
bool current_not_supported = false; //if slcr is incompatible but situation is not downgrade, we do forced updated and this bool is information to do it
if (ver_current_found && !ver_current->is_current_slic3r_supported()){
if(ver_current->is_current_slic3r_downgrade()) {
// "Reconfigure" situation.
BOOST_LOG_TRIVIAL(warning) << "Current Slic3r incompatible with installed bundle: " << bundle_path.string();
updates.incompats.emplace_back(std::move(bundle_path), *ver_current, vp.name);
continue;
}
current_not_supported = true;
}
if (recommended->config_version < vp.config_version) {
BOOST_LOG_TRIVIAL(warning) << (boost::format("Recommended config version for the currently running PrusaSlicer is older than the currently installed config for vendor %1%. This should not happen.") % idx.vendor()).str();
@ -462,7 +476,7 @@ Updates PresetUpdater::priv::get_config_updates(const Semver &old_slic3r_version
if (new_vp.config_version == recommended->config_version) {
// The config bundle from the cache directory matches the recommended version of the index from the cache directory.
// This is the newest known recommended config. Use it.
new_update = Update(std::move(path_in_cache), std::move(bundle_path), *recommended, vp.name, vp.changelog_url);
new_update = Update(std::move(path_in_cache), std::move(bundle_path), *recommended, vp.name, vp.changelog_url, current_not_supported);
// and install the config index from the cache into vendor's directory.
bundle_path_idx_to_install = idx.path();
found = true;
@ -492,7 +506,7 @@ Updates PresetUpdater::priv::get_config_updates(const Semver &old_slic3r_version
}
recommended = rsrc_idx.recommended();
if (recommended != rsrc_idx.end() && recommended->config_version == rsrc_vp.config_version && recommended->config_version > vp.config_version) {
new_update = Update(std::move(path_in_rsrc), std::move(bundle_path), *recommended, vp.name, vp.changelog_url);
new_update = Update(std::move(path_in_rsrc), std::move(bundle_path), *recommended, vp.name, vp.changelog_url, current_not_supported);
bundle_path_idx_to_install = path_idx_in_rsrc;
found = true;
} else {
@ -513,17 +527,19 @@ Updates PresetUpdater::priv::get_config_updates(const Semver &old_slic3r_version
// Find a recommended config bundle version for the slic3r version last executed. This makes sure that a config bundle update will not be missed
// when upgrading an application. On the other side, the user will be bugged every time he will switch between slic3r versions.
const auto existing_recommended = existing_idx.recommended(old_slic3r_version);
if (existing_recommended != existing_idx.end() && recommended->config_version == existing_recommended->config_version) {
/*if (existing_recommended != existing_idx.end() && recommended->config_version == existing_recommended->config_version) {
// The user has already seen (and presumably rejected) this update
BOOST_LOG_TRIVIAL(info) << boost::format("Downloaded index for `%1%` is the same as installed one, not offering an update.") % idx.vendor();
continue;
}
}*/
} catch (const std::exception &err) {
BOOST_LOG_TRIVIAL(error) << boost::format("Cannot load the installed index at `%1%`: %2%") % bundle_path_idx % err.what();
}
}
// Check if the update is already present in a snapshot
if(!current_not_supported)
{
const auto recommended_snap = SnapshotDB::singleton().snapshot_with_vendor_preset(vp.name, recommended->config_version);
if (recommended_snap != SnapshotDB::singleton().end()) {
BOOST_LOG_TRIVIAL(info) << boost::format("Bundle update %1% %2% already found in snapshot %3%, skipping...")
@ -532,6 +548,7 @@ Updates PresetUpdater::priv::get_config_updates(const Semver &old_slic3r_version
% recommended_snap->id;
continue;
}
}
updates.updates.emplace_back(std::move(new_update));
// 'Install' the index in the vendor directory. This is used to memoize
@ -561,7 +578,10 @@ void PresetUpdater::priv::perform_updates(Updates &&updates, bool snapshot) cons
BOOST_LOG_TRIVIAL(info) << '\t' << incompat;
incompat.remove();
}
} else if (updates.updates.size() > 0) {
if (snapshot) {
BOOST_LOG_TRIVIAL(info) << "Taking a snapshot...";
SnapshotDB::singleton().take_snapshot(*GUI::wxGetApp().app_config, Snapshot::SNAPSHOT_UPGRADE);
@ -688,6 +708,7 @@ PresetUpdater::UpdateResult PresetUpdater::config_update(const Semver &old_slic3
);
} else if (min_slic3r != Semver::zero()) {
restrictions = wxString::Format(_(L("requires min. %s")), min_slic3r.to_string());
BOOST_LOG_TRIVIAL(debug) << "Bundle is not downgrade, user will now have to do whole wizard. This should not happen.";
} else {
restrictions = wxString::Format(_(L("requires max. %s")), max_slic3r.to_string());
}
@ -709,11 +730,54 @@ PresetUpdater::UpdateResult PresetUpdater::config_update(const Semver &old_slic3
}
return R_INCOMPAT_CONFIGURED;
} else {
}
else {
BOOST_LOG_TRIVIAL(info) << "User wants to exit Slic3r, bye...";
return R_INCOMPAT_EXIT;
}
} else if (updates.updates.size() > 0) {
bool incompatible_version = false;
for (const auto& update : updates.updates) {
incompatible_version = (update.forced_update ? true : incompatible_version);
//td::cout << update.forced_update << std::endl;
//BOOST_LOG_TRIVIAL(info) << boost::format("Update requires higher version.");
}
//forced update
if(incompatible_version)
{
BOOST_LOG_TRIVIAL(info) << boost::format("Update of %1% bundles available. At least one requires higher version of Slicer.") % updates.updates.size();
std::vector<GUI::MsgUpdateForced::Update> updates_msg;
for (const auto& update : updates.updates) {
std::string changelog_url = update.version.config_version.prerelease() == nullptr ? update.changelog_url : std::string();
updates_msg.emplace_back(update.vendor, update.version.config_version, update.version.comment, std::move(changelog_url));
}
GUI::MsgUpdateForced dlg(updates_msg);
const auto res = dlg.ShowModal();
if (res == wxID_OK) {
BOOST_LOG_TRIVIAL(info) << "User wants to update...";
p->perform_updates(std::move(updates));
// Reload global configuration
auto* app_config = GUI::wxGetApp().app_config;
GUI::wxGetApp().preset_bundle->load_presets(*app_config);
GUI::wxGetApp().load_current_presets();
GUI::wxGetApp().plater()->set_bed_shape();
return R_UPDATE_INSTALLED;
}
else {
BOOST_LOG_TRIVIAL(info) << "User wants to exit Slic3r, bye...";
return R_INCOMPAT_EXIT;
}
}
// regular update
BOOST_LOG_TRIVIAL(info) << boost::format("Update of %1% bundles available. Asking for confirmation ...") % updates.updates.size();
std::vector<GUI::MsgUpdateConfig::Update> updates_msg;

View File

@ -14,6 +14,14 @@ SCENARIO("Placeholder parser scripting", "[PlaceholderParser]") {
{ "nozzle_diameter", "0.6;0.6;0.6;0.6" },
{ "temperature", "357;359;363;378" }
});
// To test the "first_layer_extrusion_width" over "first_layer_heigth" over "layer_height" chain.
config.option<ConfigOptionFloatOrPercent>("first_layer_height")->value = 150.;
config.option<ConfigOptionFloatOrPercent>("first_layer_height")->percent = true;
// To let the PlaceholderParser throw when referencing first_layer_speed if it is set to percent, as the PlaceholderParser does not know
// a percent to what.
config.option<ConfigOptionFloatOrPercent>("first_layer_speed")->value = 50.;
config.option<ConfigOptionFloatOrPercent>("first_layer_speed")->percent = true;
parser.apply_config(config);
parser.set("foo", 0);
parser.set("bar", 2);
@ -41,6 +49,19 @@ SCENARIO("Placeholder parser scripting", "[PlaceholderParser]") {
SECTION("math: int(13.4)") { REQUIRE(parser.process("{int(13.4)}") == "13"); }
SECTION("math: int(-13.4)") { REQUIRE(parser.process("{int(-13.4)}") == "-13"); }
// Test the "coFloatOrPercent" and "xxx_extrusion_width" substitutions.
// first_layer_extrusion_width ratio_over first_layer_heigth ratio_over layer_height
SECTION("perimeter_extrusion_width") { REQUIRE(std::stod(parser.process("{perimeter_extrusion_width}")) == Approx(0.67500001192092896)); }
SECTION("first_layer_extrusion_width") { REQUIRE(std::stod(parser.process("{first_layer_extrusion_width}")) == Approx(0.9)); }
SECTION("support_material_xy_spacing") { REQUIRE(std::stod(parser.process("{support_material_xy_spacing}")) == Approx(0.3375)); }
// external_perimeter_speed over perimeter_speed
SECTION("external_perimeter_speed") { REQUIRE(std::stod(parser.process("{external_perimeter_speed}")) == Approx(30.)); }
// infill_overlap over perimeter_extrusion_width
SECTION("infill_overlap") { REQUIRE(std::stod(parser.process("{infill_overlap}")) == Approx(0.16875)); }
// If first_layer_speed is set to percent, then it is applied over respective extrusion types by overriding their respective speeds.
// The PlaceholderParser has no way to know which extrusion type the caller has in mind, therefore it throws.
SECTION("first_layer_speed") { REQUIRE_THROWS(parser.process("{first_layer_speed}")); }
// Test the boolean expression parser.
auto boolean_expression = [&parser](const std::string& templ) { return parser.evaluate_boolean_expression(templ, parser.config()); };