mirror of
				https://github.com/SoftFever/OrcaSlicer.git
				synced 2025-10-30 20:21:12 -06:00 
			
		
		
		
	Merge branch 'master' of https://github.com/prusa3d/PrusaSlicer
This commit is contained in:
		
						commit
						c2598cf8d6
					
				
					 11 changed files with 236 additions and 89 deletions
				
			
		|  | @ -591,7 +591,7 @@ bool CLI::setup(int argc, char **argv) | ||||||
|     // Initialize with defaults.
 |     // 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 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) |         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")); |     set_data_dir(m_config.opt_string("datadir")); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -757,6 +757,12 @@ ConfigOption* DynamicConfig::optptr(const t_config_option_key &opt_key, bool cre | ||||||
|     return opt; |     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) | 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;     |     std::vector<const char*> args;     | ||||||
|  |  | ||||||
|  | @ -1494,8 +1494,49 @@ protected: | ||||||
|     ConfigOptionDef*        add_nullable(const t_config_option_key &opt_key, ConfigOptionType type); |     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.
 | // An abstract configuration store.
 | ||||||
| class ConfigBase | class ConfigBase : public ConfigOptionResolver | ||||||
| { | { | ||||||
| public: | public: | ||||||
|     // Definition of configuration values for the purpose of GUI presentation, editing, value mapping and config file handling.
 |     // 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.
 |     // but it carries the defaults of the configuration values.
 | ||||||
|      |      | ||||||
|     ConfigBase() {} |     ConfigBase() {} | ||||||
|     virtual ~ConfigBase() {} |     ~ConfigBase() override {} | ||||||
| 
 | 
 | ||||||
|     // Virtual overridables:
 |     // Virtual overridables:
 | ||||||
| public: | public: | ||||||
|  | @ -1513,6 +1554,7 @@ public: | ||||||
|     virtual ConfigOption*           optptr(const t_config_option_key &opt_key, bool create = false) = 0; |     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.
 |     // Collect names of all configuration values maintained by this configuration store.
 | ||||||
|     virtual t_config_option_keys    keys() const = 0; |     virtual t_config_option_keys    keys() const = 0; | ||||||
|  | 
 | ||||||
| protected: | protected: | ||||||
|     // Verify whether the opt_key has not been obsoleted or renamed.
 |     // Verify whether the opt_key has not been obsoleted or renamed.
 | ||||||
|     // Both opt_key and value may be modified by handle_legacy().
 |     // 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 {} |     virtual void                    handle_legacy(t_config_option_key &/*opt_key*/, std::string &/*value*/) const {} | ||||||
| 
 | 
 | ||||||
| public: | public: | ||||||
|  | 	using ConfigOptionResolver::option; | ||||||
|  | 	using ConfigOptionResolver::option_throw; | ||||||
|  | 
 | ||||||
|     // Non-virtual methods:
 |     // 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) |     ConfigOption* option(const t_config_option_key &opt_key, bool create = false) | ||||||
|         { return this->optptr(opt_key, create); } |         { return this->optptr(opt_key, create); } | ||||||
|      |      | ||||||
|  | @ -1537,10 +1577,6 @@ public: | ||||||
|         return (opt == nullptr || opt->type() != TYPE::static_type()) ? nullptr : static_cast<TYPE*>(opt); |         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* option_throw(const t_config_option_key &opt_key, bool create = false) | ||||||
|     {  |     {  | ||||||
|         ConfigOption *opt = this->optptr(opt_key, create); |         ConfigOption *opt = this->optptr(opt_key, create); | ||||||
|  | @ -1549,9 +1585,6 @@ public: | ||||||
|         return opt; |         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> |     template<typename TYPE> | ||||||
|     TYPE* option_throw(const t_config_option_key &opt_key, bool create = false) |     TYPE* option_throw(const t_config_option_key &opt_key, bool create = false) | ||||||
|     {  |     {  | ||||||
|  | @ -1561,10 +1594,6 @@ public: | ||||||
|         return static_cast<TYPE*>(opt); |         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.
 |     // 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(),
 |     // 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.
 |     // 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)); } |         { return dynamic_cast<T*>(this->option(opt_key, create)); } | ||||||
|     template<class T> const T* opt(const t_config_option_key &opt_key) const |     template<class T> const T* opt(const t_config_option_key &opt_key) const | ||||||
|         { return dynamic_cast<const T*>(this->option(opt_key)); } |         { 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.
 |     // 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; |     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.
 |     // Overrides ConfigBase::keys(). Collect names of all configuration values maintained by this configuration store.
 | ||||||
|  |  | ||||||
|  | @ -1,12 +1,17 @@ | ||||||
| #include "Flow.hpp" | #include "Flow.hpp" | ||||||
|  | #include "I18N.hpp" | ||||||
| #include "Print.hpp" | #include "Print.hpp" | ||||||
| #include <cmath> | #include <cmath> | ||||||
| #include <assert.h> | #include <assert.h> | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
|  | // Mark string for localization and translate.
 | ||||||
|  | #define L(s) Slic3r::I18N::translate(s) | ||||||
|  | 
 | ||||||
| namespace Slic3r { | namespace Slic3r { | ||||||
| 
 | 
 | ||||||
| // This static method returns a sane extrusion width default.
 | // 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) { |     switch (role) { | ||||||
|     case frSupportMaterial: |     case frSupportMaterial: | ||||||
|  | @ -22,6 +27,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
 | // This constructor builds a Flow object from an extrusion width config setting
 | ||||||
| // and other context properties.
 | // and other context properties.
 | ||||||
| Flow Flow::new_from_config_width(FlowRole role, const ConfigOptionFloatOrPercent &width, float nozzle_diameter, float height, float bridge_flow_ratio) | Flow Flow::new_from_config_width(FlowRole role, const ConfigOptionFloatOrPercent &width, float nozzle_diameter, float height, float bridge_flow_ratio) | ||||||
|  | @ -39,7 +130,7 @@ Flow Flow::new_from_config_width(FlowRole role, const ConfigOptionFloatOrPercent | ||||||
|             sqrt(bridge_flow_ratio) * nozzle_diameter; |             sqrt(bridge_flow_ratio) * nozzle_diameter; | ||||||
|     } else if (! width.percent && width.value == 0.) { |     } else if (! width.percent && width.value == 0.) { | ||||||
|         // If user left option to 0, calculate a sane default width.
 |         // 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 { |     } else { | ||||||
|         // If user set a manual value, use it.
 |         // If user set a manual value, use it.
 | ||||||
|         w = float(width.get_abs_value(height)); |         w = float(width.get_abs_value(height)); | ||||||
|  |  | ||||||
|  | @ -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
 |     // 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.
 |     // to fit a region with integer number of lines.
 | ||||||
|     static Flow new_from_spacing(float spacing, float nozzle_diameter, float height, bool bridge); |     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); | extern Flow support_material_flow(const PrintObject *object, float layer_height = 0.f); | ||||||
|  |  | ||||||
|  | @ -1,4 +1,5 @@ | ||||||
| #include "PlaceholderParser.hpp" | #include "PlaceholderParser.hpp" | ||||||
|  | #include "Flow.hpp" | ||||||
| #include <cstring> | #include <cstring> | ||||||
| #include <ctime> | #include <ctime> | ||||||
| #include <iomanip> | #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_old = config_old.option(opt_key); | ||||||
| 	const ConfigOption *opt_new = config_new.option(opt_key); | 	const ConfigOption *opt_new = config_new.option(opt_key); | ||||||
| 	assert(opt_new != nullptr); | 	assert(opt_new != nullptr); | ||||||
| 	if (opt_old == nullptr) |     return opt_old != nullptr && *opt_new == *opt_old; | ||||||
|         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; |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| std::vector<std::string> PlaceholderParser::config_diff(const DynamicPrintConfig &rhs) | std::vector<std::string> PlaceholderParser::config_diff(const DynamicPrintConfig &rhs) | ||||||
|  | @ -126,14 +123,7 @@ bool PlaceholderParser::apply_config(const DynamicPrintConfig &rhs) | ||||||
|     bool modified = false; |     bool modified = false; | ||||||
|     for (const t_config_option_key &opt_key : rhs.keys()) { |     for (const t_config_option_key &opt_key : rhs.keys()) { | ||||||
|         if (! opts_equal(m_config, rhs, opt_key)) { |         if (! opts_equal(m_config, rhs, opt_key)) { | ||||||
|             // Store a copy of the config option.
 | 			this->set(opt_key, rhs.option(opt_key)->clone()); | ||||||
|             // 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()); |  | ||||||
|             modified = true; |             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) | void PlaceholderParser::apply_only(const DynamicPrintConfig &rhs, const std::vector<std::string> &keys) | ||||||
| { | { | ||||||
|     for (const t_config_option_key &opt_key : keys) { |     for (const t_config_option_key &opt_key : keys) | ||||||
|         // Store a copy of the config option.
 |         this->set(opt_key, rhs.option(opt_key)->clone()); | ||||||
|         // 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()); |  | ||||||
|     } |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| void PlaceholderParser::apply_config(DynamicPrintConfig &&rhs) | void PlaceholderParser::apply_config(DynamicPrintConfig &&rhs) | ||||||
|  | @ -635,7 +617,7 @@ namespace client | ||||||
|         return os; |         return os; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     struct MyContext { |     struct MyContext : public ConfigOptionResolver { | ||||||
|     	const DynamicConfig     *external_config        = nullptr; |     	const DynamicConfig     *external_config        = nullptr; | ||||||
|         const DynamicConfig     *config                 = nullptr; |         const DynamicConfig     *config                 = nullptr; | ||||||
|         const DynamicConfig     *config_override        = 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; } |         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; |             const ConfigOption *opt = nullptr; | ||||||
|             if (config_override != nullptr) |             if (config_override != nullptr) | ||||||
|  | @ -662,6 +644,8 @@ namespace client | ||||||
|             return opt; |             return opt; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |         const ConfigOption*     resolve_symbol(const std::string &opt_key) const { return this->optptr(opt_key); } | ||||||
|  | 
 | ||||||
|         template <typename Iterator> |         template <typename Iterator> | ||||||
|         static void legacy_variable_expansion( |         static void legacy_variable_expansion( | ||||||
|             const MyContext                 *ctx,  |             const MyContext                 *ctx,  | ||||||
|  | @ -758,7 +742,43 @@ namespace client | ||||||
|             case coPoint:   output.set_s(opt.opt->serialize());  break; |             case coPoint:   output.set_s(opt.opt->serialize());  break; | ||||||
|             case coBool:    output.set_b(opt.opt->getBool());    break; |             case coBool:    output.set_b(opt.opt->getBool());    break; | ||||||
|             case coFloatOrPercent: |             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: |             default: | ||||||
|                 ctx->throw_exception("Unknown scalar variable type", opt.it_range); |                 ctx->throw_exception("Unknown scalar variable type", opt.it_range); | ||||||
|             } |             } | ||||||
|  |  | ||||||
|  | @ -24,8 +24,7 @@ | ||||||
| #include <boost/format.hpp> | #include <boost/format.hpp> | ||||||
| #include <boost/log/trivial.hpp> | #include <boost/log/trivial.hpp> | ||||||
| 
 | 
 | ||||||
| //! macro used to mark string used at localization,
 | // Mark string for localization and translate.
 | ||||||
| //! return same string
 |  | ||||||
| #define L(s) Slic3r::I18N::translate(s) | #define L(s) Slic3r::I18N::translate(s) | ||||||
| 
 | 
 | ||||||
| namespace Slic3r { | namespace Slic3r { | ||||||
|  | @ -527,7 +526,6 @@ void Print::config_diffs( | ||||||
| 	const DynamicPrintConfig &new_full_config,  | 	const DynamicPrintConfig &new_full_config,  | ||||||
| 	t_config_option_keys &print_diff, t_config_option_keys &object_diff, t_config_option_keys ®ion_diff,  | 	t_config_option_keys &print_diff, t_config_option_keys &object_diff, t_config_option_keys ®ion_diff,  | ||||||
| 	t_config_option_keys &full_config_diff,  | 	t_config_option_keys &full_config_diff,  | ||||||
| 	DynamicPrintConfig &placeholder_parser_overrides, |  | ||||||
| 	DynamicPrintConfig &filament_overrides) const | 	DynamicPrintConfig &filament_overrides) const | ||||||
| { | { | ||||||
|     // Collect changes to print config, account for overrides of extruder retract values by filament presets.
 |     // 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); |     object_diff = m_default_object_config.diff(new_full_config); | ||||||
|     region_diff = m_default_region_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.
 |     // 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()) { |     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_old = m_full_print_config.option(opt_key); | ||||||
|         const ConfigOption *opt_new = new_full_config.option(opt_key); |         const ConfigOption *opt_new = new_full_config.option(opt_key); | ||||||
|         if (opt_old == nullptr || *opt_new != *opt_old) |         if (opt_old == nullptr || *opt_new != *opt_old) | ||||||
|             full_config_diff.emplace_back(opt_key); |             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.
 |     // 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; | 	t_config_option_keys print_diff, object_diff, region_diff, full_config_diff; | ||||||
| 	DynamicPrintConfig placeholder_parser_overrides, filament_overrides; | 	DynamicPrintConfig filament_overrides; | ||||||
| 	this->config_diffs(new_full_config, print_diff, object_diff, region_diff, full_config_diff, placeholder_parser_overrides, 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.
 |     // Do not use the ApplyStatus as we will use the max function when updating apply_status.
 | ||||||
|     unsigned int apply_status = APPLY_STATUS_UNCHANGED; |     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.
 |     // which should be stopped if print_diff is not empty.
 | ||||||
|     size_t num_extruders = m_config.nozzle_diameter.size(); |     size_t num_extruders = m_config.nozzle_diameter.size(); | ||||||
|     bool   num_extruders_changed = false; |     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)); |         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()
 |         // 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("print_preset",    new_full_config.option("print_settings_id")->clone()); | ||||||
| 		m_placeholder_parser.set("filament_preset", new_full_config.option("filament_settings_id")->clone()); | 		m_placeholder_parser.set("filament_preset", new_full_config.option("filament_settings_id")->clone()); | ||||||
|  |  | ||||||
|  | @ -435,7 +435,6 @@ private: | ||||||
| 		const DynamicPrintConfig &new_full_config,  | 		const DynamicPrintConfig &new_full_config,  | ||||||
| 		t_config_option_keys &print_diff, t_config_option_keys &object_diff, t_config_option_keys ®ion_diff,  | 		t_config_option_keys &print_diff, t_config_option_keys &object_diff, t_config_option_keys ®ion_diff,  | ||||||
| 		t_config_option_keys &full_config_diff,  | 		t_config_option_keys &full_config_diff,  | ||||||
| 		DynamicPrintConfig &placeholder_parser_overrides, |  | ||||||
| 		DynamicPrintConfig &filament_overrides) const; | 		DynamicPrintConfig &filament_overrides) const; | ||||||
| 
 | 
 | ||||||
|     bool                invalidate_state_by_config_options(const std::vector<t_config_option_key> &opt_keys); |     bool                invalidate_state_by_config_options(const std::vector<t_config_option_key> &opt_keys); | ||||||
|  |  | ||||||
|  | @ -718,8 +718,9 @@ void PrintConfigDef::init_fff_params() | ||||||
|     def->gui_type = "f_enum_open"; |     def->gui_type = "f_enum_open"; | ||||||
|     def->gui_flags = "show_value"; |     def->gui_flags = "show_value"; | ||||||
|     def->enum_values.push_back("PLA"); |     def->enum_values.push_back("PLA"); | ||||||
|     def->enum_values.push_back("ABS"); |  | ||||||
|     def->enum_values.push_back("PET"); |     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("FLEX"); | ||||||
|     def->enum_values.push_back("HIPS"); |     def->enum_values.push_back("HIPS"); | ||||||
|     def->enum_values.push_back("EDGE"); |     def->enum_values.push_back("EDGE"); | ||||||
|  |  | ||||||
|  | @ -46,12 +46,6 @@ enum SeamPosition { | ||||||
|     spRandom, spNearest, spAligned, spRear |     spRandom, spNearest, spAligned, spRear | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| /*
 |  | ||||||
| enum FilamentType { |  | ||||||
|     ftPLA, ftABS, ftPET, ftHIPS, ftFLEX, ftSCAFF, ftEDGE, ftNGEN, ftPVA |  | ||||||
| }; |  | ||||||
| */ |  | ||||||
| 
 |  | ||||||
| enum SLAMaterial { | enum SLAMaterial { | ||||||
|     slamTough, |     slamTough, | ||||||
|     slamFlex, |     slamFlex, | ||||||
|  | @ -149,24 +143,6 @@ template<> inline const t_config_enum_values& ConfigOptionEnum<SeamPosition>::ge | ||||||
|     return keys_map; |     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() { | template<> inline const t_config_enum_values& ConfigOptionEnum<SLADisplayOrientation>::get_enum_values() { | ||||||
|     static const t_config_enum_values keys_map = { |     static const t_config_enum_values keys_map = { | ||||||
|         { "landscape", sladoLandscape}, |         { "landscape", sladoLandscape}, | ||||||
|  | @ -354,6 +330,9 @@ protected: | ||||||
| #define STATIC_PRINT_CONFIG_CACHE_BASE(CLASS_NAME) \ | #define STATIC_PRINT_CONFIG_CACHE_BASE(CLASS_NAME) \ | ||||||
| public: \ | public: \ | ||||||
|     /* Overrides ConfigBase::optptr(). Find ando/or create a ConfigOption instance for a given name. */ \ |     /* 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 \ |     ConfigOption*            optptr(const t_config_option_key &opt_key, bool create = false) override \ | ||||||
|         { return s_cache_##CLASS_NAME.optptr(opt_key, this); } \ |         { return s_cache_##CLASS_NAME.optptr(opt_key, this); } \ | ||||||
|     /* Overrides ConfigBase::keys(). Collect names of all configuration values maintained by this configuration store. */ \ |     /* Overrides ConfigBase::keys(). Collect names of all configuration values maintained by this configuration store. */ \ | ||||||
|  |  | ||||||
|  | @ -14,7 +14,15 @@ SCENARIO("Placeholder parser scripting", "[PlaceholderParser]") { | ||||||
| 	    { "nozzle_diameter", "0.6;0.6;0.6;0.6" }, | 	    { "nozzle_diameter", "0.6;0.6;0.6;0.6" }, | ||||||
| 	    { "temperature", "357;359;363;378" } | 	    { "temperature", "357;359;363;378" } | ||||||
| 	}); | 	}); | ||||||
| 	parser.apply_config(config); |     // 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("foo", 0); | ||||||
| 	parser.set("bar", 2); | 	parser.set("bar", 2); | ||||||
| 	parser.set("num_extruders", 4); | 	parser.set("num_extruders", 4); | ||||||
|  | @ -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"); } | ||||||
|     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.
 |     // Test the boolean expression parser.
 | ||||||
|     auto boolean_expression = [&parser](const std::string& templ) { return parser.evaluate_boolean_expression(templ, parser.config()); }; |     auto boolean_expression = [&parser](const std::string& templ) { return parser.evaluate_boolean_expression(templ, parser.config()); }; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 David Kocik
						David Kocik