create little hierarchy of config to better use ratio_over

fixes on ratio_over
fix Flow::extrusion_width (bad computation of first_layer_height)
fix enum visibility
moving test classes to prusaslicer test directory (wip)
all that because i was trying to write a test class for a modification in min_object_distance (and i didn't even start)
This commit is contained in:
supermerill 2020-05-31 03:54:58 +02:00
parent 9426fb4360
commit 1afaa6ef49
38 changed files with 4089 additions and 86 deletions

View File

@ -425,8 +425,8 @@ int CLI::run(int argc, char **argv)
fff_print.auto_assign_extruders(mo);
}
print->apply(model, m_print_config);
std::pair<PrintError, std::string> err = print->validate();
if (! err.first == PrintError::None) {
std::pair<PrintValidationError, std::string> err = print->validate();
if (! err.first == PrintValidationError::None) {
boost::nowide::cerr << err.second << std::endl;
return 1;
}

View File

@ -579,6 +579,12 @@ double ConfigBase::get_abs_value(const t_config_option_key &opt_key) const
}
if (raw_opt->type() == coFloat)
return static_cast<const ConfigOptionFloat*>(raw_opt)->value;
if (raw_opt->type() == coInt)
return static_cast<const ConfigOptionInt*>(raw_opt)->value;
if (raw_opt->type() == coBool)
return static_cast<const ConfigOptionBool*>(raw_opt)->value?1:0;
const ConfigOptionDef* opt_def = nullptr;
const ConfigOptionPercent* cast_opt = nullptr;
if (raw_opt->type() == coFloatOrPercent) {
if(!static_cast<const ConfigOptionFloatOrPercent*>(raw_opt)->percent)
return static_cast<const ConfigOptionFloatOrPercent*>(raw_opt)->value;
@ -586,13 +592,37 @@ double ConfigBase::get_abs_value(const t_config_option_key &opt_key) const
const ConfigDef *def = this->def();
if (def == nullptr)
throw NoDefinitionException(opt_key);
const ConfigOptionDef *opt_def = def->get(opt_key);
opt_def = def->get(opt_key);
cast_opt = static_cast<const ConfigOptionFloatOrPercent*>(raw_opt);
assert(opt_def != nullptr);
}
if (raw_opt->type() == coPercent) {
// Get option definition.
const ConfigDef* def = this->def();
if (def == nullptr)
throw NoDefinitionException(opt_key);
opt_def = def->get(opt_key);
assert(opt_def != nullptr);
cast_opt = static_cast<const ConfigOptionPercent*>(raw_opt);
}
if (opt_def != nullptr) {
//if over no other key, it's most probably a simple %
if (opt_def->ratio_over == "")
return cast_opt->get_abs_value(1);
if (opt_def->ratio_over == "nozzle_diameter") {
//use the first... i guess.
//TODO: find a better way, like a "current_extruder_idx" config option.
if (this->option(opt_def->ratio_over) == nullptr) {
std::stringstream ss; ss << "ConfigBase::get_abs_value(): " << opt_key << " need nozzle_diameter but can't acess it. Please use get_abs_value(nozzle_diam).";
throw std::runtime_error(ss.str());
}
return static_cast<const ConfigOptionFloats*>(this->option(opt_def->ratio_over))->values[0];
}
// Compute absolute value over the absolute value of the base option.
//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.
return opt_def->ratio_over.empty() ? 0. :
static_cast<const ConfigOptionFloatOrPercent*>(raw_opt)->get_abs_value(this->get_abs_value(opt_def->ratio_over));
return opt_def->ratio_over.empty() ? 0. :
cast_opt->get_abs_value(this->get_abs_value(opt_def->ratio_over));
}
std::stringstream ss; ss << "ConfigBase::get_abs_value(): "<< opt_key<<" has not a valid option type for get_abs_value()";
throw std::runtime_error(ss.str());
@ -823,7 +853,14 @@ ConfigOption* DynamicConfig::optptr(const t_config_option_key &opt_key, bool cre
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();
if (it == options.end()) {
//if not find, try with the parent config.
if (parent != nullptr)
return parent->option(opt_key);
else
return nullptr;
}else
return it->second.get();
}
void DynamicConfig::read_cli(const std::vector<std::string> &tokens, t_config_option_keys* extra, t_config_option_keys* keys)

View File

@ -1625,11 +1625,16 @@ public:
ConfigBase() {}
~ConfigBase() override {}
// to get to the config more generic than this one, if available
const ConfigBase* parent = nullptr;
// Virtual overridables:
public:
// Static configuration definition. Any value stored into this ConfigBase shall have its definition here.
// will search in parent definition if not found here.
virtual const ConfigDef* def() const = 0;
// Find ando/or create a ConfigOption instance for a given name.
// won't search in parent definition, as you can't change a parent value
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;

View File

@ -92,15 +92,11 @@ double Flow::extrusion_width(const std::string& opt_key, const ConfigOptionFloat
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);
// first_layer_height depends on first_printing_extruder
auto opt_nozzle_diameters = config.option<ConfigOptionFloats>("nozzle_diameter");
if (opt_nozzle_diameters == nullptr)
throw_on_missing_variable(opt_key, "nozzle_diameter");
return opt->get_abs_value(float(opt_nozzle_diameters->get_at(first_printing_extruder)));
}
if (opt->value == 0.) {

View File

@ -745,7 +745,7 @@ namespace client
{
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.
// Extrusion width use the first nozzle diameter
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.
@ -760,7 +760,7 @@ namespace client
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.
// Extrusion width supports defaults and a dependency over nozzle diameter
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;

View File

@ -1273,19 +1273,19 @@ static inline bool sequential_print_vertical_clearance_valid(const Print &print)
}
// Precondition: Print::validate() requires the Print::apply() to be called its invocation.
std::pair<PrintError, std::string> Print::validate() const
std::pair<PrintBase::PrintValidationError, std::string> Print::validate() const
{
if (m_objects.empty())
return { PrintError::WrongPosition, L("All objects are outside of the print volume.") };
return { PrintValidationError::WrongPosition, L("All objects are outside of the print volume.") };
if (extruders().empty())
return { PrintError::NoPrint, L("The supplied settings will cause an empty print.") };
return { PrintValidationError::NoPrint, L("The supplied settings will cause an empty print.") };
if (m_config.complete_objects) {
if (! sequential_print_horizontal_clearance_valid(*this))
return { PrintError::WrongPosition, L("Some objects are too close; your extruder will collide with them.") };
return { PrintValidationError::WrongPosition, L("Some objects are too close; your extruder will collide with them.") };
if (! sequential_print_vertical_clearance_valid(*this))
return { PrintError::WrongPosition,L("Some objects are too tall and cannot be printed without extruder collisions.") };
return { PrintValidationError::WrongPosition,L("Some objects are too tall and cannot be printed without extruder collisions.") };
}
if (m_config.spiral_vase) {
@ -1294,14 +1294,14 @@ std::pair<PrintError, std::string> Print::validate() const
total_copies_count += object->instances().size();
// #4043
if (total_copies_count > 1 && ! m_config.complete_objects.value)
return { PrintError::WrongSettings,L("The Spiral Vase option can only be used when printing a single object.") };
return { PrintValidationError::WrongSettings,L("The Spiral Vase option can only be used when printing a single object.") };
assert(m_objects.size() == 1 || config().complete_objects.value);
size_t num_regions = 0;
for (const std::vector<std::pair<t_layer_height_range, int>> &volumes_per_region : m_objects.front()->region_volumes)
if (! volumes_per_region.empty())
++ num_regions;
if (num_regions > 1)
return { PrintError::WrongSettings,L("The Spiral Vase option can only be used when printing single material objects.") };
return { PrintValidationError::WrongSettings,L("The Spiral Vase option can only be used when printing single material objects.") };
}
if (this->has_wipe_tower() && ! m_objects.empty()) {
@ -1314,21 +1314,21 @@ std::pair<PrintError, std::string> Print::validate() const
double filament_diam = m_config.filament_diameter.get_at(extruder_idx);
if (nozzle_diam - EPSILON > first_nozzle_diam || nozzle_diam + EPSILON < first_nozzle_diam
|| std::abs((filament_diam-first_filament_diam)/first_filament_diam) > 0.1)
return { PrintError::WrongSettings,L("The wipe tower is only supported if all extruders have the same nozzle diameter "
return { PrintValidationError::WrongSettings,L("The wipe tower is only supported if all extruders have the same nozzle diameter "
"and use filaments of the same diameter.") };
}
if (m_config.gcode_flavor != gcfRepRap && m_config.gcode_flavor != gcfRepetier && m_config.gcode_flavor != gcfMarlin
&& m_config.gcode_flavor != gcfKlipper)
return { PrintError::WrongSettings,L("The Wipe Tower is currently only supported for the Marlin, RepRap/Sprinter and Repetier G-code flavors.") };
return { PrintValidationError::WrongSettings,L("The Wipe Tower is currently only supported for the Marlin, RepRap/Sprinter and Repetier G-code flavors.") };
if (! m_config.use_relative_e_distances)
return { PrintError::WrongSettings,L("The Wipe Tower is currently only supported with the relative extruder addressing (use_relative_e_distances=1).") };
return { PrintValidationError::WrongSettings,L("The Wipe Tower is currently only supported with the relative extruder addressing (use_relative_e_distances=1).") };
if (m_config.ooze_prevention)
return { PrintError::WrongSettings,L("Ooze prevention is currently not supported with the wipe tower enabled.") };
return { PrintValidationError::WrongSettings,L("Ooze prevention is currently not supported with the wipe tower enabled.") };
if (m_config.use_volumetric_e)
return { PrintError::WrongSettings,L("The Wipe Tower currently does not support volumetric E (use_volumetric_e=0).") };
return { PrintValidationError::WrongSettings,L("The Wipe Tower currently does not support volumetric E (use_volumetric_e=0).") };
if (m_config.complete_objects && extruders().size() > 1)
return { PrintError::WrongSettings,L("The Wipe Tower is currently not supported for multimaterial sequential prints.") };
return { PrintValidationError::WrongSettings,L("The Wipe Tower is currently not supported for multimaterial sequential prints.") };
if (m_objects.size() > 1) {
bool has_custom_layering = false;
@ -1349,15 +1349,15 @@ std::pair<PrintError, std::string> Print::validate() const
const SlicingParameters &slicing_params = object->slicing_parameters();
if (std::abs(slicing_params.first_print_layer_height - slicing_params0.first_print_layer_height) > EPSILON ||
std::abs(slicing_params.layer_height - slicing_params0.layer_height ) > EPSILON)
return { PrintError::WrongSettings,L("The Wipe Tower is only supported for multiple objects if they have equal layer heights") };
return { PrintValidationError::WrongSettings,L("The Wipe Tower is only supported for multiple objects if they have equal layer heights") };
if (slicing_params.raft_layers() != slicing_params0.raft_layers())
return { PrintError::WrongSettings,L("The Wipe Tower is only supported for multiple objects if they are printed over an equal number of raft layers") };
return { PrintValidationError::WrongSettings,L("The Wipe Tower is only supported for multiple objects if they are printed over an equal number of raft layers") };
if (object->config().support_material_contact_distance_type != m_objects.front()->config().support_material_contact_distance_type
|| object->config().support_material_contact_distance_top != m_objects.front()->config().support_material_contact_distance_top
|| object->config().support_material_contact_distance_bottom != m_objects.front()->config().support_material_contact_distance_bottom)
return { PrintError::WrongSettings,L("The Wipe Tower is only supported for multiple objects if they are printed with the same support_material_contact_distance") };
return { PrintValidationError::WrongSettings,L("The Wipe Tower is only supported for multiple objects if they are printed with the same support_material_contact_distance") };
if (! equal_layering(slicing_params, slicing_params0))
return { PrintError::WrongSettings,L("The Wipe Tower is only supported for multiple objects if they are sliced equally.") };
return { PrintValidationError::WrongSettings,L("The Wipe Tower is only supported for multiple objects if they are sliced equally.") };
if (has_custom_layering) {
PrintObject::update_layer_height_profile(*object->model_object(), slicing_params, layer_height_profiles[i]);
if (*(layer_height_profiles[i].end()-2) > *(layer_height_profiles[tallest_object_idx].end()-2))
@ -1399,7 +1399,7 @@ std::pair<PrintError, std::string> Print::validate() const
} while (ref_z == next_ref_z);
}
if (std::abs(this_height - ref_height) > EPSILON)
return { PrintError::WrongSettings,L("The Wipe tower is only supported if all objects have the same variable layer height") };
return { PrintValidationError::WrongSettings,L("The Wipe tower is only supported if all objects have the same variable layer height") };
i += 2;
}
}
@ -1448,7 +1448,7 @@ std::pair<PrintError, std::string> Print::validate() const
// The object has some form of support and either support_material_extruder or support_material_interface_extruder
// will be printed with the current tool without a forced tool change. Play safe, assert that all object nozzles
// are of the same diameter.
return { PrintError::WrongSettings,L("Printing with multiple extruders of differing nozzle diameters. "
return { PrintValidationError::WrongSettings,L("Printing with multiple extruders of differing nozzle diameters. "
"If support is to be printed with the current extruder (support_material_extruder == 0 or support_material_interface_extruder == 0), "
"all nozzles have to be of the same diameter.") };
}
@ -1456,18 +1456,18 @@ std::pair<PrintError, std::string> Print::validate() const
if (object->config().support_material_contact_distance_type == zdNone) {
// Soluble interface
if (! object->config().support_material_synchronize_layers)
return { PrintError::WrongSettings,L("For the Wipe Tower to work with the soluble supports, the support layers need to be synchronized with the object layers.") };
return { PrintValidationError::WrongSettings,L("For the Wipe Tower to work with the soluble supports, the support layers need to be synchronized with the object layers.") };
} else {
// Non-soluble interface
if (object->config().support_material_extruder != 0 || object->config().support_material_interface_extruder != 0)
return { PrintError::WrongSettings,L("The Wipe Tower currently supports the non-soluble supports only if they are printed with the current extruder without triggering a tool change. "
return { PrintValidationError::WrongSettings,L("The Wipe Tower currently supports the non-soluble supports only if they are printed with the current extruder without triggering a tool change. "
"(both support_material_extruder and support_material_interface_extruder need to be set to 0).") };
}
}
}
// validate first_layer_height
double first_layer_height = object->config().get_abs_value("first_layer_height");
double first_layer_height = object->config().get_abs_value("first_layer_height", this->m_config.nozzle_diameter.get_at(0));
double first_layer_min_nozzle_diameter;
if (object->config().raft_layers > 0) {
// if we have raft layers, only support material extruder is used on first layer
@ -1482,27 +1482,27 @@ std::pair<PrintError, std::string> Print::validate() const
first_layer_min_nozzle_diameter = min_nozzle_diameter;
}
if (first_layer_height > first_layer_min_nozzle_diameter)
return { PrintError::WrongSettings,L("First layer height can't be greater than nozzle diameter") };
return { PrintValidationError::WrongSettings,L("First layer height can't be greater than nozzle diameter") };
// validate layer_height
double layer_height = object->config().layer_height.value;
if (layer_height > min_nozzle_diameter)
return { PrintError::WrongSettings,L("Layer height can't be greater than nozzle diameter") };
return { PrintValidationError::WrongSettings,L("Layer height can't be greater than nozzle diameter") };
// Validate extrusion widths.
std::string err_msg;
if (! validate_extrusion_width(object->config(), "extrusion_width", layer_height, err_msg))
return { PrintError::WrongSettings,err_msg };
return { PrintValidationError::WrongSettings,err_msg };
if ((object->config().support_material || object->config().raft_layers > 0) && ! validate_extrusion_width(object->config(), "support_material_extrusion_width", layer_height, err_msg))
return { PrintError::WrongSettings,err_msg };
return { PrintValidationError::WrongSettings,err_msg };
for (const char *opt_key : { "perimeter_extrusion_width", "external_perimeter_extrusion_width", "infill_extrusion_width", "solid_infill_extrusion_width", "top_infill_extrusion_width" })
for (size_t i = 0; i < object->region_volumes.size(); ++ i)
if (! object->region_volumes[i].empty() && ! validate_extrusion_width(this->get_region(i)->config(), opt_key, layer_height, err_msg))
return { PrintError::WrongSettings, err_msg };
return { PrintValidationError::WrongSettings, err_msg };
}
}
return { PrintError::None, std::string() };
return { PrintValidationError::None, std::string() };
}
#if 0

View File

@ -84,7 +84,7 @@ private:
PrintRegionConfig m_config;
//PrintRegion(Print* print) : m_refcnt(0), m_print(print) {}
PrintRegion(Print* print, const PrintRegionConfig &config) : m_refcnt(0), m_print(print), m_config(config) {}
PrintRegion(Print* print, const PrintRegionConfig& config) : m_refcnt(0), m_print(print), m_config(config) {}
~PrintRegion() = default;
};
@ -364,7 +364,12 @@ private: // Prevents erroneous use by other classes.
typedef PrintBaseWithState<PrintStep, psCount> Inherited;
public:
Print() = default;
//Print() = default;
Print() {
//create config hierachy
m_default_object_config.parent = &m_config;
m_default_region_config.parent = &m_default_object_config;
};
virtual ~Print() { this->clear(); }
PrinterTechnology technology() const noexcept override { return ptFFF; }
@ -399,7 +404,7 @@ public:
bool has_skirt() const;
// Returns an empty string if valid, otherwise returns an error message.
std::pair<PrintError, std::string> validate() const override;
std::pair<PrintValidationError, std::string> validate() const override;
double skirt_first_layer_height() const;
Flow brim_flow(size_t extruder_id) const;
Flow skirt_flow(size_t extruder_id) const;

View File

@ -203,12 +203,6 @@ protected:
ModelObject *m_model_object;
};
enum PrintError {
None,
WrongPosition,
NoPrint,
WrongSettings,
};
/**
* @brief Printing involves slicing and export of device dependent instructions.
@ -235,8 +229,15 @@ public:
// or after apply() over a model, where no object is printable (all outside the print volume).
virtual bool empty() const = 0;
enum PrintValidationError {
None,
WrongPosition,
NoPrint,
WrongSettings,
};
// Validate the print, return empty string if valid, return error if process() cannot (or should not) be started.
virtual std::pair<PrintError, std::string> validate() const { return { PrintError::None, std::string() }; }
virtual std::pair<PrintValidationError, std::string> validate() const { return { PrintValidationError::None, std::string() }; }
enum ApplyStatus {
// No change after the Print::apply() call.

View File

@ -677,6 +677,7 @@ void PrintConfigDef::init_fff_params()
def->category = OptionCategory::infill;
def->tooltip = L("This parameter grows the top/bottom/solid layers by the specified MM to anchor them into the part. Put 0 to deactivate it. Can be a % of the width of the perimeters.");
def->sidetext = L("mm");
def->ratio_over = "perimeter_extrusion_width";
def->min = 0;
def->mode = comExpert;
def->set_default_value(new ConfigOptionFloatOrPercent(150, true));
@ -687,6 +688,7 @@ void PrintConfigDef::init_fff_params()
def->category = OptionCategory::infill;
def->tooltip = L("This parameter grows the bridged solid infill layers by the specified MM to anchor them into the part. Put 0 to deactivate it. Can be a % of the width of the external perimeter.");
def->sidetext = L("mm");
def->ratio_over = "external_perimeter_extrusion_width";
def->min = 0;
def->mode = comExpert;
def->set_default_value(new ConfigOptionFloatOrPercent(200, true));
@ -699,6 +701,7 @@ void PrintConfigDef::init_fff_params()
"If left zero, default extrusion width will be used if set, otherwise 1.05 x nozzle diameter will be used. "
"If expressed as percentage (for example 112.5%), it will be computed over nozzle diameter.");
def->sidetext = L("mm or %");
def->ratio_over = "nozzle_diameter";
def->min = 0;
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionFloatOrPercent(0, false));
@ -709,7 +712,7 @@ void PrintConfigDef::init_fff_params()
def->category = OptionCategory::width;
def->tooltip = L("Activate this option to modify the flow to acknoledge that the nozzle is round and the corners will have a round shape, and so change the flow to realized that and avoid over-extrusion."
" 100% is activated, 0% is deactivated and 50% is half-activated."
"\nNote: this change the flow by ~5% over a very small distance (~nozzle diameter), so it shouldn't be noticeable unless you have a very big nozzle and a very precise printer."
"\nNote: At 100% this change the flow by ~5% over a very small distance (~nozzle diameter), so it shouldn't be noticeable unless you have a very big nozzle and a very precise printer."
"\nIt's very experimental, please report about the usefulness. It may be removed if there is no use of it.");
def->sidetext = L("%");
def->mode = comExpert;
@ -925,10 +928,11 @@ void PrintConfigDef::init_fff_params()
def->category = OptionCategory::width;
def->tooltip = L("This factor changes the amount of flow proportionally. You may need to tweak "
"this setting to get nice surface finish and correct single wall widths. "
"Usual values are between 0.9 and 1.1. If you think you need to change this more, "
"Usual values are between 90% and 110%. If you think you need to change this more, "
"check filament diameter and your firmware E steps."
" This print setting is multiplied against the extrusion_multiplier from the filament tab."
" Its only purpose is to offer the same functionality but with a per-object basis.");
def->sidetext = L("%");
def->mode = comSimple;
def->min = 2;
def->set_default_value(new ConfigOptionPercent(100));
@ -941,6 +945,7 @@ void PrintConfigDef::init_fff_params()
"(see the tooltips for perimeter extrusion width, infill extrusion width etc). "
"If expressed as percentage (for example: 105%), it will be computed over nozzle diameter.");
def->sidetext = L("mm or %");
def->ratio_over = "nozzle_diameter";
def->min = 0;
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionFloatOrPercent(0, false));
@ -1380,10 +1385,11 @@ void PrintConfigDef::init_fff_params()
def->label = L("width");
def->full_label = L("Ironing width");
def->category = OptionCategory::infill;
def->tooltip = L("This is the width of the ironing pass, in a % of the top extrusion width, should not be more than 50%"
def->tooltip = L("This is the width of the ironing pass, in a % of the top infill extrusion width, should not be more than 50%"
" (two times more lines, 50% overlap). It's not necessary to go below 25% (four times more lines, 75% overlap). \nIf you have problems with your ironing process,"
" don't forget to look at the flow->above bridge flow, as this setting should be set to min 110% to let you have enough plastic in the top layer."
" A value too low will make your extruder eat the filament.");
def->ratio_over = "top_infill_extrusion_width";
def->min = 0;
def->mode = comExpert;
def->sidetext = L("% or mm");
@ -1434,7 +1440,7 @@ void PrintConfigDef::init_fff_params()
"of the nozzle used for the type of extrusion. "
"If set to zero, it will use the default extrusion width.");
def->sidetext = L("mm or %");
def->ratio_over = "first_layer_height";
def->ratio_over = "nozzle_diameter";
def->min = 0;
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionFloatOrPercent(140, true));
@ -1447,7 +1453,7 @@ void PrintConfigDef::init_fff_params()
"This can be expressed as an absolute value or as a percentage (for example: 75%) "
"over the default nozzle width.");
def->sidetext = L("mm or %");
def->ratio_over = "layer_height";
def->ratio_over = "nozzle_diameter";
def->min = 0;
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionFloatOrPercent(75, true));
@ -1461,6 +1467,7 @@ void PrintConfigDef::init_fff_params()
"speed if it's lower than that. If expressed as a percentage "
"(for example: 40%) it will scale the 'default' speeds.");
def->sidetext = L("mm/s or %");
def->ratio_over = "depends";
def->min = 0;
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionFloatOrPercent(30, false));
@ -1474,6 +1481,7 @@ void PrintConfigDef::init_fff_params()
"speed if it's lower than that. If expressed as a percentage "
"(for example: 40%) it will scale the 'default' speed.");
def->sidetext = L("mm/s or %");
def->ratio_over = "depends";
def->min = 0;
def->mode = comExpert;
def->set_default_value(new ConfigOptionFloatOrPercent(30, false));
@ -1503,6 +1511,7 @@ void PrintConfigDef::init_fff_params()
def->full_label = ("Min gap-fill surface");
def->category = OptionCategory::perimeter;
def->tooltip = L("This setting represents the minimum mm² for a gapfill extrusion to be created.\nCan be a % of (perimeter width)²");
def->ratio_over = "perimeter_width_square";
def->min = 0;
def->mode = comExpert;
def->set_default_value(new ConfigOptionFloatOrPercent{ 100,true });
@ -1651,6 +1660,7 @@ void PrintConfigDef::init_fff_params()
"You may want to use fatter extrudates to speed up the infill and make your parts stronger. "
"If expressed as percentage (for example 110%) it will be computed over nozzle diameter.");
def->sidetext = L("mm or %");
def->ratio_over = "nozzle_diameter";
def->min = 0;
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionFloatOrPercent(0, false));
@ -2100,6 +2110,7 @@ void PrintConfigDef::init_fff_params()
def->full_label = L("Overhang bridge threshold");
def->category = OptionCategory::perimeter;
def->tooltip = L("Minimum unsupported width for an extrusion to be considered an overhang. Can be in mm or in a % of the nozzle diameter.");
def->ratio_over = "nozzle_diameter";
def->mode = comExpert;
def->set_default_value(new ConfigOptionFloatOrPercent(50, true));
@ -2117,6 +2128,7 @@ void PrintConfigDef::init_fff_params()
def->full_label = L("Overhang reversal threshold");
def->category = OptionCategory::perimeter;
def->tooltip = L("Number of mm the overhang need to be for the reversal to be considered useful. Can be a % of the periemter width.");
def->ratio_over = "perimeter_extrusion_width";
def->min = 0;
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionFloatOrPercent(250, true));
@ -2643,6 +2655,7 @@ void PrintConfigDef::init_fff_params()
"If left zero, default extrusion width will be used if set, otherwise 1.125 x nozzle diameter will be used. "
"If expressed as percentage (for example 110%) it will be computed over nozzle diameter.");
def->sidetext = L("mm or %");
def->ratio_over = "nozzle_diameter";
def->min = 0;
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionFloatOrPercent(0, false));
@ -2652,7 +2665,7 @@ void PrintConfigDef::init_fff_params()
def->full_label = ("Solid infill speed");
def->category = OptionCategory::speed;
def->tooltip = L("Speed for printing solid regions (top/bottom/internal horizontal shells). "
"This can be expressed as a percentage (for example: 80%) over the default "
"This can be expressed as a percentage (for example: 80%) over the default infill speed "
"infill speed above. Set to zero for auto.");
def->sidetext = L("mm/s or %");
def->ratio_over = "infill_speed";
@ -2834,6 +2847,7 @@ void PrintConfigDef::init_fff_params()
"(when the object is printed on top of the support). "
"Setting this to 0 will also prevent Slic3r from using bridge flow and speed "
"for the first object layer. Can be a % of the extruding width used for the interface layers.");
def->ratio_over = "top_infill_extrusion_width";
def->sidetext = L("mm");
def->enum_labels.push_back((boost::format("0.2 (%1%)") % L("detachable")).str());
def->min = 0;
@ -2851,6 +2865,7 @@ void PrintConfigDef::init_fff_params()
def->category = OptionCategory::support;
def->tooltip = L("The vertical distance between object and support material interface"
"(when the support is printed on top of the object). Can be a % of the extruding width used for the interface layers.");
def->ratio_over = "top_infill_extrusion_width";
def->sidetext = L("mm");
def->min = 0;
def->mode = comAdvanced;
@ -2886,6 +2901,7 @@ void PrintConfigDef::init_fff_params()
"If left zero, default extrusion width will be used if set, otherwise nozzle diameter will be used. "
"If expressed as percentage (for example 110%) it will be computed over nozzle diameter.");
def->sidetext = L("mm or %");
def->ratio_over = "nozzle_diameter";
def->min = 0;
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionFloatOrPercent(0, false));
@ -3058,6 +3074,7 @@ void PrintConfigDef::init_fff_params()
def->tooltip = L("Minimum width for the extrusion to be extruded (widths lower than the nozzle diameter will be over-extruded at the nozzle diameter)."
" If expressed as percentage (for example 110%) it will be computed over nozzle diameter."
" The default behavior of slic3r and slic3rPE is with a 33% value. Put 100% to avoid any sort of over-extrusion.");
def->ratio_over = "nozzle_diameter";
def->mode = comExpert;
def->min = 0;
def->set_default_value(new ConfigOptionFloatOrPercent(33, true));
@ -3067,6 +3084,7 @@ void PrintConfigDef::init_fff_params()
def->full_label = L("Thin wall overlap");
def->category = OptionCategory::perimeter;
def->tooltip = L("Overlap between the thin wall and the perimeters. Can be a % of the external perimeter width (default 50%)");
def->ratio_over = "external_periemter_extrusion_width";
def->mode = comExpert;
def->min = 0;
def->set_default_value(new ConfigOptionFloatOrPercent(50, true));
@ -3123,6 +3141,7 @@ void PrintConfigDef::init_fff_params()
"If left zero, default extrusion width will be used if set, otherwise nozzle diameter will be used. "
"If expressed as percentage (for example 110%) it will be computed over nozzle diameter.");
def->sidetext = L("mm or %");
def->ratio_over = "nozzle_diameter";
def->min = 0;
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionFloatOrPercent(0, false));
@ -3296,6 +3315,7 @@ void PrintConfigDef::init_fff_params()
def = this->add("wipe_tower_brim", coFloatOrPercent);
def->label = L("Wipe tower brim width");
def->tooltip = L("Width of the brim for the wipe tower. Can be in mm of in % of the (assumed) only one nozzle diameter.");
def->ratio_over = "nozzle_diameter";
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionFloatOrPercent(150,true));
@ -3478,6 +3498,7 @@ void PrintConfigDef::init_extruder_option_keys()
void PrintConfigDef::init_milling_params()
{
// ConfigOptionFloats, ConfigOptionPercents, ConfigOptionBools, ConfigOptionStrings
m_milling_option_keys = {
"milling_diameter",
"milling_toolchange_end_gcode",
@ -3578,6 +3599,7 @@ void PrintConfigDef::init_milling_params()
def->tooltip = L("This increase the size of the object by a certain amount to have enough plastic to mill."
" You can set a number of mm or a percentage of the calculated optimal extra width (from flow calculation).");
def->sidetext = L("mm or %");
def->ratio_over = "computed_on_the_fly";
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionFloatOrPercent(150, true));
@ -3586,6 +3608,7 @@ void PrintConfigDef::init_milling_params()
def->category = OptionCategory::milling;
def->tooltip = L("THis setting restrict the post-process milling to a certain height, to avoid milling the bed. It can be a mm of a % of the first layer height (so it can depends of the object).");
def->sidetext = L("mm or %");
def->ratio_over = "first_layer_height";
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionFloatOrPercent(200, true));
@ -4419,20 +4442,28 @@ double PrintConfig::min_object_distance(const ConfigBase *config)
if (extruder_clearance_radius > base_dist) {
base_dist = extruder_clearance_radius;
}
//std::cout << "min_object_distance! extruder_clearance_radius=" << extruder_clearance_radius << "\n";
//add brim width
//FIXME: does not take into account object-defined brim !!! you can crash yoursefl with it
if (config->option("brim_width")->getFloat() > 0) {
base_dist += config->option("brim_width")->getFloat();
}
//std::cout << "min_object_distance! adding brim=" << config->option("brim_width")->getFloat()<< " => "<< base_dist << "\n";
//add the skirt
if (config->option("skirts")->getInt() > 0 && !config->option("complete_objects_one_skirt")->getBool()) {
//add skirt dist
double dist_skirt = config->option("skirt_distance")->getFloat();
dist_skirt += max_nozzle_diam * config->option("skirts")->getInt() * 1.5;
//std::cout << "min_object_distance! adding dist_skirt=" << dist_skirt << " ? " << (dist_skirt > config->option("brim_width")->getFloat())
// << " ?x2 " << (dist_skirt * 2 > config->option("brim_width")->getFloat() && config->option("skirt_height")->getInt() > 3);
//add skirt width if needed
if (dist_skirt > config->option("brim_width")->getFloat())
base_dist += (dist_skirt - config->option("brim_width")->getFloat());
//consider skrit as part of the object if it's tall enough to be considered as a ooze shield.
else if (dist_skirt * 2 > config->option("brim_width")->getFloat() && config->option("skirt_height")->getInt() > 3)
base_dist += (dist_skirt * 2 - config->option("brim_width")->getFloat());
}
//std::cout << " => " << base_dist << "\n";
}
return base_dist;
}

View File

@ -434,7 +434,12 @@ protected:
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); } \
{ const ConfigOption* opt = s_cache_##CLASS_NAME.optptr(opt_key, this); \
if (opt == nullptr && parent != nullptr) \
/*if not find, try with the parent config.*/ \
opt = parent->option(opt_key); \
return opt; \
} \
/* 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); } \

View File

@ -68,6 +68,9 @@ PrintObject::PrintObject(Print* print, ModelObject* model_object, const Transfor
m_size = (bbox.size() * (1. / SCALING_FACTOR)).cast<coord_t>();
this->set_instances(std::move(instances));
//create config hierarchy
m_config.parent = &print->config();
}
PrintBase::ApplyStatus PrintObject::set_instances(PrintInstances &&instances)

View File

@ -602,7 +602,7 @@ std::string SLAPrint::output_filename(const std::string &filename_base) const
return this->PrintBase::output_filename(m_print_config.output_filename_format.value, ".sl1", filename_base, &config);
}
std::pair<PrintError, std::string>SLAPrint::validate() const
std::pair<PrintBase::PrintValidationError, std::string> SLAPrint::validate() const
{
for(SLAPrintObject * po : m_objects) {
@ -612,7 +612,7 @@ std::pair<PrintError, std::string>SLAPrint::validate() const
if(supports_en &&
mo->sla_points_status == sla::PointsStatus::UserModified &&
mo->sla_support_points.empty())
return { PrintError::WrongSettings, L("Cannot proceed without support points! "
return { PrintValidationError::WrongSettings, L("Cannot proceed without support points! "
"Add support points or disable support generation.") };
sla::SupportConfig cfg = make_support_cfg(po->config());
@ -623,13 +623,13 @@ std::pair<PrintError, std::string>SLAPrint::validate() const
sla::PadConfig::EmbedObject &builtinpad = padcfg.embed_object;
if(supports_en && !builtinpad.enabled && elv < cfg.head_fullwidth())
return { PrintError::WrongSettings, L(
return { PrintValidationError::WrongSettings, L(
"Elevation is too low for object. Use the \"Pad around "
"object\" feature to print the object without elevation.") };
if(supports_en && builtinpad.enabled &&
cfg.pillar_base_safety_distance_mm < builtinpad.object_gap_mm) {
return { PrintError::WrongSettings, L(
return { PrintValidationError::WrongSettings, L(
"The endings of the support pillars will be deployed on the "
"gap between the object and the pad. 'Support base safety "
"distance' has to be greater than the 'Pad object gap' "
@ -637,7 +637,7 @@ std::pair<PrintError, std::string>SLAPrint::validate() const
}
std::string pval = padcfg.validate();
if (!pval.empty()) return { PrintError::WrongSettings, pval };
if (!pval.empty()) return { PrintValidationError::WrongSettings, pval };
}
double expt_max = m_printer_config.max_exposure_time.getFloat();
@ -645,16 +645,16 @@ std::pair<PrintError, std::string>SLAPrint::validate() const
double expt_cur = m_material_config.exposure_time.getFloat();
if (expt_cur < expt_min || expt_cur > expt_max)
return { PrintError::WrongSettings, L("Exposition time is out of printer profile bounds.") };
return { PrintValidationError::WrongSettings, L("Exposition time is out of printer profile bounds.") };
double iexpt_max = m_printer_config.max_initial_exposure_time.getFloat();
double iexpt_min = m_printer_config.min_initial_exposure_time.getFloat();
double iexpt_cur = m_material_config.initial_exposure_time.getFloat();
if (iexpt_cur < iexpt_min || iexpt_cur > iexpt_max)
return { PrintError::WrongSettings, L("Initial exposition time is out of printer profile bounds.") };
return { PrintValidationError::WrongSettings, L("Initial exposition time is out of printer profile bounds.") };
return { PrintError::None, "" };
return { PrintValidationError::None, "" };
}
bool SLAPrint::invalidate_step(SLAPrintStep step)

View File

@ -432,7 +432,7 @@ public:
const SLAPrintStatistics& print_statistics() const { return m_print_statistics; }
std::pair<PrintError, std::string> validate() const override;
std::pair<PrintValidationError, std::string> validate() const override;
// An aggregation of SliceRecord-s from all the print objects for each
// occupied layer. Slice record levels dont have to match exactly.

View File

@ -373,7 +373,7 @@ bool BackgroundSlicingProcess::empty() const
return m_print->empty();
}
std::pair<PrintError, std::string> BackgroundSlicingProcess::validate()
std::pair<PrintValidationError, std::string> BackgroundSlicingProcess::validate()
{
assert(m_print != nullptr);
return m_print->validate();

View File

@ -92,9 +92,9 @@ public:
void set_task(const PrintBase::TaskParams &params);
// After calling apply, the empty() call will report whether there is anything to slice.
bool empty() const;
// Validate the print. Returns a {PrintError::None,empty string} if valid, returns an error message if invalid.
// Validate the print. Returns a {PrintValidationError::None,empty string} if valid, returns an error message if invalid.
// Call validate before calling start().
std::pair<PrintError, std::string> validate();
std::pair<PrintValidationError, std::string> validate();
// Set the export path of the G-code.
// Once the path is set, the G-code

View File

@ -2060,11 +2060,12 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame)
: q(q)
, main_frame(main_frame)
, config(Slic3r::DynamicPrintConfig::new_from_defaults_keys({
// These keys are used by (at least) printconfig::min_object_distance
"bed_shape", "bed_custom_texture", "bed_custom_model",
"complete_objects",
"complete_objects_sort",
"complete_objects_one_skirt",
"duplicate_distance", "extruder_clearance_radius", "skirts", "skirt_distance",
"duplicate_distance", "extruder_clearance_radius", "skirts", "skirt_distance", "skirt_height",
"brim_width", "variable_layer_height", "serial_port", "serial_speed", "host_type", "print_host",
"printhost_apikey", "printhost_cafile", "nozzle_diameter", "single_extruder_multi_material",
"wipe_tower", "wipe_tower_x", "wipe_tower_y", "wipe_tower_width", "wipe_tower_rotation_angle", "wipe_tower_brim",
@ -3165,9 +3166,9 @@ unsigned int Plater::priv::update_background_process(bool force_validation, bool
// The delayed error message is no more valid.
this->delayed_error_message.clear();
// The state of the Print changed, and it is non-zero. Let's validate it and give the user feedback on errors.
std::pair<PrintError, std::string> err = this->background_process.validate();
std::pair<PrintValidationError, std::string> err = this->background_process.validate();
this->get_current_canvas3D()->show_print_warning("");
if (err.first == PrintError::None) {
if (err.first == PrintValidationError::None) {
if (invalidated != Print::APPLY_STATUS_UNCHANGED && this->background_processing_enabled())
return_state |= UPDATE_BACKGROUND_PROCESS_RESTART;
} else {
@ -3177,7 +3178,7 @@ unsigned int Plater::priv::update_background_process(bool force_validation, bool
while (p->GetParent())
p = p->GetParent();
auto *top_level_wnd = dynamic_cast<wxTopLevelWindow*>(p);
if ( (err.first == PrintError::WrongPosition || err.first == PrintError::NoPrint) && top_level_wnd && top_level_wnd->IsActive()) {
if ( (err.first == PrintValidationError::WrongPosition || err.first == PrintValidationError::NoPrint) && top_level_wnd && top_level_wnd->IsActive()) {
this->get_current_canvas3D()->show_print_warning(err.second);
} else if (!postpone_error_messages && top_level_wnd && top_level_wnd->IsActive()) {
// The error returned from the Print needs to be translated into the local language.

View File

@ -293,7 +293,7 @@ void init_print(Print& print, std::initializer_list<TestMesh> meshes, Slic3r::Mo
}
print.apply(model, config);
std::pair<PrintError, std::string> err = print.validate();
std::pair<PrintValidationError, std::string> err = print.validate();
//std::cout << "validate result : " << err << ", mempty print? " << print.empty() << "\n";
}

View File

@ -29,6 +29,7 @@ set_property(GLOBAL PROPERTY USE_FOLDERS ON)
add_subdirectory(libnest2d)
add_subdirectory(libslic3r)
add_subdirectory(superslicerlibslic3r)
add_subdirectory(slic3rutils)
add_subdirectory(fff_print)
add_subdirectory(sla_print)

View File

@ -175,7 +175,7 @@ SCENARIO("Config accessor functions perform as expected.", "[Config]") {
WHEN("getX called on an unset option.") {
THEN("The default is returned.") {
REQUIRE(config.opt_float("layer_height") == 0.3);
REQUIRE(config.opt_float("layer_height") == 0.2);
REQUIRE(config.opt_int("raft_layers") == 0);
REQUIRE(config.opt_bool("support_material") == false);
}

View File

@ -309,10 +309,11 @@ SCENARIO("Circle Fit, TaubinFit with Newton's method", "[Geometry]") {
SCENARIO("Path chaining", "[Geometry]") {
GIVEN("A path") {
std::vector<Point> points = { Point(26,26),Point(52,26),Point(0,26),Point(26,52),Point(26,0),Point(0,52),Point(52,52),Point(52,0) };
THEN("Chained with no diagonals (thus 26 units long)") {
THEN("Chained with no diagonals (thus 26 units long)") { //this will fail as i deactivated the pusa traveller salesman code.
std::vector<Points::size_type> indices = chain_points(points);
for (Points::size_type i = 0; i + 1 < indices.size(); ++ i) {
double dist = (points.at(indices.at(i)).cast<double>() - points.at(indices.at(i+1)).cast<double>()).norm();
std::stringstream log;
REQUIRE(std::abs(dist-26) <= EPSILON);
}
}

View File

@ -9,19 +9,26 @@ SCENARIO("Placeholder parser scripting", "[PlaceholderParser]") {
PlaceholderParser parser;
auto config = DynamicPrintConfig::full_print_config();
// To test the "first_layer_extrusion_width" over "nozzle_diameter" chain.
config.set_deserialize( {
{ "printer_notes", " PRINTER_VENDOR_PRUSA3D PRINTER_MODEL_MK2 " },
{ "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;
config.option<ConfigOptionFloatOrPercent>("first_layer_extrusion_width")->value = 150.;
config.option<ConfigOptionFloatOrPercent>("first_layer_extrusion_width")->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;
config.option<ConfigOptionFloatOrPercent>("first_layer_extrusion_width")->value = 150.;
config.option<ConfigOptionFloatOrPercent>("first_layer_extrusion_width")->percent = true;
config.option<ConfigOptionFloatOrPercent>("support_material_xy_spacing")->value = 50.;
config.option<ConfigOptionFloatOrPercent>("support_material_xy_spacing")->percent = true;
parser.apply_config(config);
parser.set("foo", 0);
parser.set("bar", 2);
@ -50,10 +57,12 @@ SCENARIO("Placeholder parser scripting", "[PlaceholderParser]") {
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)); }
// first_layer_extrusion_width ratio_over nozzle_diameter, 150% of 0.6 is 0.9
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)); }
// support_material_xy_spacing is ratio over external_perimeter_extrusion_width
// external_perimeter_extrusion_width is at 0 by default, and so will be 1.05f * nozzle = 0.63 do 50% of that is 0.315
SECTION("support_material_xy_spacing") { REQUIRE(std::stod(parser.process("{support_material_xy_spacing}")) == Approx(0.315)); }
// 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

View File

@ -0,0 +1,30 @@
get_filename_component(_TEST_NAME ${CMAKE_CURRENT_LIST_DIR} NAME)
add_executable(${_TEST_NAME}_tests
${_TEST_NAME}_tests.cpp
# test_complete_objects.cpp
# test_fill.cpp
# test_flow.cpp
# test_gcodewriter.cpp
# test_geometry.cpp
# test_model.cpp
test_print.cpp
# test_thin.cpp
# test_denserinfill.cpp
# test_extrusion_entity.cpp
# test_skirt_brim.cpp
test_data.hpp
test_data.cpp
# test_clipper_utils.cpp
)
target_link_libraries(${_TEST_NAME}_tests test_common libslic3r)
set_property(TARGET ${_TEST_NAME}_tests PROPERTY FOLDER "tests")
if (WIN32)
prusaslicer_copy_dlls(${_TEST_NAME}_tests)
endif()
# catch_discover_tests(${_TEST_NAME}_tests TEST_PREFIX "${_TEST_NAME}: ")
add_test(${_TEST_NAME}_tests ${_TEST_NAME}_tests ${CATCH_EXTRA_ARGS})

View File

@ -0,0 +1,41 @@
#include "libslic3r/Utils.hpp"
namespace {
//
//TEST_CASE("sort_remove_duplicates", "[utils]") {
// std::vector<int> data_src = { 3, 0, 2, 1, 15, 3, 5, 6, 3, 1, 0 };
// std::vector<int> data_dst = { 0, 1, 2, 3, 5, 6, 15 };
// Slic3r::sort_remove_duplicates(data_src);
// REQUIRE(data_src == data_dst);
//}
//
//TEST_CASE("string_printf", "[utils]") {
// SECTION("Empty format with empty data should return empty string") {
// std::string outs = Slic3r::string_printf("");
// REQUIRE(outs.empty());
// }
//
// SECTION("String output length should be the same as input") {
// std::string outs = Slic3r::string_printf("1234");
// REQUIRE(outs.size() == 4);
// }
//
// SECTION("String format should be interpreted as with sprintf") {
// std::string outs = Slic3r::string_printf("%d %f %s", 10, 11.4, " This is a string");
// char buffer[1024];
//
// sprintf(buffer, "%d %f %s", 10, 11.4, " This is a string");
//
// REQUIRE(outs.compare(buffer) == 0);
// }
//
// SECTION("String format should survive large input data") {
// std::string input(2048, 'A');
// std::string outs = Slic3r::string_printf("%s", input.c_str());
// REQUIRE(outs.compare(input) == 0);
// }
//}
}

View File

@ -0,0 +1,301 @@
#include <catch2/catch.hpp>
#include <numeric>
#include <iostream>
#include <boost/filesystem.hpp>
#include "libslic3r/ClipperUtils.hpp"
#include "libslic3r/ExPolygon.hpp"
#include "libslic3r/SVG.hpp"
using namespace Slic3r;
SCENARIO("Various Clipper operations - xs/t/11_clipper.t", "[ClipperUtils]") {
// CCW oriented contour
Slic3r::Polygon square{ { 200, 100 }, {200, 200}, {100, 200}, {100, 100} };
// CW oriented contour
Slic3r::Polygon hole_in_square{ { 160, 140 }, { 140, 140 }, { 140, 160 }, { 160, 160 } };
Slic3r::ExPolygon square_with_hole(square, hole_in_square);
GIVEN("square_with_hole") {
WHEN("offset") {
Polygons result = Slic3r::offset(square_with_hole, 5.f);
THEN("offset matches") {
REQUIRE(result == Polygons {
{ { 205, 205 }, { 95, 205 }, { 95, 95 }, { 205, 95 }, },
{ { 145, 145 }, { 145, 155 }, { 155, 155 }, { 155, 145 } } });
}
}
WHEN("offset_ex") {
ExPolygons result = Slic3r::offset_ex(square_with_hole, 5.f);
THEN("offset matches") {
REQUIRE(result == ExPolygons { {
{ { 205, 205 }, { 95, 205 }, { 95, 95 }, { 205, 95 }, },
{ { 145, 145 }, { 145, 155 }, { 155, 155 }, { 155, 145 } } } } );
}
}
WHEN("offset2_ex") {
ExPolygons result = Slic3r::offset2_ex(square_with_hole, 5.f, -2.f);
THEN("offset matches") {
REQUIRE(result == ExPolygons { {
{ { 203, 203 }, { 97, 203 }, { 97, 97 }, { 203, 97 } },
{ { 143, 143 }, { 143, 157 }, { 157, 157 }, { 157, 143 } } } } );
}
}
}
GIVEN("square_with_hole 2") {
Slic3r::ExPolygon square_with_hole(
{ { 20000000, 20000000 }, { 0, 20000000 }, { 0, 0 }, { 20000000, 0 } },
{ { 5000000, 15000000 }, { 15000000, 15000000 }, { 15000000, 5000000 }, { 5000000, 5000000 } });
WHEN("offset2_ex") {
Slic3r::ExPolygons result = Slic3r::offset2_ex(ExPolygons { square_with_hole }, -1.f, 1.f);
THEN("offset matches") {
REQUIRE(result.size() == 1);
REQUIRE(square_with_hole.area() == result.front().area());
}
}
}
GIVEN("square and hole") {
WHEN("diff_ex") {
ExPolygons result = Slic3r::diff_ex({ square }, { hole_in_square });
THEN("hole is created") {
REQUIRE(result.size() == 1);
REQUIRE(square_with_hole.area() == result.front().area());
}
}
}
GIVEN("polyline") {
Polyline polyline { { 50, 150 }, { 300, 150 } };
WHEN("intersection_pl") {
Polylines result = Slic3r::intersection_pl({ polyline }, { square, hole_in_square });
THEN("correct number of result lines") {
REQUIRE(result.size() == 2);
}
THEN("result lines have correct length") {
// results are in no particular order
REQUIRE(result[0].length() == 40);
REQUIRE(result[1].length() == 40);
}
}
WHEN("diff_pl") {
Polylines result = Slic3r::diff_pl({ polyline }, { square, hole_in_square });
THEN("correct number of result lines") {
REQUIRE(result.size() == 3);
}
// results are in no particular order
THEN("the left result line has correct length") {
REQUIRE(std::count_if(result.begin(), result.end(), [](const Polyline &pl) { return pl.length() == 50; }) == 1);
}
THEN("the right result line has correct length") {
REQUIRE(std::count_if(result.begin(), result.end(), [](const Polyline &pl) { return pl.length() == 100; }) == 1);
}
THEN("the central result line has correct length") {
REQUIRE(std::count_if(result.begin(), result.end(), [](const Polyline &pl) { return pl.length() == 20; }) == 1);
}
}
}
GIVEN("Clipper bug #96 / Slic3r issue #2028") {
Slic3r::Polyline subject{
{ 44735000, 31936670 }, { 55270000, 31936670 }, { 55270000, 25270000 }, { 74730000, 25270000 }, { 74730000, 44730000 }, { 68063296, 44730000 }, { 68063296, 55270000 }, { 74730000, 55270000 },
{ 74730000, 74730000 }, { 55270000, 74730000 }, { 55270000, 68063296 }, { 44730000, 68063296 }, { 44730000, 74730000 }, { 25270000, 74730000 }, { 25270000, 55270000 }, { 31936670, 55270000 },
{ 31936670, 44730000 }, { 25270000, 44730000 }, { 25270000, 25270000 }, { 44730000, 25270000 }, { 44730000, 31936670 } };
Slic3r::Polygon clip { {75200000, 45200000}, {54800000, 45200000}, {54800000, 24800000}, {75200000, 24800000} };
Slic3r::Polylines result = Slic3r::intersection_pl({ subject }, { clip });
THEN("intersection_pl - result is not empty") {
REQUIRE(result.size() == 1);
}
}
GIVEN("Clipper bug #122") {
Slic3r::Polyline subject { { 1975, 1975 }, { 25, 1975 }, { 25, 25 }, { 1975, 25 }, { 1975, 1975 } };
Slic3r::Polygons clip { { { 2025, 2025 }, { -25, 2025 } , { -25, -25 }, { 2025, -25 } },
{ { 525, 525 }, { 525, 1475 }, { 1475, 1475 }, { 1475, 525 } } };
Slic3r::Polylines result = Slic3r::intersection_pl({ subject }, clip);
THEN("intersection_pl - result is not empty") {
REQUIRE(result.size() == 1);
REQUIRE(result.front().points.size() == 5);
}
}
GIVEN("Clipper bug #126") {
Slic3r::Polyline subject { { 200000, 19799999 }, { 200000, 200000 }, { 24304692, 200000 }, { 15102879, 17506106 }, { 13883200, 19799999 }, { 200000, 19799999 } };
Slic3r::Polygon clip { { 15257205, 18493894 }, { 14350057, 20200000 }, { -200000, 20200000 }, { -200000, -200000 }, { 25196917, -200000 } };
Slic3r::Polylines result = Slic3r::intersection_pl({ subject }, { clip });
THEN("intersection_pl - result is not empty") {
REQUIRE(result.size() == 1);
}
THEN("intersection_pl - result has same length as subject polyline") {
REQUIRE(result.front().length() == Approx(subject.length()));
}
}
#if 0
{
# Clipper does not preserve polyline orientation
my $polyline = Slic3r::Polyline->new([50, 150], [300, 150]);
my $result = Slic3r::Geometry::Clipper::intersection_pl([$polyline], [$square]);
is scalar(@$result), 1, 'intersection_pl - correct number of result lines';
is_deeply $result->[0]->pp, [[100, 150], [200, 150]], 'clipped line orientation is preserved';
}
{
# Clipper does not preserve polyline orientation
my $polyline = Slic3r::Polyline->new([300, 150], [50, 150]);
my $result = Slic3r::Geometry::Clipper::intersection_pl([$polyline], [$square]);
is scalar(@$result), 1, 'intersection_pl - correct number of result lines';
is_deeply $result->[0]->pp, [[200, 150], [100, 150]], 'clipped line orientation is preserved';
}
{
# Disabled until Clipper bug #127 is fixed
my $subject = [
Slic3r::Polyline->new([-90000000, -100000000], [-90000000, 100000000]), # vertical
Slic3r::Polyline->new([-100000000, -10000000], [100000000, -10000000]), # horizontal
Slic3r::Polyline->new([-100000000, 0], [100000000, 0]), # horizontal
Slic3r::Polyline->new([-100000000, 10000000], [100000000, 10000000]), # horizontal
];
my $clip = Slic3r::Polygon->new(# a circular, convex, polygon
[99452190, 10452846], [97814760, 20791169], [95105652, 30901699], [91354546, 40673664], [86602540, 50000000],
[80901699, 58778525], [74314483, 66913061], [66913061, 74314483], [58778525, 80901699], [50000000, 86602540],
[40673664, 91354546], [30901699, 95105652], [20791169, 97814760], [10452846, 99452190], [0, 100000000],
[-10452846, 99452190], [-20791169, 97814760], [-30901699, 95105652], [-40673664, 91354546],
[-50000000, 86602540], [-58778525, 80901699], [-66913061, 74314483], [-74314483, 66913061],
[-80901699, 58778525], [-86602540, 50000000], [-91354546, 40673664], [-95105652, 30901699],
[-97814760, 20791169], [-99452190, 10452846], [-100000000, 0], [-99452190, -10452846],
[-97814760, -20791169], [-95105652, -30901699], [-91354546, -40673664], [-86602540, -50000000],
[-80901699, -58778525], [-74314483, -66913061], [-66913061, -74314483], [-58778525, -80901699],
[-50000000, -86602540], [-40673664, -91354546], [-30901699, -95105652], [-20791169, -97814760],
[-10452846, -99452190], [0, -100000000], [10452846, -99452190], [20791169, -97814760],
[30901699, -95105652], [40673664, -91354546], [50000000, -86602540], [58778525, -80901699],
[66913061, -74314483], [74314483, -66913061], [80901699, -58778525], [86602540, -50000000],
[91354546, -40673664], [95105652, -30901699], [97814760, -20791169], [99452190, -10452846], [100000000, 0]
);
my $result = Slic3r::Geometry::Clipper::intersection_pl($subject, [$clip]);
is scalar(@$result), scalar(@$subject), 'intersection_pl - expected number of polylines';
is sum(map scalar(@$_), @$result), scalar(@$subject) * 2, 'intersection_pl - expected number of points in polylines';
}
#endif
}
SCENARIO("Various Clipper operations - t/clipper.t", "[ClipperUtils]") {
GIVEN("square with hole") {
// CCW oriented contour
Slic3r::Polygon square { { 10, 10 }, { 20, 10 }, { 20, 20 }, { 10, 20 } };
Slic3r::Polygon square2 { { 5, 12 }, { 25, 12 }, { 25, 18 }, { 5, 18 } };
// CW oriented contour
Slic3r::Polygon hole_in_square { { 14, 14 }, { 14, 16 }, { 16, 16 }, { 16, 14 } };
WHEN("intersection_ex with another square") {
ExPolygons intersection = Slic3r::intersection_ex({ square, hole_in_square }, { square2 });
THEN("intersection area matches (hole is preserved)") {
ExPolygon match({ { 20, 18 }, { 10, 18 }, { 10, 12 }, { 20, 12 } },
{ { 14, 16 }, { 16, 16 }, { 16, 14 }, { 14, 14 } });
REQUIRE(intersection.size() == 1);
REQUIRE(intersection.front().area() == Approx(match.area()));
}
}
}
GIVEN("square with hole 2") {
// CCW oriented contour
Slic3r::Polygon square { { 0, 0 }, { 40, 0 }, { 40, 40 }, { 0, 40 } };
Slic3r::Polygon square2 { { 10, 10 }, { 30, 10 }, { 30, 30 }, { 10, 30 } };
// CW oriented contour
Slic3r::Polygon hole { { 15, 15 }, { 15, 25 }, { 25, 25 }, {25, 15 } };
WHEN("union_ex with another square") {
ExPolygons union_ = Slic3r::union_ex({ square, square2, hole });
THEN("union of two ccw and one cw is a contour with no holes") {
REQUIRE(union_.size() == 1);
REQUIRE(union_.front() == ExPolygon { { 40, 40 }, { 0, 40 }, { 0, 0 }, { 40, 0 } } );
}
}
WHEN("diff_ex with another square") {
ExPolygons diff = Slic3r::diff_ex({ square, square2 }, { hole });
THEN("difference of a cw from two ccw is a contour with one hole") {
REQUIRE(diff.size() == 1);
REQUIRE(diff.front().area() == Approx(ExPolygon({ {40, 40}, {0, 40}, {0, 0}, {40, 0} }, { {15, 25}, {25, 25}, {25, 15}, {15, 15} }).area()));
}
}
}
GIVEN("yet another square") {
Slic3r::Polygon square { { 10, 10 }, { 20, 10 }, { 20, 20 }, { 10, 20 } };
Slic3r::Polyline square_pl = square.split_at_first_point();
WHEN("no-op diff_pl") {
Slic3r::Polylines res = Slic3r::diff_pl({ square_pl }, {});
THEN("returns the right number of polylines") {
REQUIRE(res.size() == 1);
}
THEN("returns the unmodified input polyline") {
REQUIRE(res.front().points.size() == square_pl.points.size());
}
}
}
}
template<e_ordering o = e_ordering::OFF, class P, class Tree>
double polytree_area(const Tree &tree, std::vector<P> *out)
{
traverse_pt<o>(tree, out);
return std::accumulate(out->begin(), out->end(), 0.0,
[](double a, const P &p) { return a + p.area(); });
}
size_t count_polys(const ExPolygons& expolys)
{
size_t c = 0;
for (auto &ep : expolys) c += ep.holes.size() + 1;
return c;
}
TEST_CASE("Traversing Clipper PolyTree", "[ClipperUtils]") {
// Create a polygon representing unit box
Polygon unitbox;
const int32_t UNIT = int32_t(1. / SCALING_FACTOR);
unitbox.points = Points{Point{0, 0}, Point{UNIT, 0}, Point{UNIT, UNIT}, Point{0, UNIT}};
Polygon box_frame = unitbox;
box_frame.scale(20, 10);
Polygon hole_left = unitbox;
hole_left.scale(8);
hole_left.translate(UNIT, UNIT);
hole_left.reverse();
Polygon hole_right = hole_left;
hole_right.translate(UNIT * 10, 0);
Polygon inner_left = unitbox;
inner_left.scale(4);
inner_left.translate(UNIT * 3, UNIT * 3);
Polygon inner_right = inner_left;
inner_right.translate(UNIT * 10, 0);
Polygons reference = union_({box_frame, hole_left, hole_right, inner_left, inner_right});
ClipperLib::PolyTree tree = union_pt(reference);
double area_sum = box_frame.area() + hole_left.area() +
hole_right.area() + inner_left.area() +
inner_right.area();
REQUIRE(area_sum > 0);
SECTION("Traverse into Polygons WITHOUT spatial ordering") {
Polygons output;
REQUIRE(area_sum == Approx(polytree_area(tree.GetFirst(), &output)));
REQUIRE(output.size() == reference.size());
}
SECTION("Traverse into ExPolygons WITHOUT spatial ordering") {
ExPolygons output;
REQUIRE(area_sum == Approx(polytree_area(tree.GetFirst(), &output)));
REQUIRE(count_polys(output) == reference.size());
}
SECTION("Traverse into Polygons WITH spatial ordering") {
Polygons output;
REQUIRE(area_sum == Approx(polytree_area<e_ordering::ON>(tree.GetFirst(), &output)));
REQUIRE(output.size() == reference.size());
}
SECTION("Traverse into ExPolygons WITH spatial ordering") {
ExPolygons output;
REQUIRE(area_sum == Approx(polytree_area<e_ordering::ON>(tree.GetFirst(), &output)));
REQUIRE(count_polys(output) == reference.size());
}
}

View File

@ -0,0 +1,15 @@
//#define CATCH_CONFIG_DISABLE
#include <catch2/catch.hpp>
#include <string>
#include "test_data.hpp"
#include <libslic3r/libslic3r.h>
#include <libslic3r/SVG.hpp>
using namespace Slic3r::Test;
using namespace Slic3r;
using namespace std::literals;
SCENARIO("Complete objects separatly") {
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,83 @@
#ifndef SLIC3R_TEST_DATA_HPP
#define SLIC3R_TEST_DATA_HPP
#include <libslic3r/Point.hpp>
#include <libslic3r/TriangleMesh.hpp>
#include <libslic3r/Geometry.hpp>
#include <libslic3r/Model.hpp>
#include <libslic3r/Print.hpp>
#include <libslic3r/Config.hpp>
#include <test_utils.hpp>
#include <unordered_map>
namespace Slic3r { namespace Test {
/// Enumeration of test meshes
enum class TestMesh {
A,
L,
V,
_40x10,
cube_20x20x20,
sphere_50mm,
bridge,
bridge_with_hole,
cube_with_concave_hole,
cube_with_hole,
gt2_teeth,
ipadstand,
overhang,
pyramid,
sloping_hole,
slopy_cube,
small_dorito,
step,
two_hollow_squares,
di_5mm_center_notch,
di_10mm_notch,
di_20mm_notch,
di_25mm_notch
};
// Neccessary for <c++17
struct TestMeshHash {
std::size_t operator()(TestMesh tm) const {
return static_cast<std::size_t>(tm);
}
};
/// Mesh enumeration to name mapping
extern const std::unordered_map<TestMesh, const std::string, TestMeshHash> mesh_names;
/// Port of Slic3r::Test::mesh
/// Basic cubes/boxes should call TriangleMesh::make_cube() directly and rescale/translate it
TriangleMesh mesh(TestMesh m);
TriangleMesh mesh(TestMesh m, Vec3f translate, Vec3f scale = Vec3f(1.0, 1.0, 1.0));
TriangleMesh mesh(TestMesh m, Vec3f translate, double scale = 1.0);
/// Templated function to see if two values are equivalent (+/- epsilon)
template <typename T>
bool _equiv(const T& a, const T& b) { return abs(a - b) < Slic3r::Geometry::epsilon; }
template <typename T>
bool _equiv(const T& a, const T& b, double epsilon) { return abs(a - b) < epsilon; }
//Slic3r::Model model(const std::string& model_name, TestMesh m, Vec3f translate = Vec3f(0,0,0), Vec3f scale = Vec3f(1.0,1.0,1.0));
//Slic3r::Model model(const std::string& model_name, TestMesh m, Vec3f translate = Vec3f(0,0,0), double scale = 1.0);
Slic3r::Model model(const std::string& model_name, TriangleMesh&& _mesh);
void init_print(Print& print, std::initializer_list<TestMesh> meshes, Slic3r::Model& model, DynamicPrintConfig* _config, bool comments = false);
void init_print(Print& print, std::initializer_list<TriangleMesh> meshes, Slic3r::Model& model, DynamicPrintConfig* _config, bool comments = false);
void gcode(std::string& gcode, Print& print);
std::string read_to_string(const std::string& name);
void clean_file(const std::string& name, const std::string& ext, bool glob = false);
} } // namespace Slic3r::Test
#endif // SLIC3R_TEST_DATA_HPP

View File

@ -0,0 +1,211 @@
//#define CATCH_CONFIG_DISABLE
#include <catch2/catch.hpp>
#include "test_data.hpp"
#include <libslic3r/libslic3r.h>
using namespace Slic3r;
using namespace Slic3r::Geometry;
SCENARIO("denser infills: ")
{
GIVEN("center hole")
{
WHEN("dense infill to enlarged") {
Model model{};
Print print{};
DynamicPrintConfig &config = Slic3r::DynamicPrintConfig::full_print_config();
config.set_key_value("layer_height", new ConfigOptionFloat(0.2));
config.set_key_value("first_layer_height", new ConfigOptionFloatOrPercent(0.2, false));
config.set_key_value("infill_dense", new ConfigOptionBool(true));
config.set_key_value("infill_dense_algo", new ConfigOptionEnum<DenseInfillAlgo>(dfaEnlarged));
config.save("C:\\Users\\Admin\\Desktop\\config_def.ini");
Slic3r::Test::init_print(print, { Slic3r::Test::TestMesh::di_5mm_center_notch }, model, &config, false);
print.process();
PrintObject& object = *(print.objects().at(0));
//for (int lidx = 0; lidx < object.layers().size(); lidx++) {
// std::cout << "layer " << lidx << " : \n";
// std::cout << " - region_count= " << object.layers()[lidx]->region_count() << "\n";
// for (int ridx = 0; ridx < object.layers()[lidx]->regions().size(); ridx++) {
// std::cout << " region " << ridx << " : \n";
// std::cout << " - fills= " << object.layers()[lidx]->regions()[ridx]->fills.entities.size() << "\n";
// std::cout << " - surfaces= " << object.layers()[lidx]->regions()[ridx]->fill_surfaces.surfaces.size() << "\n";
// for (int sidx = 0; sidx < object.layers()[lidx]->regions()[ridx]->fill_surfaces.surfaces.size(); sidx++) {
// std::cout << " - type= " << object.layers()[lidx]->regions()[ridx]->fill_surfaces.surfaces[sidx].surface_type << "\n";
// }
// }
//}
/*THEN("67 layers exist in the model") {
REQUIRE(object.layers().size() == 25);
}
THEN("at layer 16 , there are 1 region") {
REQUIRE(object.layers()[16]->region_count() == 1);
}
THEN("at layer 17 , there are 2 region") {
REQUIRE(object.layers()[17]->region_count() == 2);
}
THEN("at layer 18 , there are 2 region") {
REQUIRE(object.layers()[18]->region_count() == 2);
}*/
THEN("correct number of fills") {
REQUIRE(object.layers()[20]->regions()[0]->fills.entities.size() == 1); //sparse
REQUIRE(object.layers()[21]->regions()[0]->fills.entities.size() == 2); //sparse + dense
REQUIRE(object.layers()[21]->regions()[0]->fill_surfaces.surfaces.size() == 2);
REQUIRE(object.layers()[21]->regions()[0]->fill_surfaces.surfaces[0].surface_type == (SurfaceType::stDensSparse | SurfaceType::stPosInternal));
REQUIRE(object.layers()[21]->regions()[0]->fill_surfaces.surfaces[1].surface_type == (SurfaceType::stDensSparse | SurfaceType::stPosInternal));
REQUIRE(std::min(object.layers()[21]->regions()[0]->fill_surfaces.surfaces[0].maxNbSolidLayersOnTop, object.layers()[21]->regions()[0]->fill_surfaces.surfaces[1].maxNbSolidLayersOnTop) == 1);
Surface* srfSparse = &object.layers()[21]->regions()[0]->fill_surfaces.surfaces[0];
Surface* srfDense = &object.layers()[21]->regions()[0]->fill_surfaces.surfaces[1];
if (srfSparse->maxNbSolidLayersOnTop == 1) {
srfSparse = srfDense;
srfDense = &object.layers()[21]->regions()[0]->fill_surfaces.surfaces[0];
}
//std::cout << "sparse area = " << unscaled(unscaled(srfSparse->area())) << " , dense area = " << unscaled(unscaled(srfDense->area())) << "\n";
REQUIRE(unscaled(unscaled(srfSparse->area())) > unscaled(unscaled(srfDense->area())));
REQUIRE(object.layers()[22]->regions()[0]->fills.entities.size() == 2); //sparse + solid-bridge
REQUIRE(object.layers()[22]->regions()[0]->fill_surfaces.surfaces.size() == 2);
if (object.layers()[22]->regions()[0]->fill_surfaces.surfaces[0].surface_type == (SurfaceType::stDensSparse | SurfaceType::stPosInternal)) {
REQUIRE(object.layers()[22]->regions()[0]->fill_surfaces.surfaces[1].surface_type == (SurfaceType::stDensSolid | SurfaceType::stPosInternal | SurfaceType::stModBridge));
} else {
REQUIRE(object.layers()[22]->regions()[0]->fill_surfaces.surfaces[0].surface_type == (SurfaceType::stDensSolid | SurfaceType::stPosInternal | SurfaceType::stModBridge));
REQUIRE(object.layers()[22]->regions()[0]->fill_surfaces.surfaces[1].surface_type == (SurfaceType::stDensSparse | SurfaceType::stPosInternal));
}
REQUIRE(object.layers()[23]->regions()[0]->fills.entities.size() == 2); //sparse + solid
REQUIRE(object.layers()[24]->regions()[0]->fills.entities.size() == 3); //sparse + solid-top + solid-top (over perimeters)
REQUIRE(object.layers()[25]->regions()[0]->fills.entities.size() == 1); //sparse
}
}
}
GIVEN("side hole")
{
WHEN("dense infill to auto") {
Model model{};
Print print{};
DynamicPrintConfig &config = Slic3r::DynamicPrintConfig::full_print_config();
config.set_key_value("layer_height", new ConfigOptionFloat(0.2));
config.set_key_value("first_layer_height", new ConfigOptionFloatOrPercent(0.2, false));
config.set_key_value("infill_dense", new ConfigOptionBool(true));
config.set_key_value("infill_dense_algo", new ConfigOptionEnum<DenseInfillAlgo>(dfaAutomatic));
config.save("C:\\Users\\Admin\\Desktop\\config_def.ini");
Slic3r::Test::init_print(print, { Slic3r::Test::TestMesh::di_10mm_notch }, model, &config, false);
print.process();
PrintObject& object = *(print.objects().at(0));
THEN("correct number of fills") {
REQUIRE(object.layers().size() == 50);
REQUIRE(object.layers()[20]->regions()[0]->fills.entities.size() == 1); //sparse
REQUIRE(object.layers()[21]->regions()[0]->fills.entities.size() == 2); //sparse + dense
REQUIRE(object.layers()[21]->regions()[0]->fill_surfaces.surfaces.size() == 2);
REQUIRE(object.layers()[21]->regions()[0]->fill_surfaces.surfaces[0].surface_type == (SurfaceType::stDensSparse | SurfaceType::stPosInternal));
REQUIRE(object.layers()[21]->regions()[0]->fill_surfaces.surfaces[1].surface_type == (SurfaceType::stDensSparse | SurfaceType::stPosInternal));
REQUIRE(std::min(object.layers()[21]->regions()[0]->fill_surfaces.surfaces[0].maxNbSolidLayersOnTop, object.layers()[21]->regions()[0]->fill_surfaces.surfaces[1].maxNbSolidLayersOnTop) == 1);
Surface* srfSparse = &object.layers()[21]->regions()[0]->fill_surfaces.surfaces[0];
Surface* srfDense = &object.layers()[21]->regions()[0]->fill_surfaces.surfaces[1];
if (srfSparse->maxNbSolidLayersOnTop == 1) {
srfSparse = srfDense;
srfDense = &object.layers()[21]->regions()[0]->fill_surfaces.surfaces[0];
}
REQUIRE(unscaled(unscaled(srfSparse->area())) > unscaled(unscaled(srfDense->area())));
REQUIRE(object.layers()[22]->regions()[0]->fills.entities.size() == 2); //sparse + solid-bridge
REQUIRE(object.layers()[22]->regions()[0]->fill_surfaces.surfaces.size() == 2);
if (object.layers()[22]->regions()[0]->fill_surfaces.surfaces[0].surface_type == (SurfaceType::stDensSparse | SurfaceType::stPosInternal)) {
REQUIRE(object.layers()[22]->regions()[0]->fill_surfaces.surfaces[1].surface_type == (SurfaceType::stDensSolid | SurfaceType::stPosInternal | SurfaceType::stModBridge));
} else {
REQUIRE(object.layers()[22]->regions()[0]->fill_surfaces.surfaces[0].surface_type == (SurfaceType::stDensSolid | SurfaceType::stPosInternal | SurfaceType::stModBridge));
REQUIRE(object.layers()[22]->regions()[0]->fill_surfaces.surfaces[1].surface_type == (SurfaceType::stDensSparse | SurfaceType::stPosInternal));
}
REQUIRE(object.layers()[23]->regions()[0]->fills.entities.size() == 2); //sparse + solid
REQUIRE(object.layers()[24]->regions()[0]->fills.entities.size() == 3); //sparse + solid-top + solid-top (over perimeters)
REQUIRE(object.layers()[25]->regions()[0]->fills.entities.size() == 1); //sparse
REQUIRE(object.layers()[45]->regions()[0]->fills.entities.size() == 1); //sparse
REQUIRE(object.layers()[45]->regions()[0]->fill_surfaces.surfaces.size() == 1);
REQUIRE(object.layers()[45]->regions()[0]->fill_surfaces.surfaces[0].surface_type == (SurfaceType::stDensSparse | SurfaceType::stPosInternal));
REQUIRE(object.layers()[45]->regions()[0]->fill_surfaces.surfaces[0].maxNbSolidLayersOnTop > 1);
REQUIRE(object.layers()[46]->regions()[0]->fills.entities.size() == 1); //dense
REQUIRE(object.layers()[46]->regions()[0]->fills.entities.size() == 1);
REQUIRE(object.layers()[46]->regions()[0]->fill_surfaces.surfaces.size() == 1);
REQUIRE(object.layers()[46]->regions()[0]->fill_surfaces.surfaces[0].surface_type == (SurfaceType::stDensSparse | SurfaceType::stPosInternal));
REQUIRE(object.layers()[46]->regions()[0]->fill_surfaces.surfaces[0].maxNbSolidLayersOnTop == 1);
REQUIRE(object.layers()[47]->regions()[0]->fills.entities.size() == 1); //solid-bridge
REQUIRE(object.layers()[47]->regions()[0]->fill_surfaces.surfaces.size() == 1);
REQUIRE(object.layers()[47]->regions()[0]->fill_surfaces.surfaces[0].surface_type == (SurfaceType::stDensSolid | SurfaceType::stPosInternal | SurfaceType::stModBridge));
REQUIRE(object.layers()[47]->regions()[0]->fill_surfaces.surfaces[0].maxNbSolidLayersOnTop > 1);
REQUIRE(object.layers()[48]->regions()[0]->fills.entities.size() == 1); //solid
REQUIRE(object.layers()[48]->regions()[0]->fill_surfaces.surfaces.size() == 1);
REQUIRE(object.layers()[48]->regions()[0]->fill_surfaces.surfaces[0].surface_type == (SurfaceType::stDensSolid | SurfaceType::stPosInternal));
REQUIRE(object.layers()[48]->regions()[0]->fill_surfaces.surfaces[0].maxNbSolidLayersOnTop > 1);
REQUIRE(object.layers()[49]->regions()[0]->fills.entities.size() == 1); //top
REQUIRE(object.layers()[49]->regions()[0]->fill_surfaces.surfaces.size() == 1);
REQUIRE(object.layers()[49]->regions()[0]->fill_surfaces.surfaces[0].surface_type == (SurfaceType::stDensSolid | SurfaceType::stPosTop));
REQUIRE(object.layers()[49]->regions()[0]->fill_surfaces.surfaces[0].maxNbSolidLayersOnTop > 1);
}
}
WHEN("dense infill to auto-not-full") {
Model model{};
Print print{};
DynamicPrintConfig &config = Slic3r::DynamicPrintConfig::full_print_config();
config.set_key_value("layer_height", new ConfigOptionFloat(0.2));
config.set_key_value("first_layer_height", new ConfigOptionFloatOrPercent(0.2, false));
config.set_key_value("infill_dense", new ConfigOptionBool(true));
config.set_key_value("infill_dense_algo", new ConfigOptionEnum<DenseInfillAlgo>(dfaAutoNotFull));
config.save("C:\\Users\\Admin\\Desktop\\config_def.ini");
Slic3r::Test::init_print(print, { Slic3r::Test::TestMesh::di_10mm_notch }, model, &config, false);
print.process();
PrintObject& object = *(print.objects().at(0));
THEN("correct number of fills") {
REQUIRE(object.layers().size() == 50);
REQUIRE(object.layers()[20]->regions()[0]->fills.entities.size() == 1); //sparse
REQUIRE(object.layers()[21]->regions()[0]->fills.entities.size() == 2); //sparse + dense
REQUIRE(object.layers()[21]->regions()[0]->fill_surfaces.surfaces.size() == 2);
REQUIRE(object.layers()[21]->regions()[0]->fill_surfaces.surfaces[0].surface_type == (SurfaceType::stDensSparse | SurfaceType::stPosInternal));
REQUIRE(object.layers()[21]->regions()[0]->fill_surfaces.surfaces[1].surface_type == (SurfaceType::stDensSparse | SurfaceType::stPosInternal));
REQUIRE(std::min(object.layers()[21]->regions()[0]->fill_surfaces.surfaces[0].maxNbSolidLayersOnTop, object.layers()[21]->regions()[0]->fill_surfaces.surfaces[1].maxNbSolidLayersOnTop) == 1);
Surface* srfSparse = &object.layers()[21]->regions()[0]->fill_surfaces.surfaces[0];
Surface* srfDense = &object.layers()[21]->regions()[0]->fill_surfaces.surfaces[1];
if (srfSparse->maxNbSolidLayersOnTop == 1) {
srfSparse = srfDense;
srfDense = &object.layers()[21]->regions()[0]->fill_surfaces.surfaces[0];
}
REQUIRE(unscaled(unscaled(srfSparse->area())) > unscaled(unscaled(srfDense->area())));
REQUIRE(object.layers()[22]->regions()[0]->fills.entities.size() == 2); //sparse + solid-bridge
REQUIRE(object.layers()[22]->regions()[0]->fill_surfaces.surfaces.size() == 2);
if (object.layers()[22]->regions()[0]->fill_surfaces.surfaces[0].surface_type == (SurfaceType::stDensSparse | SurfaceType::stPosInternal)) {
REQUIRE(object.layers()[22]->regions()[0]->fill_surfaces.surfaces[1].surface_type == (SurfaceType::stDensSolid | SurfaceType::stPosInternal | SurfaceType::stModBridge));
} else {
REQUIRE(object.layers()[22]->regions()[0]->fill_surfaces.surfaces[0].surface_type == (SurfaceType::stDensSolid | SurfaceType::stPosInternal | SurfaceType::stModBridge));
REQUIRE(object.layers()[22]->regions()[0]->fill_surfaces.surfaces[1].surface_type == (SurfaceType::stDensSparse | SurfaceType::stPosInternal));
}
REQUIRE(object.layers()[23]->regions()[0]->fills.entities.size() == 2); //sparse + solid
REQUIRE(object.layers()[24]->regions()[0]->fills.entities.size() == 3); //sparse + solid-top + solid-top (over perimeters)
REQUIRE(object.layers()[25]->regions()[0]->fills.entities.size() == 1); //sparse
REQUIRE(object.layers()[45]->regions()[0]->fills.entities.size() == 1); //sparse
REQUIRE(object.layers()[45]->regions()[0]->fill_surfaces.surfaces.size() == 1);
REQUIRE(object.layers()[45]->regions()[0]->fill_surfaces.surfaces[0].surface_type == (SurfaceType::stDensSparse | SurfaceType::stPosInternal));
REQUIRE(object.layers()[45]->regions()[0]->fill_surfaces.surfaces[0].maxNbSolidLayersOnTop > 1);
REQUIRE(object.layers()[46]->regions()[0]->fills.entities.size() == 1); //dense
REQUIRE(object.layers()[46]->regions()[0]->fill_surfaces.surfaces.size() == 1);
REQUIRE(object.layers()[46]->regions()[0]->fill_surfaces.surfaces[0].surface_type == (SurfaceType::stDensSparse | SurfaceType::stPosInternal));
REQUIRE(object.layers()[46]->regions()[0]->fill_surfaces.surfaces[0].maxNbSolidLayersOnTop > 1);
REQUIRE(object.layers()[47]->regions()[0]->fills.entities.size() == 1); //solid-bridge
REQUIRE(object.layers()[47]->regions()[0]->fill_surfaces.surfaces.size() == 1);
REQUIRE(object.layers()[47]->regions()[0]->fill_surfaces.surfaces[0].surface_type == (SurfaceType::stDensSolid | SurfaceType::stPosInternal | SurfaceType::stModBridge));
REQUIRE(object.layers()[47]->regions()[0]->fill_surfaces.surfaces[0].maxNbSolidLayersOnTop > 1);
REQUIRE(object.layers()[48]->regions()[0]->fills.entities.size() == 1); //solid
REQUIRE(object.layers()[48]->regions()[0]->fill_surfaces.surfaces.size() == 1);
REQUIRE(object.layers()[48]->regions()[0]->fill_surfaces.surfaces[0].surface_type == (SurfaceType::stDensSolid | SurfaceType::stPosInternal));
REQUIRE(object.layers()[48]->regions()[0]->fill_surfaces.surfaces[0].maxNbSolidLayersOnTop > 1);
REQUIRE(object.layers()[49]->regions()[0]->fills.entities.size() == 1); //top
REQUIRE(object.layers()[49]->regions()[0]->fill_surfaces.surfaces.size() == 1);
REQUIRE(object.layers()[49]->regions()[0]->fill_surfaces.surfaces[0].surface_type == (SurfaceType::stDensSolid | SurfaceType::stPosTop));
REQUIRE(object.layers()[49]->regions()[0]->fill_surfaces.surfaces[0].maxNbSolidLayersOnTop > 1);
}
}
}
}

View File

@ -0,0 +1,165 @@
//#define CATCH_CONFIG_DISABLE
#include <catch2/catch.hpp>
#include <string>
#include "test_data.hpp"
#include <libslic3r/libslic3r.h>
#include <libslic3r/ExtrusionEntityCollection.hpp>
#include <libslic3r/ExtrusionEntity.hpp>
#include <libslic3r/Point.hpp>
#include <libslic3r/GCodeReader.hpp>
#include <cstdlib>
using namespace Slic3r;
Slic3r::Point random_point(float LO=-50, float HI=50) {
float x = LO + static_cast <float> (rand()) /( static_cast <float> (RAND_MAX/(HI-LO)));
float y = LO + static_cast <float> (rand()) /( static_cast <float> (RAND_MAX/(HI-LO)));
return Slic3r::Point(x, y);
}
// build a sample extrusion entity collection with random start and end points.
Slic3r::ExtrusionPath random_path(size_t length = 20, float LO=-50, float HI=50) {
Slic3r::ExtrusionPath t { Slic3r::ExtrusionRole::erPerimeter, 1.0, 1.0, 1.0};
for (size_t j = 0; j < length; j++) {
t.polyline.append(random_point(LO, HI));
}
return t;
}
Slic3r::ExtrusionPaths random_paths(size_t count=10, size_t length=20, float LO=-50, float HI=50) {
Slic3r::ExtrusionPaths p;
for (size_t i = 0; i < count; i++)
p.push_back(random_path(length, LO, HI));
return p;
}
using namespace Slic3r;
SCENARIO("ExtrusionEntityCollection: Polygon flattening") {
srand(0xDEADBEEF); // consistent seed for test reproducibility.
// Generate one specific random path set and save it for later comparison
Slic3r::ExtrusionPaths nosort_path_set {random_paths()};
Slic3r::ExtrusionEntityCollection sub_nosort;
sub_nosort.append(nosort_path_set);
sub_nosort.no_sort = true;
Slic3r::ExtrusionEntityCollection sub_sort;
sub_sort.no_sort = false;
sub_sort.append(random_paths());
GIVEN("A Extrusion Entity Collection with a child that has one child that is marked as no-sort") {
Slic3r::ExtrusionEntityCollection sample;
sample.append(sub_sort);
sample.append(sub_nosort);
sample.append(sub_sort);
WHEN("The EEC is flattened with default options (preserve_order=false)") {
Slic3r::ExtrusionEntityCollection output = Slic3r::FlatenEntities(false).flatten(sample);
THEN("The output EEC contains no Extrusion Entity Collections") {
CHECK(std::count_if(output.entities.cbegin(), output.entities.cend(), [=](const ExtrusionEntity* e) {return e->is_collection();}) == 0);
}
}
WHEN("The EEC is flattened with preservation (preserve_order=true)") {
Slic3r::ExtrusionEntityCollection output = Slic3r::FlatenEntities(true).flatten(sample);
THEN("The output EECs contains one EEC.") {
CHECK(std::count_if(output.entities.cbegin(), output.entities.cend(), [=](const ExtrusionEntity* e) {return e->is_collection();}) == 1);
}
AND_THEN("The ordered EEC contains the same order of elements than the original") {
// find the entity in the collection
for (auto e : output.entities) {
if (!e->is_collection()) continue;
Slic3r::ExtrusionEntityCollection* temp = dynamic_cast<ExtrusionEntityCollection*>(e);
// check each Extrusion path against nosort_path_set to see if the first and last match the same
CHECK(nosort_path_set.size() == temp->entities.size());
for (size_t i = 0; i < nosort_path_set.size(); i++) {
CHECK(temp->entities[i]->first_point() == nosort_path_set[i].first_point());
CHECK(temp->entities[i]->last_point() == nosort_path_set[i].last_point());
}
}
}
}
}
}
SCENARIO("ExtrusionEntityCollection: no sort") {
DynamicPrintConfig &config = Slic3r::DynamicPrintConfig::full_print_config();
config.set_key_value("gcode_comments", new ConfigOptionBool(true));
config.set_deserialize("skirts", "0");
Model model{};
Print print{};
Slic3r::Test::init_print(print, { Slic3r::Test::TestMesh::cube_20x20x20}, model, &config);
std::map<double, bool> layers_with_skirt;
std::map<double, bool> layers_with_brim;
std::string gcode_filepath{ "" };
print.process();
//replace extrusion from sliceing by manual ones
print.objects()[0]->clear_layers();
Layer* customL_layer = print.objects()[0]->add_layer(0, 0.2, 0.2, 0.1);
LayerRegion* custom_region = customL_layer->add_region(print.regions()[0]);
ExtrusionPath path_peri(ExtrusionRole::erPerimeter);
path_peri.polyline.append(Point{ 0,0 });
path_peri.polyline.append(Point{ scale_(1),scale_(0) });
ExtrusionPath path_fill1(ExtrusionRole::erInternalInfill);
path_fill1.polyline.append(Point{ scale_(1),scale_(0) });
path_fill1.polyline.append(Point{ scale_(2),scale_(0) });
ExtrusionPath path_fill2(ExtrusionRole::erInternalInfill);
path_fill2.polyline.append(Point{ scale_(2),scale_(0) });
path_fill2.polyline.append(Point{ scale_(3),scale_(0) });
ExtrusionEntityCollection coll_fill;
coll_fill.append(path_fill2);
coll_fill.append(path_fill1);
ExtrusionEntityCollection coll_peri;
coll_peri.append(path_peri);
WHEN("sort") {
custom_region->fills.append(coll_fill);
custom_region->perimeters.append(coll_peri);
coll_fill.no_sort = false;
Slic3r::Test::gcode(gcode_filepath, print);
auto parser{ Slic3r::GCodeReader() };
std::vector<float> extrude_x;
parser.parse_file(gcode_filepath, [&extrude_x](Slic3r::GCodeReader& self, const Slic3r::GCodeReader::GCodeLine& line)
{
if (line.comment() == " infill" || line.comment() == " perimeter" || line.comment() == " move to first infill point") {
extrude_x.push_back(line.x());
}
});
Slic3r::Test::clean_file(gcode_filepath, "gcode");
REQUIRE(extrude_x.size()==3);
REQUIRE(extrude_x[0] == 91);
REQUIRE(extrude_x[1] == 92);
REQUIRE(extrude_x[2] == 93);
}
WHEN("no sort") {
coll_fill.no_sort = true;
custom_region->fills.append(coll_fill);
custom_region->perimeters.append(coll_peri);
Slic3r::Test::gcode(gcode_filepath, print);
auto parser{ Slic3r::GCodeReader() };
std::vector<float> extrude_x;
parser.parse_file(gcode_filepath, [&extrude_x](Slic3r::GCodeReader& self, const Slic3r::GCodeReader::GCodeLine& line)
{
if (line.comment() == " infill" || line.comment() == " perimeter" || line.comment() == " move to first infill point") {
extrude_x.push_back(line.x());
}
});
Slic3r::Test::clean_file(gcode_filepath, "gcode");
REQUIRE(extrude_x.size() == 5);
REQUIRE(extrude_x[0] == 91);
REQUIRE(extrude_x[1] == 92);
REQUIRE(extrude_x[2] == 93);
REQUIRE(extrude_x[3] == 91);
REQUIRE(extrude_x[4] == 92);
}
}

View File

@ -0,0 +1,705 @@
//#define CATCH_CONFIG_DISABLE
#include <catch2/catch.hpp>
#include "test_data.hpp"
#include <libslic3r/Fill/Fill.hpp>
#include <libslic3r/Print.hpp>
#include <libslic3r/Geometry.hpp>
#include <libslic3r/Flow.hpp>
#include <libslic3r/ClipperUtils.hpp>
#include <libslic3r/SVG.hpp>
using namespace Slic3r;
using namespace Slic3r::Geometry;
using namespace Slic3r::Test;
bool test_if_solid_surface_filled(const ExPolygon& expolygon, double flow_spacing, double angle = 0, double density = 1.0);
//TEST_CASE("Fill: adjusted solid distance") {
// Print print;
// int surface_width {250};
//
// int distance {Slic3r::Flow::solid_spacing(surface_width, 47)};
//
// REQUIRE(distance == Approx(50));
// REQUIRE(surface_width % distance == 0);
//}
Polylines test(const ExPolygon& poly, Fill &filler, const FillParams &params){
Surface surface{ Slic3r::Surface((stPosTop | stDensSolid), poly) };
return filler.fill_surface(&surface, params);
}
TEST_CASE("Fill: Pattern Path Length") {
Fill* filler {Slic3r::Fill::new_from_type("rectilinear")};
filler->angle = -(PI) / 2.0;
FillParams params{};
params.dont_adjust = true;
params.density = 0.1; // 5/50
filler->set_bounding_box(BoundingBox(Point(0, 0), Point::new_scale(Point(100, 100))));
filler->init_spacing(5, params);
//params.endpoints_overlap = false;
SECTION("Square") {
Points test_set{};
test_set.reserve(4);
Points points {Point{0,0}, Point{100,0}, Point{100,100}, Point{0,100}};
for (size_t i = 0; i < 4; ++i) {
std::transform(points.cbegin()+i, points.cend(), std::back_inserter(test_set), [] (const Point& a) -> Point { return Point::new_scale(a); } );
std::transform(points.cbegin(), points.cbegin()+i, std::back_inserter(test_set), [] (const Point& a) -> Point { return Point::new_scale(a); } );
Slic3r::ExPolygon expoly{};
expoly.contour = Slic3r::Polygon{ test_set };
Polylines paths {test(expoly, *filler, params)};
REQUIRE(paths.size() == 1); // one continuous path
// TODO: determine what the "Expected length" should be for rectilinear fill of a 100x100 polygon.
// This check only checks that it's above scale(3*100 + 2*50) + scaled_epsilon.
// ok abs($paths->[0]->length - scale(3*100 + 2*50)) - scaled_epsilon, 'path has expected length';
REQUIRE(std::abs(paths[0].length() - static_cast<double>(scale_(3*100 + 2*50))) - SCALED_EPSILON > 0); // path has expected length
test_set.clear();
}
}
SECTION("Diamond with endpoints on grid") {
Points points {Point{0,0}, Point{100,0}, Point{150,50}, Point{100,100}, Point{0,100}, Point{-50,50}};
Points test_set{};
test_set.reserve(6);
std::transform(points.cbegin(), points.cend(), std::back_inserter(test_set), [] (const Point& a) -> Point { return Point::new_scale(a); } );
Slic3r::ExPolygon expoly;
expoly.contour = Slic3r::Polygon(test_set);
Polylines paths {test(expoly, *filler, params)};
REQUIRE(paths.size() == 1); // one continuous path
}
SECTION("Square with hole") {
Points square { Point{0,0}, Point{100,0}, Point{100,100}, Point{0,100}};
Points hole {Point{25,25}, Point{75,25}, Point{75,75}, Point{25,75} };
std::reverse(hole.begin(), hole.end());
Points test_hole{};
Points test_square{};
std::transform(square.cbegin(), square.cend(), std::back_inserter(test_square), [] (const Point& a) -> Point { return Point::new_scale(a); } );
std::transform(hole.cbegin(), hole.cend(), std::back_inserter(test_hole), [] (const Point& a) -> Point { return Point::new_scale(a); } );
for (double angle : {-(PI/2.0), -(PI/4.0), -(PI), PI/2.0, PI}) {
for (double spacing : {25.0, 5.0, 7.5, 8.5}) {
FillParams params_local = params;
params_local.density = filler->get_spacing() / spacing;
filler->angle = angle;
Slic3r::ExPolygon e{};
e.contour = Slic3r::Polygon(test_square);
e.holes = Slic3r::Polygons(Slic3r::Polygon(test_hole));
Polylines paths {test(e, *filler, params_local)};
//std::cout << "paths.size="<<paths.size() << "\n";
//{
// std::stringstream stri;
// stri << "squarewithhole.svg";
// SVG svg(stri.str());
// svg.draw(paths);
// svg.draw(e);
// svg.Close();
//}
//path CAN loop around the hole
REQUIRE(paths.size() >= 1);
REQUIRE(paths.size() <= 3);
// paths don't cross hole
REQUIRE(diff_pl(paths, offset(e, (float)(+SCALED_EPSILON * 10))).size() == 0);
}
}
}
SECTION("Regression: Missing infill segments in some rare circumstances") {
FillParams params_local = params;
params_local.density = 1;
params_local.dont_adjust = false;
Fill* filler_local = { Slic3r::Fill::new_from_type("rectilinear") };
filler_local->angle = (PI/4.0);
filler_local->set_bounding_box(BoundingBox(Point(0, 0), Point(2512749, 2512749)));
filler_local->init_spacing(0.654498, params_local);
//filler_local->endpoints_overlap = unscale(359974);
filler_local->layer_id = 66;
filler_local->z = 20.15;
Points points {Point{25771516,14142125},Point{14142138,25771515},Point{2512749,14142131},Point{14142125,2512749}};
Slic3r::ExPolygon expoly{};
expoly.contour = Slic3r::Polygon(points);
Polylines paths {test(expoly, *filler_local, params_local)};
REQUIRE(paths.size() == 1); // one continuous path
// TODO: determine what the "Expected length" should be for rectilinear fill of a 100x100 polygon.
// This check only checks that it's above scale(3*100 + 2*50) + scaled_epsilon.
// ok abs($paths->[0]->length - scale(3*100 + 2*50)) - scaled_epsilon, 'path has expected length';
REQUIRE(std::abs(paths[0].length() - static_cast<double>(scale_(3*100 + 2*50))) - SCALED_EPSILON > 0); // path has expected length
}
SECTION("Rotated Square") {
Points square { Point::new_scale(0,0), Point::new_scale(50,0), Point::new_scale(50,50), Point::new_scale(0,50)};
ExPolygon expolygon{};
expolygon.contour = Slic3r::Polygon(square);
auto filler {Slic3r::Fill::new_from_type("rectilinear")};
filler->bounding_box = expolygon.contour.bounding_box();
filler->angle = 0.F;
Surface surface {(stPosTop|stDensSolid), expolygon};
Flow flow {0.69f, 0.4f, 0.50f};
params.density = 1.0;
filler->init_spacing(flow.spacing(), params);
for (auto angle : { 0.0, 45.0}) {
surface.expolygon.rotate(angle, Point{0,0});
Polylines paths = filler->fill_surface(&surface, params);
REQUIRE(paths.size() == 1);
}
}
SECTION("Solid surface fill") {
Points points {
Point::new_scale(6883102, 9598327.01296997),
Point::new_scale(6883102, 20327272.01297),
Point::new_scale(3116896, 20327272.01297),
Point::new_scale(3116896, 9598327.01296997)
};
Slic3r::ExPolygon expolygon{};
expolygon.contour = Slic3r::Polygon{ points };
REQUIRE(test_if_solid_surface_filled(expolygon, 0.55) == true);
for (size_t i = 0; i <= 20; ++i)
{
expolygon.scale(1.05);
//FIXME number overflow.
//REQUIRE(test_if_solid_surface_filled(expolygon, 0.55) == true);
}
}
SECTION("Solid surface fill") {
Points points {
Point{59515297,5422499},Point{59531249,5578697},Point{59695801,6123186},
Point{59965713,6630228},Point{60328214,7070685},Point{60773285,7434379},
Point{61274561,7702115},Point{61819378,7866770},Point{62390306,7924789},
Point{62958700,7866744},Point{63503012,7702244},Point{64007365,7434357},
Point{64449960,7070398},Point{64809327,6634999},Point{65082143,6123325},
Point{65245005,5584454},Point{65266967,5422499},Point{66267307,5422499},
Point{66269190,8310081},Point{66275379,17810072},Point{66277259,20697500},
Point{65267237,20697500},Point{65245004,20533538},Point{65082082,19994444},
Point{64811462,19488579},Point{64450624,19048208},Point{64012101,18686514},
Point{63503122,18415781},Point{62959151,18251378},Point{62453416,18198442},
Point{62390147,18197355},Point{62200087,18200576},Point{61813519,18252990},
Point{61274433,18415918},Point{60768598,18686517},Point{60327567,19047892},
Point{59963609,19493297},Point{59695865,19994587},Point{59531222,20539379},
Point{59515153,20697500},Point{58502480,20697500},Point{58502480,5422499}
};
Slic3r::ExPolygon expolygon;
expolygon.contour = Slic3r::Polygon{ points };
REQUIRE(test_if_solid_surface_filled(expolygon, 0.55) == true);
REQUIRE(test_if_solid_surface_filled(expolygon, 0.55, PI/2.0) == true);
}
SECTION("Solid surface fill") {
Points points {
Point::new_scale(0,0),Point::new_scale(98,0),Point::new_scale(98,10), Point::new_scale(0,10)
};
Slic3r::ExPolygon expolygon{};
expolygon.contour = Slic3r::Polygon{ points };
REQUIRE(test_if_solid_surface_filled(expolygon, 0.5, 45.0, 0.99) == true);
}
}
class ExtrusionGetVolume : public ExtrusionVisitor {
double volume = 0;
public:
ExtrusionGetVolume() {}
void use(ExtrusionPath &path) override {
volume += unscaled(path.length()) * path.mm3_per_mm; }
void use(ExtrusionPath3D &path3D) override { volume += unscaled(path3D.length()) * path3D.mm3_per_mm; }
void use(ExtrusionMultiPath &multipath) override { for (ExtrusionPath path : multipath.paths) path.visit(*this); }
void use(ExtrusionMultiPath3D &multipath) override { for (ExtrusionPath path : multipath.paths) path.visit(*this); }
void use(ExtrusionLoop &loop) override { for (ExtrusionPath path : loop.paths) path.visit(*this); }
void use(ExtrusionEntityCollection &collection) override { for (ExtrusionEntity *entity : collection.entities) entity->visit(*this); }
double get(ExtrusionEntityCollection &coll) {
for (ExtrusionEntity *entity : coll.entities) entity->visit(*this);
return volume;
}
};
#include "libslic3r/GCodeReader.hpp"
TEST_CASE("Fill: extrude gcode and check it")
{
SECTION("simple square") {
Model model{};
TriangleMesh sample_mesh = make_cube(5, 5, 0.2);
double volume = (5 * 5 * 0.2);
sample_mesh.repair();
DynamicPrintConfig &config = Slic3r::DynamicPrintConfig::full_print_config();
config.set_key_value("perimeters", new ConfigOptionInt(1));
config.set_key_value("top_solid_layers", new ConfigOptionInt(1));
config.set_key_value("bottom_solid_layers", new ConfigOptionInt(1));
config.set_key_value("enforce_full_fill_volume", new ConfigOptionBool(true));
config.set_key_value("infill_overlap", new ConfigOptionFloatOrPercent(0.1, true));
config.set_key_value("skirts", new ConfigOptionInt(0));
config.set_key_value("layer_height", new ConfigOptionFloat(0.2)); // get a known number of layers
config.set_key_value("first_layer_height", new ConfigOptionFloatOrPercent(0.2, false));
config.set_key_value("extrusion_width", new ConfigOptionFloatOrPercent(0.5, false));
config.set_key_value("infill_extrusion_width", new ConfigOptionFloatOrPercent(0.5, false));
config.set_key_value("perimeter_extrusion_width", new ConfigOptionFloatOrPercent(0.5, false));
config.set_key_value("first_layer_extrusion_width", new ConfigOptionFloatOrPercent(0.5, false));
config.set_key_value("external_perimeter_extrusion_width", new ConfigOptionFloatOrPercent(0.5, false));
config.set_key_value("solid_infill_extrusion_width", new ConfigOptionFloatOrPercent(0.5, false));
config.set_key_value("top_infill_extrusion_width", new ConfigOptionFloatOrPercent(0.5, false));
auto event_counter{ 0U };
std::string stage;
Print print{};
Slic3r::Test::init_print(print, { sample_mesh }, model, &config);
print.process();
std::string gcode_filepath{ "" };
Slic3r::Test::gcode(gcode_filepath, print);
//std::cout << "gcode generation done\n";
std::string gcode_from_file = read_to_string(gcode_filepath);
//string[] lineArray = gcode_from_file
GCodeReader parser;
double volume_extruded = 0;
int idx = 0;
double volume_perimeter_extruded = 0;
double volume_infill_extruded = 0;
// add remaining time lines where needed
parser.parse_buffer(gcode_from_file,
[&](GCodeReader& reader, const GCodeReader::GCodeLine& line)
{
if (line.cmd_is("G1"))
{
if (line.dist_E(reader) > 0 && line.dist_XY(reader) > 0) {
//std::cout << "add " << line.dist_E(reader)<<" now "<< volume_extruded<<"=>";
volume_extruded += line.dist_E(reader)*(PI*1.75*1.75 / 4.);
//std::cout << volume_extruded << "\n";
if (idx<4)volume_perimeter_extruded += line.dist_E(reader)*(PI*1.75*1.75 / 4.);
else volume_infill_extruded += line.dist_E(reader)*(PI*1.75*1.75 / 4.);
idx++;
}
}
});
double perimeterRoundGapRemove = unscaled(print.objects()[0]->layers()[0]->lslices[0].contour.length()) * 0.1*0.1 * (2 - (PI / 2));
double perimeterRoundGapAdd = unscaled(print.objects()[0]->layers()[0]->lslices[0].contour.length()) * 0.1*0.1 * ((PI / 2));
//for (Line &l : print.objects()[0]->layers()[0]->slices.expolygons[0].contour.lines()) {
//}
//std::cout << "flow mm3permm: " << Flow{ 0.5f,0.2f,0.4f,false }.mm3_per_mm() << "\n";
//std::cout << "perimeter : " << unscaled(print.objects()[0]->layers()[0]->slices.expolygons[0].contour.length()) << " != " << (PI * 10) << "\n";
//std::cout << "created a mesh of volume " << volume << " and i have extruded " << volume_extruded << " mm3.\n";
//std::cout << "Note that if we remove the bits of the external extrusion, it's only a volume of " << (volume - perimeterRoundGapRemove) << " that needs to be filled\n";
//std::cout << "Note that if we add the bits of the external extrusion, it's a volume of " << (volume + perimeterRoundGapAdd) << " that needs to be filled\n";
double volumeExtrPerimeter = ExtrusionGetVolume{}.get(print.objects()[0]->layers()[0]->regions()[0]->perimeters);
double volumeExtrInfill = ExtrusionGetVolume{}.get(print.objects()[0]->layers()[0]->regions()[0]->fills);
double volumeInfill = 0;
for (const ExPolygon & p : print.objects()[0]->layers()[0]->regions()[0]->fill_no_overlap_expolygons) {
volumeInfill += unscaled(unscaled(p.area()));
}
volumeInfill *= 0.2;/*
std::cout << "volumeRealr=" << (volume_perimeter_extruded + volume_infill_extruded) << " volumeRealPerimeter= " << volume_perimeter_extruded << " and volumeRealInfill=" << volume_infill_extruded << " mm3." << "\n";
std::cout << "volumeExtr=" << (volumeExtrPerimeter + volumeExtrInfill) << " volumeExtrPerimeter= " << volumeExtrPerimeter << " and volumeExtrInfill=" << volumeExtrInfill << " mm3." << "\n";
std::cout << "volumePerimeter= " << (volume - volumeInfill) << " volumePerimeter(wo/bits)= " << (volume - volumeInfill- perimeterRoundGapRemove) << " and volumeInfill=" << volumeInfill << " mm3." << "\n";*/
//Flow fl{0.5f, 0.2f, 0.4f, false};
//{
// std::stringstream stri;
// stri << "extrusion_width_learning" << ".svg";
// SVG svg(stri.str());
// //svg.draw(bounds);
// svg.draw(print.objects()[0]->layers()[0]->slices.expolygons[0].contour, "green");
// svg.draw(print.objects()[0]->layers()[0]->regions()[0]->fill_no_overlap_expolygons, "black", scale_(0.01));
// svg.draw(print.objects()[0]->layers()[0]->regions()[0]->perimeters.as_polylines(), "orange", fl.scaled_width());
// svg.draw(print.objects()[0]->layers()[0]->regions()[0]->perimeters.as_polylines(), "red", fl.scaled_spacing());
// svg.draw(print.objects()[0]->layers()[0]->regions()[0]->fills.as_polylines(), "cyan", fl.scaled_width());
// svg.draw(print.objects()[0]->layers()[0]->regions()[0]->fills.as_polylines(), "blue", fl.scaled_spacing());
// svg.Close();
//}
//std::cout << gcode_from_file;
REQUIRE(abs(volumeInfill - volumeExtrInfill) < EPSILON);
REQUIRE(abs(volumeInfill - volume_infill_extruded) < 0.01);
REQUIRE(abs((volume - volumeInfill - perimeterRoundGapRemove) - volumeExtrPerimeter) < 0.01);
REQUIRE(abs((volume - volumeInfill - perimeterRoundGapRemove) - volume_perimeter_extruded) < 0.1); //there are a bit less for seam mitigation
clean_file(gcode_filepath, "gcode");
}
SECTION("simple disk") {
Model model{};
TriangleMesh sample_mesh = make_cylinder(5, 0.2);
double volume = (PI * 25 * 0.2);
sample_mesh.repair();
DynamicPrintConfig &config = Slic3r::DynamicPrintConfig::full_print_config();
config.set_key_value("perimeters", new ConfigOptionInt(1));
config.set_key_value("top_solid_layers", new ConfigOptionInt(1));
config.set_key_value("bottom_solid_layers", new ConfigOptionInt(1));
config.set_key_value("enforce_full_fill_volume", new ConfigOptionBool(true));
config.set_key_value("infill_overlap", new ConfigOptionFloatOrPercent(0.1, true));
config.set_key_value("skirts", new ConfigOptionInt(0));
config.set_key_value("layer_height", new ConfigOptionFloat(0.2)); // get a known number of layers
config.set_key_value("first_layer_height", new ConfigOptionFloatOrPercent(0.2, false));
config.set_key_value("extrusion_width", new ConfigOptionFloatOrPercent(0.5, false));
config.set_key_value("infill_extrusion_width", new ConfigOptionFloatOrPercent(0.5, false));
config.set_key_value("perimeter_extrusion_width", new ConfigOptionFloatOrPercent(0.5, false));
config.set_key_value("first_layer_extrusion_width", new ConfigOptionFloatOrPercent(0.5, false));
config.set_key_value("external_perimeter_extrusion_width", new ConfigOptionFloatOrPercent(0.5, false));
config.set_key_value("solid_infill_extrusion_width", new ConfigOptionFloatOrPercent(0.5, false));
config.set_key_value("top_infill_extrusion_width", new ConfigOptionFloatOrPercent(0.5, false));
auto event_counter{ 0U };
std::string stage;
Print print{};
Slic3r::Test::init_print(print, { sample_mesh }, model, &config);
print.process();
std::string gcode_filepath{ "" };
Slic3r::Test::gcode(gcode_filepath, print);
//std::cout << "gcode generation done\n";
std::string gcode_from_file = read_to_string(gcode_filepath);
//string[] lineArray = gcode_from_file
GCodeReader parser;
double volume_extruded = 0;
int idx = 0;
double volume_perimeter_extruded = 0;
double volume_infill_extruded = 0;
// add remaining time lines where needed
parser.parse_buffer(gcode_from_file,
[&](GCodeReader& reader, const GCodeReader::GCodeLine& line)
{
if (line.cmd_is("G1"))
{
if (line.dist_E(reader) > 0 && line.dist_XY(reader) > 0) {
//std::cout << "add " << line.dist_E(reader)<<" now "<< volume_extruded<<"=>";
volume_extruded += line.dist_E(reader)*(PI*1.75*1.75 / 4.);
//std::cout << volume_extruded << "\n";
if (idx<36)volume_perimeter_extruded += line.dist_E(reader)*(PI*1.75*1.75 / 4.);
else volume_infill_extruded += line.dist_E(reader)*(PI*1.75*1.75 / 4.);
idx++;
}
}
});
double perimeterRoundGapRemove = unscaled(print.objects()[0]->layers()[0]->lslices[0].contour.length()) * 0.1*0.1 * (2 - (PI / 2));
double perimeterRoundGapAdd = unscaled(print.objects()[0]->layers()[0]->lslices[0].contour.length()) * 0.1*0.1 * ((PI / 2));
double volumeExtrPerimeter = ExtrusionGetVolume{}.get(print.objects()[0]->layers()[0]->regions()[0]->perimeters);
double volumeExtrInfill = ExtrusionGetVolume{}.get(print.objects()[0]->layers()[0]->regions()[0]->fills);
double volumeInfill = 0;
for (const ExPolygon & p : print.objects()[0]->layers()[0]->regions()[0]->fill_no_overlap_expolygons) {
volumeInfill += unscaled(unscaled(p.area()));
}
volumeInfill *= 0.2;
std::cout << "volumeRealr=" << (volume_perimeter_extruded + volume_infill_extruded) << " volumeRealPerimeter= " << volume_perimeter_extruded << " and volumeRealInfill=" << volume_infill_extruded << " mm3." << "\n";
std::cout << "volumeExtr=" << (volumeExtrPerimeter + volumeExtrInfill) << " volumeExtrPerimeter= " << volumeExtrPerimeter << " and volumeExtrInfill=" << volumeExtrInfill << " mm3." << "\n";
std::cout << "volumePerimeter= " << (volume - volumeInfill) << " volumePerimeter(wo/bits)= " << (volume - volumeInfill - perimeterRoundGapRemove) << " and volumeInfill=" << volumeInfill << " mm3." << "\n";
REQUIRE(abs(volumeInfill - volumeExtrInfill) < EPSILON);
REQUIRE(abs(volumeInfill - volume_infill_extruded) < 0.01);
REQUIRE(abs((volume - volumeInfill - perimeterRoundGapRemove) - volumeExtrPerimeter) < EPSILON);
REQUIRE(abs((volume - volumeInfill - perimeterRoundGapRemove) - volume_perimeter_extruded) < 0.1); //there are a bit less for seam mitigation
clean_file(gcode_filepath, "gcode");
}
}
/*
{
my $collection = Slic3r::Polyline::Collection->new(
Slic3r::Polyline->new([0,15], [0,18], [0,20]),
Slic3r::Polyline->new([0,10], [0,8], [0,5]),
);
is_deeply
[ map $_->[Y], map @$_, @{$collection->chained_path_from(Slic3r::Point->new(0,30), 0)} ],
[20, 18, 15, 10, 8, 5],
'chained path';
}
{
my $collection = Slic3r::Polyline::Collection->new(
Slic3r::Polyline->new([4,0], [10,0], [15,0]),
Slic3r::Polyline->new([10,5], [15,5], [20,5]),
);
is_deeply
[ map $_->[X], map @$_, @{$collection->chained_path_from(Slic3r::Point->new(30,0), 0)} ],
[reverse 4, 10, 15, 10, 15, 20],
'chained path';
}
{
my $collection = Slic3r::ExtrusionPath::Collection->new(
map Slic3r::ExtrusionPath->new(polyline => $_, role => 0, mm3_per_mm => 1),
Slic3r::Polyline->new([0,15], [0,18], [0,20]),
Slic3r::Polyline->new([0,10], [0,8], [0,5]),
);
is_deeply
[ map $_->[Y], map @{$_->polyline}, @{$collection->chained_path_from(Slic3r::Point->new(0,30), 0)} ],
[20, 18, 15, 10, 8, 5],
'chained path';
}
{
my $collection = Slic3r::ExtrusionPath::Collection->new(
map Slic3r::ExtrusionPath->new(polyline => $_, role => 0, mm3_per_mm => 1),
Slic3r::Polyline->new([15,0], [10,0], [4,0]),
Slic3r::Polyline->new([10,5], [15,5], [20,5]),
);
is_deeply
[ map $_->[X], map @{$_->polyline}, @{$collection->chained_path_from(Slic3r::Point->new(30,0), 0)} ],
[reverse 4, 10, 15, 10, 15, 20],
'chained path';
}
for my $pattern (qw(rectilinear honeycomb hilbertcurve concentric)) {
my $config = Slic3r::Config->new_from_defaults;
$config->set('fill_pattern', $pattern);
$config->set('external_fill_pattern', $pattern);
$config->set('perimeters', 1);
$config->set('skirts', 0);
$config->set('fill_density', 20);
$config->set('layer_height', 0.05);
$config->set('perimeter_extruder', 1);
$config->set('infill_extruder', 2);
my $print = Slic3r::Test::init_print('20mm_cube', config => $config, scale => 2);
ok my $gcode = Slic3r::Test::gcode($print), "successful $pattern infill generation";
my $tool = undef;
my @perimeter_points = my @infill_points = ();
Slic3r::GCode::Reader->new->parse($gcode, sub {
my ($self, $cmd, $args, $info) = @_;
if ($cmd =~ /^T(\d+)/) {
$tool = $1;
} elsif ($cmd eq 'G1' && $info->{extruding} && $info->{dist_XY} > 0) {
if ($tool == $config->perimeter_extruder-1) {
push @perimeter_points, Slic3r::Point->new_scale($args->{X}, $args->{Y});
} elsif ($tool == $config->infill_extruder-1) {
push @infill_points, Slic3r::Point->new_scale($args->{X}, $args->{Y});
}
}
});
my $convex_hull = convex_hull(\@perimeter_points);
ok !(defined first { !$convex_hull->contains_point($_) } @infill_points), "infill does not exceed perimeters ($pattern)";
}
{
my $config = Slic3r::Config->new_from_defaults;
$config->set('infill_only_where_needed', 1);
$config->set('bottom_solid_layers', 0);
$config->set('infill_extruder', 2);
$config->set('infill_extrusion_width', 0.5);
$config->set('fill_density', 40);
$config->set('cooling', 0); # for preventing speeds from being altered
$config->set('first_layer_speed', '100%'); # for preventing speeds from being altered
my $test = sub {
my $print = Slic3r::Test::init_print('pyramid', config => $config);
my $tool = undef;
my @infill_extrusions = (); # array of polylines
Slic3r::GCode::Reader->new->parse(Slic3r::Test::gcode($print), sub {
my ($self, $cmd, $args, $info) = @_;
if ($cmd =~ /^T(\d+)/) {
$tool = $1;
} elsif ($cmd eq 'G1' && $info->{extruding} && $info->{dist_XY} > 0) {
if ($tool == $config->infill_extruder-1) {
push @infill_extrusions, Slic3r::Line->new_scale(
[ $self->X, $self->Y ],
[ $info->{new_X}, $info->{new_Y} ],
);
}
}
});
return 0 if !@infill_extrusions; # prevent calling convex_hull() with no points
my $convex_hull = convex_hull([ map $_->pp, map @$_, @infill_extrusions ]);
return unscale unscale sum(map $_->area, @{offset([$convex_hull], scale(+$config->infill_extrusion_width/2))});
};
my $tolerance = 5; # mm^2
$config->set('solid_infill_below_area', 0);
ok $test->() < $tolerance,
'no infill is generated when using infill_only_where_needed on a pyramid';
$config->set('solid_infill_below_area', 70);
ok abs($test->() - $config->solid_infill_below_area) < $tolerance,
'infill is only generated under the forced solid shells';
}
{
my $config = Slic3r::Config->new_from_defaults;
$config->set('skirts', 0);
$config->set('perimeters', 1);
$config->set('fill_density', 0);
$config->set('top_solid_layers', 0);
$config->set('bottom_solid_layers', 0);
$config->set('solid_infill_below_area', 20000000);
$config->set('solid_infill_every_layers', 2);
$config->set('perimeter_speed', 99);
$config->set('external_perimeter_speed', 99);
$config->set('cooling', 0);
$config->set('first_layer_speed', '100%');
my $print = Slic3r::Test::init_print('20mm_cube', config => $config);
my %layers_with_extrusion = ();
Slic3r::GCode::Reader->new->parse(Slic3r::Test::gcode($print), sub {
my ($self, $cmd, $args, $info) = @_;
if ($cmd eq 'G1' && $info->{dist_XY} > 0 && $info->{extruding}) {
if (($args->{F} // $self->F) != $config->perimeter_speed*60) {
$layers_with_extrusion{$self->Z} = ($args->{F} // $self->F);
}
}
});
ok !%layers_with_extrusion,
"solid_infill_below_area and solid_infill_every_layers are ignored when fill_density is 0";
}
{
my $config = Slic3r::Config->new_from_defaults;
$config->set('skirts', 0);
$config->set('perimeters', 3);
$config->set('fill_density', 0);
$config->set('layer_height', 0.2);
$config->set('first_layer_height', 0.2);
$config->set('nozzle_diameter', [0.35]);
$config->set('infill_extruder', 2);
$config->set('solid_infill_extruder', 2);
$config->set('infill_extrusion_width', 0.52);
$config->set('solid_infill_extrusion_width', 0.52);
$config->set('first_layer_extrusion_width', 0);
my $print = Slic3r::Test::init_print('A', config => $config);
my %infill = (); # Z => [ Line, Line ... ]
my $tool = undef;
Slic3r::GCode::Reader->new->parse(Slic3r::Test::gcode($print), sub {
my ($self, $cmd, $args, $info) = @_;
if ($cmd =~ /^T(\d+)/) {
$tool = $1;
} elsif ($cmd eq 'G1' && $info->{extruding} && $info->{dist_XY} > 0) {
if ($tool == $config->infill_extruder-1) {
my $z = 1 * $self->Z;
$infill{$z} ||= [];
push @{$infill{$z}}, Slic3r::Line->new_scale(
[ $self->X, $self->Y ],
[ $info->{new_X}, $info->{new_Y} ],
);
}
}
});
my $grow_d = scale($config->infill_extrusion_width)/2;
my $layer0_infill = union([ map @{$_->grow($grow_d)}, @{ $infill{0.2} } ]);
my $layer1_infill = union([ map @{$_->grow($grow_d)}, @{ $infill{0.4} } ]);
my $diff = diff($layer0_infill, $layer1_infill);
$diff = offset2_ex($diff, -$grow_d, +$grow_d);
$diff = [ grep { $_->area > 2*(($grow_d*2)**2) } @$diff ];
is scalar(@$diff), 0, 'no missing parts in solid shell when fill_density is 0';
}
{
# GH: #2697
my $config = Slic3r::Config->new_from_defaults;
$config->set('perimeter_extrusion_width', 0.72);
$config->set('top_infill_extrusion_width', 0.1);
$config->set('infill_extruder', 2); # in order to distinguish infill
$config->set('solid_infill_extruder', 2); # in order to distinguish infill
my $print = Slic3r::Test::init_print('20mm_cube', config => $config);
my %infill = (); # Z => [ Line, Line ... ]
my %other = (); # Z => [ Line, Line ... ]
my $tool = undef;
Slic3r::GCode::Reader->new->parse(Slic3r::Test::gcode($print), sub {
my ($self, $cmd, $args, $info) = @_;
if ($cmd =~ /^T(\d+)/) {
$tool = $1;
} elsif ($cmd eq 'G1' && $info->{extruding} && $info->{dist_XY} > 0) {
my $z = 1 * $self->Z;
my $line = Slic3r::Line->new_scale(
[ $self->X, $self->Y ],
[ $info->{new_X}, $info->{new_Y} ],
);
if ($tool == $config->infill_extruder-1) {
$infill{$z} //= [];
push @{$infill{$z}}, $line;
} else {
$other{$z} //= [];
push @{$other{$z}}, $line;
}
}
});
my $top_z = max(keys %infill);
my $top_infill_grow_d = scale($config->top_infill_extrusion_width)/2;
my $top_infill = union([ map @{$_->grow($top_infill_grow_d)}, @{ $infill{$top_z} } ]);
my $perimeters_grow_d = scale($config->perimeter_extrusion_width)/2;
my $perimeters = union([ map @{$_->grow($perimeters_grow_d)}, @{ $other{$top_z} } ]);
my $covered = union_ex([ @$top_infill, @$perimeters ]);
my @holes = map @{$_->holes}, @$covered;
ok sum(map unscale unscale $_->area*-1, @holes) < 1, 'no gaps between top solid infill and perimeters';
}
*/
//TODO: also check by volume extruded
//TODO: replace the simple area coverage check by one that takes into account the width of the path, not only the default flow spacing
//TODO: test more fills
bool test_if_solid_surface_filled(const ExPolygon& expolygon, double flow_width, double angle, double density) {
auto* filler {Slic3r::Fill::new_from_type("concentricgapfill")};
filler->bounding_box = expolygon.contour.bounding_box();
filler->angle = angle;
FillParams params;
params.dont_adjust = false;
Surface surface((stPosBottom | stDensSolid), expolygon);
//note: here we do flow.width = flow_width , flow.gheight = 0.4, flow.nozzle_size = flow_width;
Flow flow(flow_width, 0.4, flow_width);
params.density = density;
filler->init_spacing(flow.spacing(), params);
Polylines paths {filler->fill_surface(&surface, params)};
// check whether any part was left uncovered
Polygons grown_paths;
grown_paths.reserve(paths.size());
// figure out what is actually going on here re: data types
std::for_each(paths.begin(), paths.end(), [filler, &grown_paths] (const Slic3r::Polyline& p) {
polygons_append(grown_paths, offset(p, scale_(filler->get_spacing() / 2.0)));
});
ExPolygons uncovered = diff_ex(expolygon, grown_paths, true);
// ignore very small dots
const auto scaled_flow_width { std::pow(scale_(flow_width), 2) };
auto iter {std::remove_if(uncovered.begin(), uncovered.end(), [scaled_flow_width] (const ExPolygon& poly) {
return poly.area() > scaled_flow_width;
}) };
uncovered.erase(iter, uncovered.end());
double uncovered_area = 0;
for (ExPolygon &p : uncovered) uncovered_area += unscaled(unscaled(p.area()));
std::cout << "uncovered size =" << uncovered_area << " / "<< unscaled(unscaled(expolygon.area()))<<"\n";
return uncovered.size() == 0; // solid surface is fully filled
}

View File

@ -0,0 +1,221 @@
//#define CATCH_CONFIG_DISABLE
#include <catch2/catch.hpp>
#include <numeric>
#include <sstream>
#include "test_data.hpp" // get access to init_print, etc
#include <libslic3r/Config.hpp>
#include <libslic3r/Model.hpp>"
#include <libslic3r/Config.hpp>
#include <libslic3r/GCodeReader.hpp>
#include <libslic3r/Flow.hpp>
#include <libslic3r/libslic3r.h>
using namespace Slic3r::Test;
using namespace Slic3r;
SCENARIO("Extrusion width specifics", "[!mayfail]") {
GIVEN("A config with a skirt, brim, some fill density, 3 perimeters, and 1 bottom solid layer and a 20mm cube mesh") {
// this is a sharedptr
DynamicPrintConfig &config {Slic3r::DynamicPrintConfig::full_print_config()};
config.set_key_value("skirts", new ConfigOptionInt{1});
config.set_key_value("brim_width", new ConfigOptionFloat{2});
config.set_key_value("perimeters", new ConfigOptionInt{3});
config.set_key_value("fill_density", new ConfigOptionPercent{40});
config.set_key_value("first_layer_height", new ConfigOptionFloatOrPercent{100, true});
WHEN("first layer width set to 2mm") {
Slic3r::Model model;
config.set_key_value("first_layer_extrusion_width", new ConfigOptionFloatOrPercent{2.0, false});
Print print;
Slic3r::Test::init_print(print, { TestMesh::cube_20x20x20 }, model, &config);
//std::cout << "model pos: " << model.objects.front()->instances.front()->get_offset().x() << ": " << model.objects.front()->instances.front()->get_offset().x() << "\n";
//Print print;
//for (auto* mo : model.objects)
// print.auto_assign_extruders(mo);
//print.apply(model, *config);
////std::cout << "print volume: " << print.<< ": " << model.objects().front()->copies().front().x() << "\n";
//std::string err = print.validate();
std::vector<double> E_per_mm_bottom;
std::string gcode_filepath("");
Slic3r::Test::gcode(gcode_filepath, print);
GCodeReader parser {Slic3r::GCodeReader()};
const double layer_height = config.opt_float("layer_height");
std::string gcode_from_file= read_to_string(gcode_filepath);
parser.parse_buffer(gcode_from_file, [&E_per_mm_bottom, layer_height] (Slic3r::GCodeReader& self, const Slic3r::GCodeReader::GCodeLine& line)
{
if (self.z() == Approx(layer_height).margin(0.01)) { // only consider first layer
if (line.extruding(self) && line.dist_XY(self) > 0) {
E_per_mm_bottom.emplace_back(line.dist_E(self) / line.dist_XY(self));
}
}
});
THEN(" First layer width applies to everything on first layer.") {
bool pass = false;
auto avg_E {std::accumulate(E_per_mm_bottom.cbegin(), E_per_mm_bottom.cend(), 0.0) / static_cast<double>(E_per_mm_bottom.size())};
pass = (std::count_if(E_per_mm_bottom.cbegin(), E_per_mm_bottom.cend(), [avg_E] (const double& v) { return v == Approx(avg_E); }) == 0);
REQUIRE(pass == true);
REQUIRE(E_per_mm_bottom.size() > 0); // make sure it actually passed because of extrusion
}
THEN(" First layer width does not apply to upper layer.") {
}
clean_file(gcode_filepath, "gcode");
}
}
}
// needs gcode export
SCENARIO(" Bridge flow specifics.", "[!mayfail]") {
GIVEN("A default config with no cooling and a fixed bridge speed, flow ratio and an overhang mesh.") {
WHEN("bridge_flow_ratio is set to 1.0") {
THEN("Output flow is as expected.") {
}
}
WHEN("bridge_flow_ratio is set to 0.5") {
THEN("Output flow is as expected.") {
}
}
WHEN("bridge_flow_ratio is set to 2.0") {
THEN("Output flow is as expected.") {
}
}
}
GIVEN("A default config with no cooling and a fixed bridge speed, flow ratio, fixed extrusion width of 0.4mm and an overhang mesh.") {
WHEN("bridge_flow_ratio is set to 1.0") {
THEN("Output flow is as expected.") {
}
}
WHEN("bridge_flow_ratio is set to 0.5") {
THEN("Output flow is as expected.") {
}
}
WHEN("bridge_flow_ratio is set to 2.0") {
THEN("Output flow is as expected.") {
}
}
}
}
/// Test the expected behavior for auto-width,
/// spacing, etc
SCENARIO("Flow: Flow math for non-bridges", "[!mayfail]") {
GIVEN("Nozzle Diameter of 0.4, a desired width of 1mm and layer height of 0.5") {
auto width {ConfigOptionFloatOrPercent{1.0, false}};
float spacing {0.4f};
float nozzle_diameter {0.4f};
float bridge_flow {1.0f};
float layer_height {0.5f};
// Spacing for non-bridges is has some overlap
THEN("External perimeter flow has a default spacing fixed to 1.05*nozzle_diameter") {
Flow flow {Flow::new_from_config_width(frExternalPerimeter, ConfigOptionFloatOrPercent{0, false}, nozzle_diameter, layer_height, 0.0f)};
REQUIRE(flow.spacing() == Approx((1.05f*nozzle_diameter) - layer_height * (1.0 - PI / 4.0)));
}
THEN("Internal perimeter flow has a default spacing fixed to 1.125*nozzle_diameter") {
Flow flow {Flow::new_from_config_width(frPerimeter, ConfigOptionFloatOrPercent{0, false}, nozzle_diameter, layer_height, 0.0f)};
REQUIRE(flow.spacing() == Approx((1.125*nozzle_diameter) - layer_height * (1.0 - PI / 4.0)));
}
THEN("Spacing for supplied width is 0.8927f") {
Flow flow {Flow::new_from_config_width(frExternalPerimeter, width, nozzle_diameter, layer_height, 0.0f)};
REQUIRE(flow.spacing() == Approx(width - layer_height * (1.0 - PI / 4.0)));
flow = Flow::new_from_config_width(frPerimeter, width, nozzle_diameter, layer_height, 0.0f);
REQUIRE(flow.spacing() == Approx(width - layer_height * (1.0 - PI / 4.0)));
}
}
/// Check the min/max
GIVEN("Nozzle Diameter of 0.25 with extreme width") {
float nozzle_diameter {0.25f};
float layer_height {0.5f};
WHEN("layer height is set to 0.15") {
layer_height = 5.f;
THEN("Max width is respected.") {
auto flow {Flow::new_from_config_width(frPerimeter, ConfigOptionFloatOrPercent{0, false}, nozzle_diameter, layer_height, 0.0f)};
REQUIRE(flow.width <= Approx(1.4*nozzle_diameter));
}
THEN("Min width is respected") {
auto flow{ Flow::new_from_config_width(frPerimeter, ConfigOptionFloatOrPercent{0, false}, nozzle_diameter, layer_height, 0.0f) };
REQUIRE(flow.width >= Approx(1.05*nozzle_diameter));
}
}
WHEN("Layer height is set to 0.3") {
layer_height = 0.01f;
THEN("Max width is respected.") {
auto flow{ Flow::new_from_config_width(frPerimeter, ConfigOptionFloatOrPercent{0, false}, nozzle_diameter, layer_height, 0.0f) };
REQUIRE(flow.width <= Approx(1.4*nozzle_diameter));
}
THEN("Min width is respected.") {
auto flow{ Flow::new_from_config_width(frPerimeter, ConfigOptionFloatOrPercent{0, false}, nozzle_diameter, layer_height, 0.0f) };
REQUIRE(flow.width >= Approx(1.05*nozzle_diameter));
}
}
}
///// Check for an edge case in the maths where the spacing could be 0; original
///// math is 0.99. Slic3r issue #4654
//GIVEN("Input spacing of 0.414159 and a total width of 2") {
// double in_spacing = 0.414159;
// double total_width = 2.0;
// auto flow {Flow::new_from_spacing(1.0, 0.4, 0.3, false)};
// WHEN("solid_spacing() is called") {
// double result = flow.solid_spacing(total_width, in_spacing);
// THEN("Yielded spacing is greater than 0") {
// REQUIRE(result > 0);
// }
// }
//}
}
/// Spacing, width calculation for bridge extrusions
SCENARIO("Flow: Flow math for bridges", "[!mayfail]") {
GIVEN("Nozzle Diameter of 0.4, a desired width of 1mm and layer height of 0.5") {
auto width {ConfigOptionFloatOrPercent{1.0, false}};
auto spacing {0.4};
auto nozzle_diameter {0.4};
auto bridge_flow {1.0};
auto layer_height {0.5};
WHEN("Flow role is frExternalPerimeter") {
auto flow {Flow::new_from_config_width(frExternalPerimeter, width, nozzle_diameter, layer_height, bridge_flow)};
THEN("Bridge width is same as nozzle diameter") {
REQUIRE(flow.width == Approx(nozzle_diameter));
}
THEN("Bridge spacing is same as nozzle diameter + BRIDGE_EXTRA_SPACING_MULT * nozzle_diameter") {
REQUIRE(flow.spacing() == Approx(nozzle_diameter + BRIDGE_EXTRA_SPACING_MULT * nozzle_diameter));
}
}
WHEN("Flow role is frInfill") {
auto flow {Flow::new_from_config_width(frInfill, width, nozzle_diameter, layer_height, bridge_flow)};
THEN("Bridge width is same as nozzle diameter") {
REQUIRE(flow.width == Approx(nozzle_diameter));
}
THEN("Bridge spacing is same as nozzle diameter + BRIDGE_EXTRA_SPACING_MULT * nozzle_diameter") {
REQUIRE(flow.spacing() == Approx(nozzle_diameter + BRIDGE_EXTRA_SPACING_MULT * nozzle_diameter));
}
}
WHEN("Flow role is frPerimeter") {
auto flow {Flow::new_from_config_width(frPerimeter, width, nozzle_diameter, layer_height, bridge_flow)};
THEN("Bridge width is same as nozzle diameter") {
REQUIRE(flow.width == Approx(nozzle_diameter));
}
THEN("Bridge spacing is same as nozzle diameter + BRIDGE_EXTRA_SPACING_MULT * nozzle_diameter") {
REQUIRE(flow.spacing() == Approx(nozzle_diameter + BRIDGE_EXTRA_SPACING_MULT * nozzle_diameter));
}
}
WHEN("Flow role is frSupportMaterial") {
auto flow {Flow::new_from_config_width(frSupportMaterial, width, nozzle_diameter, layer_height, bridge_flow)};
THEN("Bridge width is same as nozzle diameter") {
REQUIRE(flow.width == Approx(nozzle_diameter));
}
THEN("Bridge spacing is same as nozzle diameter + BRIDGE_EXTRA_SPACING_MULT * nozzle_diameter") {
REQUIRE(flow.spacing() == Approx(nozzle_diameter + BRIDGE_EXTRA_SPACING_MULT * nozzle_diameter));
}
}
}
}

View File

@ -0,0 +1,146 @@
//#define CATCH_CONFIG_DISABLE
#include <catch2/catch.hpp>
#include <memory>
#include "libslic3r/GCodeWriter.hpp"
#include "test_data.hpp"
#include <iomanip>
#include <iostream>
//#include "test_data.hpp" // get access to init_print, etc
#define PRECISION(val, precision) std::fixed << std::setprecision(precision) << val
#define XYZF_NUM(val) PRECISION(val, 3)
using namespace Slic3r;
using namespace std::literals::string_literals;
// can't understand what the test want to test: here we are overflowing the double capacity to break the lift logic...
//so i moved m_lifted = 0; out of the test in unlift to make that pass.
SCENARIO("lift() and unlift() behavior with large values of Z", "[!shouldfail]") {
GIVEN("A config from a file and a single extruder.") {
GCodeWriter writer{};
GCodeConfig& config {writer.config};
config.set_defaults();
config.load(std::string{ TEST_DATA_DIR PATH_SEPARATOR } +"test_gcodewriter/config_lift_unlift.ini"s);
std::vector<uint16_t> extruder_ids {0};
writer.set_extruders(extruder_ids);
writer.set_tool(0);
WHEN("Z is set to 9007199254740992") {
double trouble_Z{ 9007199254740992 };
writer.travel_to_z(trouble_Z);
AND_WHEN("GcodeWriter::Lift() is called") {
const std::string lift = writer.lift();
REQUIRE(lift.size() > 0);
AND_WHEN("Z is moved post-lift to the same delta as the config Z lift") {
REQUIRE(writer.travel_to_z(trouble_Z + config.retract_lift.values[0]).size() == 0);
AND_WHEN("GCodeWriter::Unlift() is called") {
const std::string unlift = writer.unlift();
REQUIRE(unlift.size() == 0); // we're the same height so no additional move happens.
THEN("GCodeWriter::Lift() emits gcode.") {
const std::string lift_again = writer.lift();
REQUIRE(lift_again.size() > 0);
}
}
}
}
}
}
}
SCENARIO("lift() is not ignored after unlift() at normal values of Z") {
GIVEN("A config from a file and a single extruder.") {
GCodeWriter writer{};
GCodeConfig& config {writer.config};
config.set_defaults();
config.load(std::string{ TEST_DATA_DIR PATH_SEPARATOR } +"test_gcodewriter/config_lift_unlift.ini"s);
std::vector<uint16_t> extruder_ids {0};
writer.set_extruders(extruder_ids);
writer.set_tool(0);
WHEN("Z is set to 203") {
double trouble_Z{ 203 };
writer.travel_to_z(trouble_Z);
AND_WHEN("GcodeWriter::Lift() is called") {
REQUIRE(writer.lift().size() > 0);
AND_WHEN("Z is moved post-lift to the same delta as the config Z lift") {
REQUIRE(writer.travel_to_z(trouble_Z + config.retract_lift.values[0]).size() == 0);
AND_WHEN("GCodeWriter::Unlift() is called") {
REQUIRE(writer.unlift().size() == 0); // we're the same height so no additional move happens.
THEN("GCodeWriter::Lift() emits gcode.") {
REQUIRE(writer.lift().size() > 0);
}
}
}
}
}
WHEN("Z is set to 500003") {
double trouble_Z{ 500003 };
writer.travel_to_z(trouble_Z);
AND_WHEN("GcodeWriter::Lift() is called") {
REQUIRE(writer.lift().size() > 0);
AND_WHEN("Z is moved post-lift to the same delta as the config Z lift") {
REQUIRE(writer.travel_to_z(trouble_Z + config.retract_lift.values[0]).size() == 0);
AND_WHEN("GCodeWriter::Unlift() is called") {
REQUIRE(writer.unlift().size() == 0); // we're the same height so no additional move happens.
THEN("GCodeWriter::Lift() emits gcode.") {
REQUIRE(writer.lift().size() > 0);
}
}
}
}
}
WHEN("Z is set to 10.3") {
double trouble_Z{ 10.3 };
writer.travel_to_z(trouble_Z);
AND_WHEN("GcodeWriter::Lift() is called") {
REQUIRE(writer.lift().size() > 0);
AND_WHEN("Z is moved post-lift to the same delta as the config Z lift") {
REQUIRE(writer.travel_to_z(trouble_Z + config.retract_lift.values[0]).size() == 0);
AND_WHEN("GCodeWriter::Unlift() is called") {
REQUIRE(writer.unlift().size() == 0); // we're the same height so no additional move happens.
THEN("GCodeWriter::Lift() emits gcode.") {
REQUIRE(writer.lift().size() > 0);
}
}
}
}
}
}
}
SCENARIO("set_speed emits values with fixed-point output.") {
GIVEN("GCodeWriter instance") {
GCodeWriter writer;
//max in assert is 100k
//WHEN("set_speed is called to set speed to 1.09321e+06") {
// THEN("Output string is G1 F1093210.000") {
// REQUIRE_THAT(writer.set_speed(1.09321e+06), Catch::Equals("G1 F1093210.000\n"));
// }
//}
WHEN("set_speed is called to set speed to 9.99321e+04") {
THEN("Output string is G1 F99932.100") {
REQUIRE_THAT(writer.set_speed(9.99321e+04), Catch::Equals("G1 F10932.100\n"));
}
}
WHEN("set_speed is called to set speed to 1") {
THEN("Output string is G1 F1.000") {
REQUIRE_THAT(writer.set_speed(1.0), Catch::Equals("G1 F1.000\n"));
}
}
WHEN("set_speed is called to set speed to 203.200022") {
THEN("Output string is G1 F203.200") {
REQUIRE_THAT(writer.set_speed(203.200022), Catch::Equals("G1 F203.200\n"));
}
}
WHEN("set_speed is called to set speed to 203.200522") {
THEN("Output string is G1 F203.200") {
REQUIRE_THAT(writer.set_speed(203.200522), Catch::Equals("G1 F203.201\n"));
}
}
}
}

View File

@ -0,0 +1,379 @@
#define CATCH_CONFIG_DISABLE
#include <catch2/catch.hpp>
#include <libslic3r/Point.hpp>
#include <libslic3r/BoundingBox.hpp>
#include <libslic3r/Polygon.hpp>
#include <libslic3r/Polyline.hpp>
#include <libslic3r/Line.hpp>
#include <libslic3r/Geometry.hpp>
#include <libslic3r/ClipperUtils.hpp>
using namespace Slic3r;
TEST_CASE("Polygon::contains works properly", ""){
// this test was failing on Windows (GH #1950)
Polygon polygon{ Points{
Point{207802834,-57084522},
Point{196528149,-37556190},
Point{173626821,-25420928},
Point{171285751,-21366123},
Point{118673592,-21366123},
Point{116332562,-25420928},
Point{93431208,-37556191},
Point{82156517,-57084523},
Point{129714478,-84542120},
Point{160244873,-84542120}
} };
Point point{ 95706562, -57294774 };
REQUIRE(polygon.contains(point));
}
SCENARIO("Intersections of line segments"){
GIVEN("Integer coordinates"){
Line line1{ Point{5,15},Point{30,15} };
Line line2{ Point{10,20}, Point{10,10} };
THEN("The intersection is valid"){
Point point;
line1.intersection(line2,&point);
REQUIRE(Point{ 10,15 } == point);
}
}
GIVEN("Scaled coordinates"){
Line line1{ Point{73.6310778185108 / 0.0000001, 371.74239268924 / 0.0000001}, Point{73.6310778185108 / 0.0000001, 501.74239268924 / 0.0000001} };
Line line2{ Point{75 / 0.0000001, 437.9853 / 0.0000001}, Point{62.7484 / 0.0000001, 440.4223 / 0.0000001} };
THEN("There is still an intersection"){
Point point;
REQUIRE(line1.intersection(line2,&point));
}
}
}
/*
Tests for unused methods still written in perl
{
my $polygon = Slic3r::Polygon->new(
[45919000, 515273900], [14726100, 461246400], [14726100, 348753500], [33988700, 315389800],
[43749700, 343843000], [45422300, 352251500], [52362100, 362637800], [62748400, 369577600],
[75000000, 372014700], [87251500, 369577600], [97637800, 362637800], [104577600, 352251500],
[107014700, 340000000], [104577600, 327748400], [97637800, 317362100], [87251500, 310422300],
[82789200, 309534700], [69846100, 294726100], [254081000, 294726100], [285273900, 348753500],
[285273900, 461246400], [254081000, 515273900],
);
# this points belongs to $polyline
# note: it's actually a vertex, while we should better check an intermediate point
my $point = Slic3r::Point->new(104577600, 327748400);
local $Slic3r::Geometry::epsilon = 1E-5;
is_deeply Slic3r::Geometry::polygon_segment_having_point($polygon, $point)->pp,
[ [107014700, 340000000], [104577600, 327748400] ],
'polygon_segment_having_point';
}
{
auto point = Point{736310778.185108, 5017423926.8924};
auto line = Line(Point{(long int} 627484000, (long int) 3695776000), Point{(long int} 750000000, (long int)3720147000));
//is Slic3r::Geometry::point_in_segment($point, $line), 0, 'point_in_segment';
}
// Possible to delete
{
//my $p1 = [10, 10];
//my $p2 = [10, 20];
//my $p3 = [10, 30];
//my $p4 = [20, 20];
//my $p5 = [0, 20];
THEN("Points in a line give the correct angles"){
//is Slic3r::Geometry::angle3points($p2, $p3, $p1), PI(), 'angle3points';
//is Slic3r::Geometry::angle3points($p2, $p1, $p3), PI(), 'angle3points';
}
THEN("Left turns give the correct angle"){
//is Slic3r::Geometry::angle3points($p2, $p4, $p3), PI()/2, 'angle3points';
//is Slic3r::Geometry::angle3points($p2, $p1, $p4), PI()/2, 'angle3points';
}
THEN("Right turns give the correct angle"){
//is Slic3r::Geometry::angle3points($p2, $p3, $p4), PI()/2*3, 'angle3points';
//is Slic3r::Geometry::angle3points($p2, $p1, $p5), PI()/2*3, 'angle3points';
}
//my $p1 = [30, 30];
//my $p2 = [20, 20];
//my $p3 = [10, 10];
//my $p4 = [30, 10];
//is Slic3r::Geometry::angle3points($p2, $p1, $p3), PI(), 'angle3points';
//is Slic3r::Geometry::angle3points($p2, $p1, $p4), PI()/2*3, 'angle3points';
//is Slic3r::Geometry::angle3points($p2, $p1, $p1), 2*PI(), 'angle3points';
}
SCENARIO("polygon_is_convex works"){
GIVEN("A square of dimension 10"){
//my $cw_square = [ [0,0], [0,10], [10,10], [10,0] ];
THEN("It is not convex clockwise"){
//is polygon_is_convex($cw_square), 0, 'cw square is not convex';
}
THEN("It is convex counter-clockwise"){
//is polygon_is_convex([ reverse @$cw_square ]), 1, 'ccw square is convex';
}
}
GIVEN("A concave polygon"){
//my $convex1 = [ [0,0], [10,0], [10,10], [0,10], [0,6], [4,6], [4,4], [0,4] ];
THEN("It is concave"){
//is polygon_is_convex($convex1), 0, 'concave polygon';
}
}
}*/
TEST_CASE("Creating a polyline generates the obvious lines"){
auto polyline = Polyline();
polyline.points = std::vector<Point>({Point{0, 0}, Point{10, 0}, Point{20, 0}});
REQUIRE(polyline.lines().at(0).a == Point{0,0});
REQUIRE(polyline.lines().at(0).b == Point{10,0});
REQUIRE(polyline.lines().at(1).a == Point{10,0});
REQUIRE(polyline.lines().at(1).b == Point{20,0});
}
TEST_CASE("Splitting a Polygon generates a polyline correctly"){
auto polygon = Polygon(std::vector<Point>({Point{0, 0}, Point{10, 0}, Point{5, 5}}));
auto split = polygon.split_at_index(1);
REQUIRE(split.points[0]==Point{10,0});
REQUIRE(split.points[1]==Point{5,5});
REQUIRE(split.points[2]==Point{0,0});
REQUIRE(split.points[3]==Point{10,0});
}
TEST_CASE("Bounding boxes are scaled appropriately"){
auto bb = BoundingBox(std::vector<Point>({Point{0, 1}, Point{10, 2}, Point{20, 2}}));
bb.scale(2);
REQUIRE(bb.min == Point{0,2});
REQUIRE(bb.max == Point{40,4});
}
TEST_CASE("Offseting a line generates a polygon correctly"){
Polyline tmp({ Point{10,10}, Point{20,10} });
Polygon area = offset(tmp,5).at(0);
REQUIRE(area.area() == Polygon(std::vector<Point>({Point{10,5},Point{20,5},Point{20,15},Point{10,15}})).area());
}
SCENARIO("Circle Fit, TaubinFit with Newton's method") {
GIVEN("A vector of Pointfs arranged in a half-circle with approximately the same distance R from some point") {
Vec2d expected_center(-6, 0);
Pointfs sample {Vec2d{6.0, 0}, Vec2d{5.1961524, 3}, Vec2d{3 ,5.1961524}, Vec2d{0, 6.0}, Vec2d{-3, 5.1961524}, Vec2d{-5.1961524, 3}, Vec2d{-6.0, 0}};
std::transform(sample.begin(), sample.end(), sample.begin(), [expected_center] (const Vec2d& a) { return a + expected_center;});
WHEN("Circle fit is called on the entire array") {
Vec2d result_center(0,0);
result_center = Geometry::circle_taubin_newton(sample);
THEN("A center point of -6,0 is returned.") {
REQUIRE(result_center == expected_center);
}
}
WHEN("Circle fit is called on the first four points") {
Vec2d result_center(0,0);
result_center = Geometry::circle_taubin_newton(sample.cbegin(), sample.cbegin()+4);
THEN("A center point of -6,0 is returned.") {
REQUIRE(result_center == expected_center);
}
}
WHEN("Circle fit is called on the middle four points") {
Vec2d result_center(0,0);
result_center = Geometry::circle_taubin_newton(sample.cbegin()+2, sample.cbegin()+6);
THEN("A center point of -6,0 is returned.") {
REQUIRE(result_center == expected_center);
}
}
}
GIVEN("A vector of Pointfs arranged in a half-circle with approximately the same distance R from some point") {
Vec2d expected_center(-3, 9);
Pointfs sample {Vec2d{6.0, 0}, Vec2d{5.1961524, 3}, Vec2d{3 ,5.1961524},
Vec2d{0, 6.0},
Vec2d{3, 5.1961524}, Vec2d{-5.1961524, 3}, Vec2d{-6.0, 0}};
std::transform(sample.begin(), sample.end(), sample.begin(), [expected_center] (const Vec2d& a) { return a + expected_center;});
WHEN("Circle fit is called on the entire array") {
Vec2d result_center(0,0);
result_center = Geometry::circle_taubin_newton(sample);
THEN("A center point of 3,9 is returned.") {
REQUIRE(result_center == expected_center);
}
}
WHEN("Circle fit is called on the first four points") {
Vec2d result_center(0,0);
result_center = Geometry::circle_taubin_newton(sample.cbegin(), sample.cbegin()+4);
THEN("A center point of 3,9 is returned.") {
REQUIRE(result_center == expected_center);
}
}
WHEN("Circle fit is called on the middle four points") {
Vec2d result_center(0,0);
result_center = Geometry::circle_taubin_newton(sample.cbegin()+2, sample.cbegin()+6);
THEN("A center point of 3,9 is returned.") {
REQUIRE(result_center == expected_center);
}
}
}
GIVEN("A vector of Points arranged in a half-circle with approximately the same distance R from some point") {
Point expected_center { Point::new_scale(-3, 9)};
Points sample {Point::new_scale(6.0, 0), Point::new_scale(5.1961524, 3), Point::new_scale(3 ,5.1961524),
Point::new_scale(0, 6.0),
Point::new_scale(3, 5.1961524), Point::new_scale(-5.1961524, 3), Point::new_scale(-6.0, 0)};
std::transform(sample.begin(), sample.end(), sample.begin(), [expected_center] (const Point& a) { return a + expected_center;});
WHEN("Circle fit is called on the entire array") {
Point result_center(0,0);
result_center = Geometry::circle_taubin_newton(sample);
THEN("A center point of scaled 3,9 is returned.") {
REQUIRE(result_center.coincides_with_epsilon(expected_center));
}
}
WHEN("Circle fit is called on the first four points") {
Point result_center(0,0);
result_center = Geometry::circle_taubin_newton(sample.cbegin(), sample.cbegin()+4);
THEN("A center point of scaled 3,9 is returned.") {
REQUIRE(result_center.coincides_with_epsilon(expected_center));
}
}
WHEN("Circle fit is called on the middle four points") {
Point result_center(0,0);
result_center = Geometry::circle_taubin_newton(sample.cbegin()+2, sample.cbegin()+6);
THEN("A center point of scaled 3,9 is returned.") {
REQUIRE(result_center.coincides_with_epsilon(expected_center));
}
}
}
}
// A PU
//TEST_CASE("Chained path working correctly"){
// // if chained_path() works correctly, these points should be joined with no diagonal paths
// // (thus 26 units long)
// std::vector<Point> points = {Point{26,26},Point{52,26},Point{0,26},Point{26,52},Point{26,0},Point{0,52},Point{52,52},Point{52,0}};
// std::vector<Points::size_type> indices;
// Geometry::chained_path(points,indices);
// for(Points::size_type i = 0; i < indices.size()-1;i++){
// double dist = points.at(indices.at(i)).distance_to(points.at(indices.at(i+1)));
// REQUIRE(abs(dist-26) <= EPSILON);
// }
//}
SCENARIO("Line distances"){
GIVEN("A line"){
Line line{ Point{0, 0}, Point{20, 0} };
THEN("Points on the line segment have 0 distance"){
REQUIRE(Point{0, 0}.distance_to(line) == 0);
REQUIRE(Point{20, 0}.distance_to(line) == 0);
REQUIRE(Point{10, 0}.distance_to(line) == 0);
}
THEN("Points off the line have the appropriate distance"){
REQUIRE(Point{10, 10}.distance_to(line) == 10);
REQUIRE(Point{50, 0}.distance_to(line) == 30);
}
}
}
SCENARIO("Polygon convex/concave detection"){
GIVEN(("A Square with dimension 100")){
Polygon square/*new_scale*/{ std::vector<Point>{
Point{100,100},
Point{200,100},
Point{200,200},
Point{100,200}}};
THEN("It has 4 convex points counterclockwise"){
REQUIRE(square.concave_points(PI*4/3).size() == 0);
REQUIRE(square.convex_points(PI*2/3).size() == 4);
}
THEN("It has 4 concave points clockwise"){
square.make_clockwise();
REQUIRE(square.concave_points(PI*4/3).size() == 4);
REQUIRE(square.convex_points(PI*2/3).size() == 0);
}
}
GIVEN("A Square with an extra colinearvertex"){
Polygon square /*new_scale*/{ std::vector<Point>{
Point{150,100},
Point{200,100},
Point{200,200},
Point{100,200},
Point{100,100}} };
THEN("It has 4 convex points counterclockwise"){
REQUIRE(square.concave_points(PI*4/3).size() == 0);
REQUIRE(square.convex_points(PI*2/3).size() == 4);
}
}
GIVEN("A Square with an extra collinear vertex in different order"){
Polygon square = Polygon /*new_scale*/{ std::vector<Point>{
Point{200,200},
Point{100,200},
Point{100,100},
Point{150,100},
Point{200,100}} };
THEN("It has 4 convex points counterclockwise"){
REQUIRE(square.concave_points(PI*4/3).size() == 0);
REQUIRE(square.convex_points(PI*2/3).size() == 4);
}
}
GIVEN("A triangle"){
Polygon triangle{ std::vector<Point>{
Point{16000170,26257364},
Point{714223,461012},
Point{31286371,461008}
} };
THEN("it has three convex vertices"){
REQUIRE(triangle.concave_points(PI*4/3).size() == 0);
REQUIRE(triangle.convex_points(PI*2/3).size() == 3);
}
}
GIVEN("A triangle with an extra collinear point"){
Polygon triangle{ std::vector<Point>{
Point{16000170,26257364},
Point{714223,461012},
Point{20000000,461012},
Point{31286371,461012}
} };
THEN("it has three convex vertices"){
REQUIRE(triangle.concave_points(PI*4/3).size() == 0);
REQUIRE(triangle.convex_points(PI*2/3).size() == 3);
}
}
GIVEN("A polygon with concave vertices with angles of specifically 4/3pi"){
// Two concave vertices of this polygon have angle = PI*4/3, so this test fails
// if epsilon is not used.
Polygon polygon{ std::vector<Point>{
Point{60246458,14802768},Point{64477191,12360001},
Point{63727343,11060995},Point{64086449,10853608},
Point{66393722,14850069},Point{66034704,15057334},
Point{65284646,13758387},Point{61053864,16200839},
Point{69200258,30310849},Point{62172547,42483120},
Point{61137680,41850279},Point{67799985,30310848},
Point{51399866,1905506},Point{38092663,1905506},
Point{38092663,692699},Point{52100125,692699}
} };
THEN("the correct number of points are detected"){
REQUIRE(polygon.concave_points(PI*4/3).size() == 6);
REQUIRE(polygon.convex_points(PI*2/3).size() == 10);
}
}
}
TEST_CASE("Triangle Simplification does not result in less than 3 points"){
Polygon triangle{ std::vector<Point>{
Point{16000170,26257364}, Point{714223,461012}, Point{31286371,461008}
} };
REQUIRE(triangle.simplify(250000).at(0).points.size() == 3);
}

View File

@ -0,0 +1,78 @@
//#define CATCH_CONFIG_DISABLE
#include <catch2/catch.hpp>
#include <libslic3r/Config.hpp>
#include <libslic3r/Model.hpp>
#include "test_data.hpp" // get access to init_print, etc
using namespace Slic3r;
using namespace Slic3r::Test;
SCENARIO("Model construction") {
GIVEN("A Slic3r Model") {
Model model{};
TriangleMesh sample_mesh = make_cube(20,20,20);
sample_mesh.repair();
DynamicPrintConfig &config = Slic3r::DynamicPrintConfig::full_print_config();
Slic3r::Print print{};
print.apply(model, config);
//Slic3r::Test::init_print(print, { sample_mesh }, model, config);
WHEN("Model object is added") {
ModelObject* mo = model.add_object();
mo->name = "cube20";
THEN("Model object list == 1") {
REQUIRE(model.objects.size() == 1);
}
mo->add_volume(sample_mesh, false);
THEN("Model volume list == 1") {
REQUIRE(mo->volumes.size() == 1);
}
THEN("Model volume modifier is false") {
REQUIRE(mo->volumes.front()->is_modifier() == false);
}
THEN("Mesh is equivalent to input mesh.") {
TriangleMesh trimesh = mo->volumes.front()->mesh();
REQUIRE(sample_mesh.vertices() == trimesh.vertices());
}
ModelInstance* inst = mo->add_instance();
inst->set_rotation(Vec3d(0,0,0));
inst->set_scaling_factor(Vec3d(1, 1, 1));
model.arrange_objects(print.config().min_object_distance());
model.center_instances_around_point(Slic3r::Vec2d(100,100));
print.auto_assign_extruders(mo);
//print.add_model_object(mo);
print.apply(model, config);
print.validate();
THEN("Print works?") {
std::string gcode_filepath{ "" };
Slic3r::Test::gcode(gcode_filepath, print);
std::cout << "gcode generation done\n";
std::string gcode_from_file = read_to_string(gcode_filepath);
REQUIRE(gcode_from_file.size() > 0);
clean_file(gcode_filepath, "gcode");
}
}
}
}
SCENARIO("xy compensations"){
GIVEN(("A Square with a complex hole inside")){
Slic3r::Polygon square/*new_scale*/{ std::vector<Point>{
Point{ 100, 100 },
Point{ 200, 100 },
Point{ 200, 200 },
Point{ 100, 200 }} };
THEN("elephant and xy can compensate each other"){
//TODO
}
THEN("hole and xy can compensate each othere"){
}
}
}

View File

@ -0,0 +1,176 @@
//#define CATCH_CONFIG_DISABLE
#include <catch_main.hpp>
#include "test_data.hpp"
#include <libslic3r/libslic3r.h>
#include <libslic3r/SVG.hpp>
#include <libslic3r/config.hpp>
#include <string>
using namespace Slic3r;
using namespace Slic3r::Test;
using namespace std::literals;
SCENARIO("PrintObject: Perimeter generation") {
GIVEN("20mm cube and default config & 0.3 layer height") {
DynamicPrintConfig &config = Slic3r::DynamicPrintConfig::full_print_config();
TestMesh m = TestMesh::cube_20x20x20;
Model model{};
config.set_key_value("fill_density", new ConfigOptionPercent(0));
config.set_deserialize("nozzle_diameter", "0.4");
config.set_deserialize("layer_height", "0.3");
WHEN("make_perimeters() is called") {
Print print{};
Slic3r::Test::init_print(print, { m }, model, &config);
PrintObject& object = *(print.objects().at(0));
print.process();
// there are 66.66666.... layers for 0.3mm in 20mm
//slic3r is rounded (slice at half-layer), slic3rPE is less?
//TODO: check the slic32r why it's not cut at half-layer
THEN("67 layers exist in the model") {
REQUIRE(object.layers().size() == 67);
}
THEN("Every layer in region 0 has 1 island of perimeters") {
for(Layer* layer : object.layers()) {
REQUIRE(layer->regions()[0]->perimeters.entities.size() == 1);
}
}
THEN("Every layer (but top) in region 0 has 3 paths in its perimeters list.") {
LayerPtrs layers = object.layers();
for (auto it_layer = layers.begin(); it_layer != layers.end() - 1; ++it_layer) {
REQUIRE((*it_layer)->regions()[0]->perimeters.items_count() == 3);
}
}
THEN("Top layer in region 0 has 1 path in its perimeters list (only 1 perimeter on top).") {
REQUIRE(object.layers().back()->regions()[0]->perimeters.items_count() == 1);
}
}
}
}
SCENARIO("Print: Skirt generation") {
GIVEN("20mm cube and default config") {
DynamicPrintConfig &config = Slic3r::DynamicPrintConfig::full_print_config();
TestMesh m = TestMesh::cube_20x20x20;
Slic3r::Model model{};
config.set_key_value("skirt_height", new ConfigOptionInt(1));
config.set_key_value("skirt_distance", new ConfigOptionFloat(1));
WHEN("Skirts is set to 2 loops") {
config.set_key_value("skirts", new ConfigOptionInt(2));
Print print{};
Slic3r::Test::init_print(print, { m }, model, &config);
print.process();
THEN("Skirt Extrusion collection has 2 loops in it") {
REQUIRE(print.skirt().items_count() == 2);
REQUIRE(print.skirt().flatten().entities.size() == 2);
}
}
}
}
void test_is_solid_infill(Print &p, size_t obj_id, size_t layer_id ) {
const PrintObject& obj { *(p.objects().at(obj_id)) };
const Layer& layer { *(obj.get_layer((int)layer_id)) };
// iterate over all of the regions in the layer
for (const LayerRegion* reg : layer.regions()) {
// for each region, iterate over the fill surfaces
for (const Surface& s : reg->fill_surfaces.surfaces) {
CHECK(s.has_fill_solid());
}
}
}
SCENARIO("Print: Changing number of solid surfaces does not cause all surfaces to become internal.") {
GIVEN("sliced 20mm cube and config with top_solid_surfaces = 2 and bottom_solid_surfaces = 1") {
DynamicPrintConfig &config = Slic3r::DynamicPrintConfig::full_print_config();
TestMesh m { TestMesh::cube_20x20x20 };
config.set_key_value("top_solid_layers", new ConfigOptionInt(2));
config.set_key_value("bottom_solid_layers", new ConfigOptionInt(1));
config.set_key_value("layer_height", new ConfigOptionFloat(0.5)); // get a known number of layers
config.set_key_value("first_layer_height", new ConfigOptionFloatOrPercent(0.5, false));
config.set_key_value("enforce_full_fill_volume", new ConfigOptionBool(true));
Slic3r::Model model;
auto event_counter {0U};
std::string stage;
Print print{};
Slic3r::Test::init_print(print, { m }, model, &config);
print.process();
// Precondition: Ensure that the model has 2 solid top layers (39, 38)
// and one solid bottom layer (0).
test_is_solid_infill(print, 0, 0); // should be solid
test_is_solid_infill(print, 0, 39); // should be solid
test_is_solid_infill(print, 0, 38); // should be solid
WHEN("Model is re-sliced with top_solid_layers == 3") {
((ConfigOptionInt&)(print.regions()[0]->config().top_solid_layers)).value = 3;
print.invalidate_state_by_config_options(std::vector<Slic3r::t_config_option_key>{ "posPrepareInfill" });
print.process();
THEN("Print object does not have 0 solid bottom layers.") {
test_is_solid_infill(print, 0, 0);
}
AND_THEN("Print object has 3 top solid layers") {
test_is_solid_infill(print, 0, 39);
test_is_solid_infill(print, 0, 38);
test_is_solid_infill(print, 0, 37);
}
}
}
}
SCENARIO("Print: Brim generation") {
GIVEN("20mm cube and default config, 1mm first layer width") {
DynamicPrintConfig &config = Slic3r::DynamicPrintConfig::full_print_config();
TestMesh m{ TestMesh::cube_20x20x20 };
Slic3r::Model model{};
config.set_key_value("first_layer_extrusion_width", new ConfigOptionFloatOrPercent(1, false));
WHEN("Brim is set to 3mm") {
config.set_key_value("brim_width", new ConfigOptionFloat(3));
Print print{};
Slic3r::Test::init_print(print, { m }, model, &config);
print.process();
//{
// std::stringstream stri;
// stri << "20mm_cube_brim_test_" << ".svg";
// SVG svg(stri.str());
// svg.draw(print.brim().as_polylines(), "red");
// svg.Close();
//}
THEN("Brim Extrusion collection has 3 loops in it") {
REQUIRE(print.brim().items_count() == 3);
}
}
WHEN("Brim is set to 6mm") {
config.set_key_value("brim_width", new ConfigOptionFloat(6));
Print print{};
Slic3r::Test::init_print(print, { m }, model, &config);
print.process();
THEN("Brim Extrusion collection has 6 loops in it") {
REQUIRE(print.brim().items_count() == 6);
}
}
WHEN("Brim is set to 6mm, extrusion width 0.5mm") {
config.set_key_value("brim_width", new ConfigOptionFloat(6));
config.set_key_value("first_layer_extrusion_width", new ConfigOptionFloatOrPercent(0.5, false));
Print print{};
Slic3r::Test::init_print(print, { m }, model, &config);
print.process();
double nbLoops = 6.0 / print.brim_flow(print.extruders().front()).spacing();
THEN("Brim Extrusion collection has " + std::to_string(nbLoops) + " loops in it (flow="+ std::to_string(print.brim_flow(print.extruders().front()).spacing())+")") {
REQUIRE(print.brim().items_count() == floor(nbLoops));
}
}
WHEN("Brim ears activated, 3mm") {
config.set_key_value("brim_width", new ConfigOptionFloat(3));
config.set_key_value("brim_ears", new ConfigOptionBool(true));
Print print{};
Slic3r::Test::init_print(print, { m }, model, &config);
print.process();
THEN("Brim ears Extrusion collection has 4 extrusions in it") {
REQUIRE(print.brim().items_count() == 4);
}
}
}
}

View File

@ -0,0 +1,437 @@
//#define CATCH_CONFIG_DISABLE
#include <catch2/catch.hpp>
#include "test_data.hpp"
#include <libslic3r/GCodeReader.hpp>
using namespace Slic3r::Test;
using namespace Slic3r;
SCENARIO("skirt test by merill", "") {
GIVEN("2 objects, don't complete individual object") {
DynamicPrintConfig &config = Slic3r::DynamicPrintConfig::full_print_config();
// remove noise
config.set_deserialize("top_solid_layers", "0");
config.set_deserialize("bottom_solid_layers", "0");
config.set_deserialize("fill_density", "0");
config.set_deserialize("perimeters", "1");
config.set_deserialize("complete_objects", "0");
config.set_key_value("gcode_comments", new ConfigOptionBool(true));
WHEN("skirt with 3 layers is requested") {
config.set_deserialize("skirts", "1");
config.set_deserialize("skirt_height", "3");
config.set_deserialize("brim_width", "0");
Model model{};
Print print{};
Slic3r::Test::init_print(print, { TestMesh::cube_20x20x20, TestMesh::cube_20x20x20 }, model, &config);
std::map<double, bool> layers_with_skirt;
std::map<double, bool> layers_with_brim;
std::string gcode_filepath{ "" };
Slic3r::Test::gcode(gcode_filepath, print);
auto parser{ Slic3r::GCodeReader() };
parser.parse_file(gcode_filepath, [&layers_with_skirt, &layers_with_brim, &config](Slic3r::GCodeReader& self, const Slic3r::GCodeReader::GCodeLine& line)
{
if (line.extruding(self) && line.comment().find("skirt") != std::string::npos) {
layers_with_skirt[self.z()] = 1;
}
if (line.extruding(self) && line.comment().find("brim") != std::string::npos) {
layers_with_brim[self.z()] = 1;
}
});
clean_file(gcode_filepath, "gcode");
THEN("one skrit generated") {
REQUIRE(print.skirt().entities.size() == 1);
for (auto obj : print.objects()) {
REQUIRE(obj->skirt().entities.size() == 0);
}
}
THEN("brim is not generated") {
REQUIRE(print.brim().entities.size() == 0);
for (auto obj : print.objects()) {
REQUIRE(obj->brim().entities.size() == 0);
}
REQUIRE(layers_with_brim.size() == 0);
}
THEN("skirt_height is honored") {
REQUIRE(layers_with_skirt.size() == (size_t)config.opt_int("skirt_height"));
}
}
WHEN("brim and skirt with 3 layers is requested") {
config.set_deserialize("skirts", "1");
config.set_deserialize("skirt_height", "3");
config.set_deserialize("brim_width", "4");
Model model{};
Print print{};
Slic3r::Test::init_print(print, { TestMesh::cube_20x20x20, TestMesh::cube_20x20x20 }, model, &config);
std::map<double, bool> layers_with_skirt;
std::map<double, bool> layers_with_brim;
std::string gcode_filepath{ "" };
Slic3r::Test::gcode(gcode_filepath, print);
auto parser{ Slic3r::GCodeReader() };
parser.parse_file(gcode_filepath, [&layers_with_skirt, &layers_with_brim, &config](Slic3r::GCodeReader& self, const Slic3r::GCodeReader::GCodeLine& line)
{
if (line.extruding(self) && line.comment().find("skirt") != std::string::npos) {
layers_with_skirt[self.z()] = 1;
}
if (line.extruding(self) && line.comment().find("brim") != std::string::npos) {
layers_with_brim[self.z()] = 1;
}
});
clean_file(gcode_filepath, "gcode");
THEN("one skirt generated") {
REQUIRE(print.skirt().entities.size() == 1);
for (auto obj : print.objects()) {
REQUIRE(obj->skirt().entities.size() == 0);
}
}
THEN("brim generated") {
REQUIRE(print.brim().entities.size() > 0);
for (auto obj : print.objects()) {
REQUIRE(obj->brim().entities.size() == 0);
}
REQUIRE(layers_with_brim.size() == 1);
}
THEN("skirt_height is honored") {
REQUIRE(layers_with_skirt.size() == (size_t)config.opt_int("skirt_height"));
}
}
WHEN("brim is requested") {
config.set_deserialize("skirts", "0");
config.set_deserialize("skirt_height", "3");
config.set_deserialize("brim_width", "4");
Model model{};
Print print{};
Slic3r::Test::init_print(print, { TestMesh::cube_20x20x20, TestMesh::cube_20x20x20 }, model, &config);
std::map<double, bool> layers_with_skirt;
std::map<double, bool> layers_with_brim;
std::string gcode_filepath{ "" };
Slic3r::Test::gcode(gcode_filepath, print);
auto parser{ Slic3r::GCodeReader() };
parser.parse_file(gcode_filepath, [&layers_with_skirt, &layers_with_brim, &config](Slic3r::GCodeReader& self, const Slic3r::GCodeReader::GCodeLine& line)
{
if (line.extruding(self) && line.comment().find("skirt") != std::string::npos) {
layers_with_skirt[self.z()] = 1;
}
if (line.extruding(self) && line.comment().find("brim") != std::string::npos) {
layers_with_brim[self.z()] = 1;
}
});
clean_file(gcode_filepath, "gcode");
THEN("no skirt generated") {
REQUIRE(print.skirt().entities.size() == 0);
for (auto obj : print.objects()) {
REQUIRE(obj->skirt().entities.size() == 0);
}
}
THEN("brim generated") {
REQUIRE(print.brim().entities.size() > 0);
for (auto obj : print.objects()) {
REQUIRE(obj->brim().entities.size() == 0);
}
REQUIRE(layers_with_brim.size() == 1);
}
THEN("skirt_height is honored") {
REQUIRE(layers_with_skirt.size() == 0);
}
}
}
GIVEN("3 objects, complete individual object") {
DynamicPrintConfig &config = Slic3r::DynamicPrintConfig::full_print_config();
// remove noise
config.set_deserialize("top_solid_layers", "0");
config.set_deserialize("bottom_solid_layers", "0");
config.set_deserialize("fill_density", "0");
config.set_deserialize("perimeters", "1");
config.set_deserialize("complete_objects", "1");
config.set_key_value("gcode_comments", new ConfigOptionBool(true));
WHEN("skirt with 3 layers is requested") {
config.set_deserialize("skirts", "1");
config.set_deserialize("skirt_height", "3");
config.set_deserialize("brim_width", "0");
Model model{};
Print print{};
Slic3r::Test::init_print(print, { TestMesh::cube_20x20x20, TestMesh::cube_20x20x20, TestMesh::pyramid }, model, &config);
std::map<double, bool> layers_with_skirt;
std::map<double, bool> layers_with_brim;
std::string gcode_filepath{ "" };
Slic3r::Test::gcode(gcode_filepath, print);
auto parser{ Slic3r::GCodeReader() };
parser.parse_file(gcode_filepath, [&layers_with_skirt, &layers_with_brim, &config](Slic3r::GCodeReader& self, const Slic3r::GCodeReader::GCodeLine& line)
{
if (line.extruding(self) && line.comment().find("skirt") != std::string::npos) {
layers_with_skirt[self.z()] = 1;
}
if (line.extruding(self) && line.comment().find("brim") != std::string::npos) {
layers_with_brim[self.z()] = 1;
}
});
THEN("one skirt per object") {
REQUIRE(print.skirt().entities.size() == 0);
for (auto obj : print.objects()) {
REQUIRE(obj->skirt().entities.size() == 1);
}
}
THEN("brim is not generated") {
REQUIRE(print.brim().entities.size() == 0);
for (auto obj : print.objects()) {
REQUIRE(obj->brim().entities.size() == 0);
}
REQUIRE(layers_with_brim.size() == 0);
}
THEN("skirt_height is honored") {
REQUIRE(layers_with_skirt.size() == (size_t)config.opt_int("skirt_height"));
}
}
WHEN("brim and skirt with 3 layers is requested") {
config.set_deserialize("skirts", "1");
config.set_deserialize("skirt_height", "3");
config.set_deserialize("brim_width", "4");
Model model{};
Print print{};
Slic3r::Test::init_print(print, { TestMesh::cube_20x20x20, TestMesh::cube_20x20x20 }, model, &config);
std::map<double, bool> layers_with_skirt;
std::map<double, bool> layers_with_brim;
std::string gcode_filepath{ "" };
Slic3r::Test::gcode(gcode_filepath, print);
auto parser{ Slic3r::GCodeReader() };
parser.parse_file(gcode_filepath, [&layers_with_skirt, &layers_with_brim, &config](Slic3r::GCodeReader& self, const Slic3r::GCodeReader::GCodeLine& line)
{
if (line.extruding(self) && line.comment().find("skirt") != std::string::npos) {
layers_with_skirt[self.z()] = 1;
}
if (line.extruding(self) && line.comment().find("brim") != std::string::npos) {
layers_with_brim[self.z()] = 1;
}
});
clean_file(gcode_filepath, "gcode");
THEN("one skirt per object") {
REQUIRE(print.skirt().entities.size() == 0);
for (auto obj : print.objects()) {
REQUIRE(obj->skirt().entities.size() == 1);
}
}
THEN("brim generated") {
REQUIRE(print.brim().entities.size() == 0);
for (auto obj : print.objects()) {
REQUIRE(obj->brim().entities.size() > 0);
}
REQUIRE(layers_with_brim.size() == 1);
}
THEN("skirt_height is honored") {
REQUIRE(layers_with_skirt.size() == (size_t)config.opt_int("skirt_height"));
}
}
WHEN("brim is requested") {
config.set_deserialize("skirts", "0");
config.set_deserialize("skirt_height", "3");
config.set_deserialize("brim_width", "4");
Model model{};
Print print{};
Slic3r::Test::init_print(print, { TestMesh::cube_20x20x20, TestMesh::cube_20x20x20 }, model, &config);
std::map<double, bool> layers_with_skirt;
std::map<double, bool> layers_with_brim;
std::string gcode_filepath{ "" };
Slic3r::Test::gcode(gcode_filepath, print);
auto parser{ Slic3r::GCodeReader() };
parser.parse_file(gcode_filepath, [&layers_with_skirt, &layers_with_brim, &config](Slic3r::GCodeReader& self, const Slic3r::GCodeReader::GCodeLine& line)
{
if (line.extruding(self) && line.comment().find("skirt") != std::string::npos) {
layers_with_skirt[self.z()] = 1;
}
if (line.extruding(self) && line.comment().find("brim") != std::string::npos) {
layers_with_brim[self.z()] = 1;
}
});
clean_file(gcode_filepath, "gcode");
THEN("no skrit") {
REQUIRE(print.skirt().entities.size() == 0);
for (auto obj : print.objects()) {
REQUIRE(obj->skirt().entities.size() == 0);
}
}
THEN("brim generated") {
REQUIRE(print.brim().entities.size() == 0);
for (auto obj : print.objects()) {
REQUIRE(obj->brim().entities.size() > 0);
}
REQUIRE(layers_with_brim.size() == 1);
}
THEN("skirt_height is honored") {
REQUIRE(layers_with_skirt.size() == 0);
}
}
}
}
SCENARIO("Original Slic3r Skirt/Brim tests", "[!mayfail]") {
GIVEN("Configuration with a skirt height of 2") {
DynamicPrintConfig &config = Slic3r::DynamicPrintConfig::full_print_config();
config.set_deserialize("skirts", "1");
config.set_deserialize("skirt_height", "2");
config.set_deserialize("perimeters", "1");
config.set_deserialize("support_material_speed", "99");
config.set_key_value("gcode_comments", new ConfigOptionBool(true));
// avoid altering speeds unexpectedly
config.set_deserialize("cooling", "0");
config.set_deserialize("first_layer_speed", "100%");
WHEN("multiple objects are printed") {
auto gcode {std::stringstream("")};
Model model{};
Print print{};
Slic3r::Test::init_print(print, { TestMesh::cube_20x20x20, TestMesh::cube_20x20x20 }, model, &config, false);
std::map<double, bool> layers_with_skirt;
std::string gcode_filepath{ "" };
Slic3r::Test::gcode(gcode_filepath, print);
auto parser {Slic3r::GCodeReader()};
parser.parse_file(gcode_filepath, [&layers_with_skirt, &config] (Slic3r::GCodeReader& self, const Slic3r::GCodeReader::GCodeLine& line)
{
if (line.extruding(self) && line.comment().find("skirt") != std::string::npos) {
layers_with_skirt[self.z()] = 1;
}
//if (self.z() > 0) {
// if (line.extruding(self) && (line.new_F(self) == config.opt_float("support_material_speed") * 60.0) ) {
// layers_with_skirt[self.z()] = 1;
// }
//}
});
clean_file(gcode_filepath, "gcode");
THEN("skirt_height is honored") {
REQUIRE(layers_with_skirt.size() == (size_t)config.opt_int("skirt_height"));
}
}
}
GIVEN("A default configuration") {
DynamicPrintConfig &config = Slic3r::DynamicPrintConfig::full_print_config();
config.set_deserialize("support_material_speed", "99");
// avoid altering speeds unexpectedly
config.set_deserialize("cooling", "0");
config.set_deserialize("first_layer_speed", "100%");
// remove noise from top/solid layers
config.set_deserialize("top_solid_layers", "0");
config.set_deserialize("bottom_solid_layers", "0");
WHEN("Brim width is set to 5") {
config.set_deserialize("perimeters", "0");
config.set_deserialize("skirts", "0");
config.set_deserialize("brim_width", "5");
THEN("Brim is generated") {
Model model{};
Print print{};
Slic3r::Test::init_print(print, { TestMesh::cube_20x20x20 }, model, &config, false);
print.process();
REQUIRE(print.brim().entities.size()>0);
}
}
WHEN("Skirt area is smaller than the brim") {
config.set_deserialize("skirts", "1");
config.set_deserialize("brim_width", "10");
THEN("GCode generates successfully.") {
Model model{};
Print print{};
Slic3r::Test::init_print(print, { TestMesh::cube_20x20x20 }, model, &config, false);
std::string gcode_filepath{ "" };
Slic3r::Test::gcode(gcode_filepath, print);
std::string gcode_from_file = read_to_string(gcode_filepath);
REQUIRE(gcode_from_file.size() > 0);
clean_file(gcode_filepath, "gcode");
}
}
WHEN("Skirt height is 0 and skirts > 0") {
config.set_deserialize("skirts", "2");
config.set_deserialize("skirt_height", "0");
THEN("GCode generates successfully.") {
Model model{};
Print print{};
Slic3r::Test::init_print(print, { TestMesh::cube_20x20x20 }, model, &config, false);
std::string gcode_filepath{ "" };
Slic3r::Test::gcode(gcode_filepath, print);
std::string gcode_from_file = read_to_string(gcode_filepath);
REQUIRE(gcode_from_file.size() > 0);
clean_file(gcode_filepath, "gcode");
}
}
WHEN("Perimeter extruder = 2 and support extruders = 3") {
THEN("Brim is printed with the extruder used for the perimeters of first object") {
REQUIRE(true); //TODO
}
}
WHEN("Perimeter extruder = 2, support extruders = 3, raft is enabled") {
THEN("brim is printed with same extruder as skirt") {
REQUIRE(true); //TODO
}
}
//TODO review this test that fail because "there are no layer" and it test nothing anyway
//WHEN("Object is plated with overhang support and a brim") {
// config.set_deserialize("layer_height", "0.4");
// config.set_deserialize("first_layer_height", "0.4");
// config.set_deserialize("skirts", "1");
// config.set_deserialize("skirt_distance", "0");
// config.set_deserialize("support_material_speed", "99");
// config.set_deserialize("perimeter_extruder", "1");
// config.set_deserialize("support_material_extruder", "2");
// config.set_deserialize("cooling", "0"); // to prevent speeds to be altered
// config.set_deserialize("first_layer_speed", "100%"); // to prevent speeds to be altered
// Slic3r::Model model;
// Print print{};
// Slic3r::Test::init_print(print, { TestMesh::overhang }, model, &config, false);
// print.process();
//
// config.set_deserialize("support_material", "true"); // to prevent speeds to be altered
// THEN("skirt length is large enough to contain object with support") {
// REQUIRE(true); //TODO
// }
//}
WHEN("Large minimum skirt length is used.") {
config.set_deserialize("min_skirt_length", "20");
auto gcode {std::stringstream("")};
Slic3r::Model model;
Print print{};
Slic3r::Test::init_print(print, { TestMesh::cube_20x20x20 }, model, &config, false);
THEN("Gcode generation doesn't crash") {
std::string gcode_filepath{ "" };
Slic3r::Test::gcode(gcode_filepath, print);
std::string gcode_from_file = read_to_string(gcode_filepath);
REQUIRE(gcode_from_file.size() > 0);
clean_file(gcode_filepath, "gcode");
}
}
}
}

View File

@ -0,0 +1,520 @@
//#define CATCH_CONFIG_DISABLE
#include <catch2/catch.hpp>
#include "test_data.hpp"
#include <libslic3r/ClipperUtils.hpp>
#include <libslic3r/MedialAxis.hpp>
#include <libslic3r/SVG.hpp>
#include <libslic3r/GCode.hpp>
using namespace Slic3r;
using namespace Slic3r::Geometry;
using namespace Slic3r::Test;
class ExtrusionVolumeVisitor : public ExtrusionVisitorConst {
double volume = 0;
public:
virtual void use(const ExtrusionPath &path) override {
for (int i = 0; i < path.polyline.size() - 1; i++) volume += path.polyline.points[i].distance_to(path.polyline.points[i + 1]) * path.mm3_per_mm;
};
virtual void use(const ExtrusionPath3D &path3D) override { std::cout << "error, not supported"; };
virtual void use(const ExtrusionMultiPath &multipath) override {
for (const ExtrusionPath &path : multipath.paths) use(path);
}
virtual void use(const ExtrusionMultiPath3D &multipath) override { std::cout << "error, not supported"; };
virtual void use(const ExtrusionLoop &loop) override {
for (const ExtrusionEntity &path : loop.paths) path.visit(*this);
}
virtual void use(const ExtrusionEntityCollection &collection) override {
for (const ExtrusionEntity *path : collection.entities) path->visit(*this);
}
double compute(const ExtrusionEntity &entity) && {
entity.visit(*this);
return volume;
}
};
SCENARIO("extrude_thinwalls") {
GIVEN("ThickLine") {
ExPolygon expolygon;
expolygon.contour = Slic3r::Polygon{ Points{
Point::new_scale(-0.5, 0),
Point::new_scale(0.5, 0),
Point::new_scale(0.3, 10),
Point::new_scale(-0.3, 10) } };
ThickPolylines res;
MedialAxis{ expolygon, scale_(1.1), scale_(0.5), scale_(0.2) }.build(res);
Flow periflow{ 1.1, 0.2, 0.4 };
ExtrusionEntityCollection gap_fill = thin_variable_width(res, erGapFill, periflow);
//std::string gcode = gcodegen.get_visitor_gcode();
THEN("analyse extrusion.") {
ExtrusionVolumeVisitor vis;
std::cout << " volume is " << ExtrusionVolumeVisitor{}.compute(gap_fill) << "\n";
std::cout << " wanted volume is " << ((0.6*0.2 * 10) + (0.2*0.2 * 10)) << "\n";
REQUIRE(std::abs(ExtrusionVolumeVisitor{}.compute(gap_fill) - ((0.6*0.2 * 10) + (0.2*0.2 * 10)))<0.01);
}
}
}
SCENARIO("thin walls: ")
{
GIVEN("Square")
{
Points test_set;
test_set.reserve(4);
Points square {Point::new_scale(100, 100),
Point::new_scale(200, 100),
Point::new_scale(200, 200),
Point::new_scale(100, 200)};
Slic3r::Polygon hole_in_square{ Points{
Point::new_scale(140, 140),
Point::new_scale(140, 160),
Point::new_scale(160, 160),
Point::new_scale(160, 140) } };
ExPolygon expolygon;
expolygon.contour = Slic3r::Polygon{ square };
expolygon.holes = Slic3r::Polygons{ hole_in_square };
WHEN("creating the medial axis"){
Polylines res;
expolygon.medial_axis(scale_(40), scale_(0.5), &res);
THEN("medial axis of a square shape is a single path"){
REQUIRE(res.size() == 1);
}
THEN("polyline forms a closed loop"){
REQUIRE(res[0].first_point().coincides_with(res[0].last_point()) == true);
}
THEN("medial axis loop has reasonable length"){
REQUIRE(res[0].length() > expolygon.holes[0].length());
REQUIRE(res[0].length() < expolygon.contour.length());
}
}
}
GIVEN("narrow rectangle") {
ExPolygon expolygon;
expolygon.contour = Slic3r::Polygon{ Points{
Point::new_scale(100, 100),
Point::new_scale(120, 100),
Point::new_scale(120, 200),
Point::new_scale(100, 200) } };
Polylines res;
expolygon.medial_axis(scale_(20), scale_(0.5), &res);
ExPolygon expolygon2;
expolygon2.contour = Slic3r::Polygon{ Points{
Point::new_scale(100, 100),
Point::new_scale(120, 100),
Point::new_scale(120, 200),
Point::new_scale(105, 200), // extra point in the short side
Point::new_scale(100, 200) } };
Polylines res2;
expolygon.medial_axis(scale_(20), scale_(0.5), &res2);
WHEN("creating the medial axis") {
THEN("medial axis of a narrow rectangle is a single line") {
REQUIRE(res.size() == 1);
THEN("medial axis has reasonable length") {
REQUIRE(res[0].length() >= scale_(200 - 100 - (120 - 100)) - SCALED_EPSILON);
}
}
THEN("medial axis of a narrow rectangle with an extra vertex is still a single line") {
REQUIRE(res2.size() == 1);
THEN("medial axis of a narrow rectangle with an extra vertex has reasonable length") {
REQUIRE(res2[0].length() >= scale_(200 - 100 - (120 - 100)) - SCALED_EPSILON);
}
THEN("extra vertices don't influence medial axis") {
REQUIRE(res2[0].length() - res[0].length() < SCALED_EPSILON);
}
}
}
}
//TODO: compare with mainline slic3r
GIVEN("semicicumference") {
ExPolygon expolygon;
expolygon.contour = Slic3r::Polygon{ Points{
Point{ 1185881, 829367 }, Point{ 1421988, 1578184 }, Point{ 1722442, 2303558 }, Point{ 2084981, 2999998 }, Point{ 2506843, 3662186 }, Point{ 2984809, 4285086 }, Point{ 3515250, 4863959 }, Point{ 4094122, 5394400 }, Point{ 4717018, 5872368 },
Point{ 5379210, 6294226 }, Point{ 6075653, 6656769 }, Point{ 6801033, 6957229 }, Point{ 7549842, 7193328 }, Point{ 8316383, 7363266 }, Point{ 9094809, 7465751 }, Point{ 9879211, 7500000 }, Point{ 10663611, 7465750 }, Point{ 11442038, 7363265 },
Point{ 12208580, 7193327 }, Point{ 12957389, 6957228 }, Point{ 13682769, 6656768 }, Point{ 14379209, 6294227 }, Point{ 15041405, 5872366 }, Point{ 15664297, 5394401 }, Point{ 16243171, 4863960 }, Point{ 16758641, 4301424 }, Point{ 17251579, 3662185 },
Point{ 17673439, 3000000 }, Point{ 18035980, 2303556 }, Point{ 18336441, 1578177 }, Point{ 18572539, 829368 }, Point{ 18750748, 0 }, Point{ 19758422, 0 }, Point{ 19727293, 236479 }, Point{ 19538467, 1088188 }, Point{ 19276136, 1920196 },
Point{ 18942292, 2726179 }, Point{ 18539460, 3499999 }, Point{ 18070731, 4235755 }, Point{ 17539650, 4927877 }, Point{ 16950279, 5571067 }, Point{ 16307090, 6160437 }, Point{ 15614974, 6691519 }, Point{ 14879209, 7160248 }, Point{ 14105392, 7563079 },
Point{ 13299407, 7896927 }, Point{ 12467399, 8159255 }, Point{ 11615691, 8348082 }, Point{ 10750769, 8461952 }, Point{ 9879211, 8500000 }, Point{ 9007652, 8461952 }, Point{ 8142729, 8348082 }, Point{ 7291022, 8159255 }, Point{ 6459015, 7896927 },
Point{ 5653029, 7563079 }, Point{ 4879210, 7160247 }, Point{ 4143447, 6691519 }, Point{ 3451331, 6160437 }, Point{ 2808141, 5571066 }, Point{ 2218773, 4927878 }, Point{ 1687689, 4235755 }, Point{ 1218962, 3499999 }, Point{ 827499, 2748020 },
Point{ 482284, 1920196 }, Point{ 219954, 1088186 }, Point{ 31126, 236479 }, Point{ 0, 0 }, Point{ 1005754, 0 }
} };
WHEN("creating the medial axis") {
Polylines res;
expolygon.medial_axis(scale_(1.324888), scale_(0.25), &res);
THEN("medial axis of a semicircumference is a single line") {
REQUIRE(res.size() == 1);
}
THEN("all medial axis segments of a semicircumference have the same orientation (but the 2 end points)") {
Lines lines = res[0].lines();
double min_angle = 1, max_angle = -1;
//std::cout << "first angle=" << lines[0].ccw(lines[1].b) << "\n";
for (int idx = 1; idx < lines.size() - 1; idx++) {
double angle = lines[idx - 1].ccw(lines[idx].b);
if (std::abs(angle) - EPSILON < 0) angle = 0;
//if (angle < 0) std::cout << unscale_(lines[idx - 1].a.x()) << ":" << unscale_(lines[idx - 1].a.y()) << " -> " << unscale_(lines[idx - 1].b.x()) << ":" << unscale_(lines[idx - 1].b.y()) << " -> " << unscale_(lines[idx].b.x()) << ":" << unscale_(lines[idx].b.y()) << "\n";
std::cout << "angle=" << 180 * lines[idx].a.ccw_angle(lines[idx - 1].a, lines[idx].b) / PI << "\n";
min_angle = std::min(min_angle, angle);
max_angle = std::max(max_angle, angle);
}
//std::cout << "last angle=" << lines[lines.size() - 2].ccw(lines[lines.size() - 1].b) << "\n";
// check whether turns are all CCW or all CW
bool allccw = (min_angle <= 0 && max_angle <= 0);
bool allcw = (min_angle >= 0 && max_angle >= 0);
bool allsame_orientation = allccw || allcw;
REQUIRE(allsame_orientation);
}
}
}
GIVEN("round with large and very small distance between points"){
ExPolygon expolygon;
expolygon.contour = Slic3r::Polygon{ Points{
Point::new_scale(15.181601,-2.389639), Point::new_scale(15.112616,-1.320034), Point::new_scale(14.024491,-0.644338), Point::new_scale(13.978982,-0.624495), Point::new_scale(9.993299,0.855584), Point::new_scale(9.941970,0.871195), Point::new_scale(5.796743,1.872643),
Point::new_scale(5.743826,1.882168), Point::new_scale(1.509170,2.386464), Point::new_scale(1.455460,2.389639), Point::new_scale(-2.809359,2.389639), Point::new_scale(-2.862805,2.386464), Point::new_scale(-7.097726,1.882168), Point::new_scale(-7.150378,1.872643), Point::new_scale(-11.286344,0.873576),
Point::new_scale(-11.335028,0.858759), Point::new_scale(-14.348632,-0.237938), Point::new_scale(-14.360538,-0.242436), Point::new_scale(-15.181601,-0.737570), Point::new_scale(-15.171309,-2.388509)
} };
expolygon.holes.push_back(Slic3r::Polygon{ Points{
Point::new_scale( -11.023311,-1.034226 ), Point::new_scale( -6.920984,-0.042941 ), Point::new_scale( -2.768613,0.463207 ), Point::new_scale( 1.414714,0.463207 ), Point::new_scale( 5.567085,-0.042941 ), Point::new_scale( 9.627910,-1.047563 )
} });
WHEN("creating the medial axis"){
Polylines res;
expolygon.medial_axis(scale_(2.5), scale_(0.5), &res);
THEN("medial axis of it is two line"){
REQUIRE(res.size() == 2);
}
}
}
GIVEN("french cross")
{
ExPolygon expolygon;
expolygon.contour = Slic3r::Polygon{ Points{
Point::new_scale(4.3, 4), Point::new_scale(4.3, 0), Point::new_scale(4, 0), Point::new_scale(4, 4), Point::new_scale(0, 4), Point::new_scale(0, 4.5), Point::new_scale(4, 4.5), Point::new_scale(4, 10), Point::new_scale(4.3, 10), Point::new_scale(4.3, 4.5),
Point::new_scale(6, 4.5), Point::new_scale(6, 10), Point::new_scale(6.2, 10), Point::new_scale(6.2, 4.5), Point::new_scale(10, 4.5), Point::new_scale(10, 4), Point::new_scale(6.2, 4), Point::new_scale(6.2, 0), Point::new_scale(6, 0), Point::new_scale(6, 4),
} };
expolygon.contour.make_counter_clockwise();
WHEN("creating the medial axis"){
Polylines res;
expolygon.medial_axis(scale_(0.55), scale_(0.25), &res);
THEN("medial axis of a (bit too narrow) french cross is two lines"){
REQUIRE(res.size() == 2);
}
THEN("medial axis has reasonable length"){
REQUIRE(res[0].length() >= scale_(9.9) - SCALED_EPSILON);
REQUIRE(res[1].length() >= scale_(9.9) - SCALED_EPSILON);
}
THEN("medial axis of a (bit too narrow) french cross is two lines has only strait lines (first line)"){
Lines lines = res[0].lines();
double min_angle = 1, max_angle = -1;
for (int idx = 1; idx < lines.size(); idx++){
double angle = lines[idx - 1].ccw(lines[idx].b);
min_angle = std::min(min_angle, angle);
max_angle = std::max(max_angle, angle);
}
REQUIRE(min_angle == max_angle);
REQUIRE(min_angle == 0);
}
THEN("medial axis of a (bit too narrow) french cross is two lines has only strait lines (second line)"){
Lines lines = res[1].lines();
double min_angle = 1, max_angle = -1;
for (int idx = 1; idx < lines.size(); idx++){
double angle = lines[idx - 1].ccw(lines[idx].b);
min_angle = std::min(min_angle, angle);
max_angle = std::max(max_angle, angle);
}
REQUIRE(min_angle == max_angle);
REQUIRE(min_angle == 0);
}
}
}
//TODO: compare with mainline slic3r
//GIVEN("tooth")
//{
// ExPolygon expolygon;
// expolygon.contour = Slic3r::Polygon{ Points{
// Point::new_scale(0.86526705, 1.4509841), Point::new_scale(0.57696039, 1.8637021),
// Point::new_scale(0.4502297, 2.5569978), Point::new_scale(0.45626199, 3.2965596),
// Point::new_scale(1.1218851, 3.3049455), Point::new_scale(0.96681072, 2.8243202),
// Point::new_scale(0.86328971, 2.2056997), Point::new_scale(0.85367905, 1.7790778)
// } };
// expolygon.contour.make_counter_clockwise();
// WHEN("creating the medial axis"){
// Polylines res;
// expolygon.medial_axis(scale_(1), scale_(0.25), &res);
// THEN("medial axis of a tooth is two lines"){
// REQUIRE(res.size() == 2);
// THEN("medial axis has reasonable length") {
// REQUIRE(res[0].length() >= scale_(1.4) - SCALED_EPSILON);
// REQUIRE(res[1].length() >= scale_(1.4) - SCALED_EPSILON);
// // TODO: check if min width is < 0.3 and max width is > 0.6 (min($res->[0]->width.front, $res->[0]->width.back) # problem: can't have access to width
// //TODO: now i have access! correct it!
// }
// }
// }
//}
GIVEN("Anchor & Tapers")
{
ExPolygon tooth;
tooth.contour = Slic3r::Polygon{ Points{
Point::new_scale(0,0), Point::new_scale(10,0), Point::new_scale(10,1.2), Point::new_scale(0,1.2)
} };
tooth.contour.make_counter_clockwise();
ExPolygon base_part;
base_part.contour = Slic3r::Polygon{ Points{
Point::new_scale(0,-3), Point::new_scale(0,3), Point::new_scale(-2,3), Point::new_scale(-2,-3)
} };
base_part.contour.make_counter_clockwise();
//expolygon.contour = Slic3r::Polygon{ Points{
// //Point::new_scale(0, 13), Point::new_scale(-1, 13), Point::new_scale(-1, 0), Point::new_scale(0.0,0.0),
// Point::new_scale(0,0.2), Point::new_scale(3,0.2), Point::new_scale(3,0.4), Point::new_scale(0,0.4),
// Point::new_scale(0,1), Point::new_scale(3,1), Point::new_scale(3,1.3), Point::new_scale(0,1.3),
// Point::new_scale(0,2), Point::new_scale(3,2), Point::new_scale(3,2.4), Point::new_scale(0,2.4),
// Point::new_scale(0,3), Point::new_scale(3,3), Point::new_scale(3,3.5), Point::new_scale(0,3.5),
// Point::new_scale(0,4), Point::new_scale(3,4), Point::new_scale(3,4.6), Point::new_scale(0,4.6),
// Point::new_scale(0,5), Point::new_scale(3,5), Point::new_scale(3,5.7), Point::new_scale(0,5.7),
// Point::new_scale(0,6), Point::new_scale(3,6), Point::new_scale(3,6.8), Point::new_scale(0,6.8),
// Point::new_scale(0,7.5), Point::new_scale(3,7.5), Point::new_scale(3,8.4), Point::new_scale(0,8.4),
// Point::new_scale(0,9), Point::new_scale(3,9), Point::new_scale(3,10), Point::new_scale(0,10),
// Point::new_scale(0,11), Point::new_scale(3,11), Point::new_scale(3,12.2), Point::new_scale(0,12.2),
//} };
WHEN("1 nozzle, 0.2 layer height") {
const coord_t nozzle_diam = scale_(1);
ExPolygon anchor = union_ex(ExPolygons{ tooth }, intersection_ex(ExPolygons{ base_part }, offset_ex(tooth, nozzle_diam / 2)), true)[0];
ThickPolylines res;
//expolygon.medial_axis(scale_(1), scale_(0.5), &res);
Slic3r::MedialAxis ma(tooth, nozzle_diam * 2, nozzle_diam/3, scale_(0.2));
ma.use_bounds(anchor)
.use_min_real_width(nozzle_diam)
.use_tapers(0.25*nozzle_diam);
ma.build(res);
THEN("medial axis of a simple line is one line") {
REQUIRE(res.size() == 1);
THEN("medial axis has the length of the line + the length of the anchor") {
std::cout << res[0].length() << "\n";
REQUIRE(std::abs(res[0].length() - scale_(10.5)) < SCALED_EPSILON);
}
THEN("medial axis has the line width as max width") {
double max_width = 0;
for (coordf_t width : res[0].width) max_width = std::max(max_width, width);
REQUIRE(std::abs(max_width - scale_(1.2)) < SCALED_EPSILON);
}
//compute the length of the tapers
THEN("medial axis has good tapers length") {
int l1 = 0;
for (size_t idx = 0; idx < res[0].width.size() - 1 && res[0].width[idx] - nozzle_diam < SCALED_EPSILON; ++idx)
l1 += res[0].lines()[idx].length();
int l2 = 0;
for (size_t idx = res[0].width.size() - 1; idx > 0 && res[0].width[idx] - nozzle_diam < SCALED_EPSILON; --idx)
l2 += res[0].lines()[idx - 1].length();
REQUIRE(std::abs(l1 - l2) < SCALED_EPSILON);
REQUIRE(std::abs(l1 - scale_(0.25 - 0.1)) < SCALED_EPSILON);
}
}
}
WHEN("1.2 nozzle, 0.6 layer height") {
const coord_t nozzle_diam = scale_(1.2);
ExPolygon anchor = union_ex(ExPolygons{ tooth }, intersection_ex(ExPolygons{ base_part }, offset_ex(tooth, nozzle_diam / 4)), true)[0];
ThickPolylines res;
//expolygon.medial_axis(scale_(1), scale_(0.5), &res);
Slic3r::MedialAxis ma(tooth, nozzle_diam * 2, nozzle_diam/3, scale_(0.6));
ma.use_bounds(anchor)
.use_min_real_width(nozzle_diam)
.use_tapers(1.0*nozzle_diam);
ma.build(res);
THEN("medial axis of a simple line is one line") {
REQUIRE(res.size() == 1);
THEN("medial axis has the length of the line + the length of the anchor") {
//0.3 because it's offseted by nozzle_diam / 4
REQUIRE(std::abs(res[0].length() - scale_(10.3)) < SCALED_EPSILON);
}
THEN("medial axis can'ty have a line width below Flow::new_from_spacing(nozzle_diam).width") {
double max_width = 0;
for (coordf_t width : res[0].width) max_width = std::max(max_width, width);
double min_width = Flow::new_from_spacing(float(unscale_(nozzle_diam)), float(unscale_(nozzle_diam)), 0.6f, false).scaled_width();
REQUIRE(std::abs(max_width - min_width) < SCALED_EPSILON);
REQUIRE(std::abs(max_width - nozzle_diam) > SCALED_EPSILON);
}
//compute the length of the tapers
THEN("medial axis has a 45<34> taper and a shorter one") {
int l1 = 0;
for (size_t idx = 0; idx < res[0].width.size() - 1 && res[0].width[idx] - scale_(1.2) < SCALED_EPSILON; ++idx)
l1 += res[0].lines()[idx].length();
int l2 = 0;
for (size_t idx = res[0].width.size() - 1; idx > 0 && res[0].width[idx] - scale_(1.2) < SCALED_EPSILON; --idx)
l2 += res[0].lines()[idx - 1].length();
//here the taper is limited by the 0-width spacing
double min_width = Flow::new_from_spacing(float(unscale_(nozzle_diam)), float(unscale_(nozzle_diam)), 0.6f, false).scaled_width();
REQUIRE(std::abs(l1 - l2) < SCALED_EPSILON);
REQUIRE(l1 < scale_(0.6));
REQUIRE(l1 > scale_(0.4));
}
}
}
}
GIVEN("1<EFBFBD> rotated tooths")
{
}
GIVEN("narrow trapezoid")
{
ExPolygon expolygon;
expolygon.contour = Slic3r::Polygon{ Points{
Point::new_scale(100, 100),
Point::new_scale(120, 100),
Point::new_scale(112, 200),
Point::new_scale(108, 200)
} };
WHEN("creating the medial axis"){
Polylines res;
expolygon.medial_axis(scale_(20), scale_(0.5), &res);
THEN("medial axis of a narrow trapezoid is a single line"){
REQUIRE(res.size() == 1);
THEN("medial axis has reasonable length") {
REQUIRE(res[0].length() >= scale_(200 - 100 - (120 - 100)) - SCALED_EPSILON);
}
}
}
}
GIVEN("L shape")
{
ExPolygon expolygon;
expolygon.contour = Slic3r::Polygon{ Points{
Point::new_scale(100, 100),
Point::new_scale(120, 100),
Point::new_scale(120, 180),
Point::new_scale(200, 180),
Point::new_scale(200, 200),
Point::new_scale(100, 200)
} };
WHEN("creating the medial axis"){
Polylines res;
expolygon.medial_axis(scale_(20), scale_(0.5), &res);
THEN("medial axis of a L shape is a single line"){
REQUIRE(res.size() == 1);
THEN("medial axis has reasonable length") {
// 20 is the thickness of the expolygon, which is subtracted from the ends
REQUIRE(res[0].length() + 20 > scale_(80 * 2) - SCALED_EPSILON);
REQUIRE(res[0].length() + 20 < scale_(100 * 2) + SCALED_EPSILON);
}
}
}
}
GIVEN("shape"){
ExPolygon expolygon;
expolygon.contour = Slic3r::Polygon{ Points{
Point{ -203064906, -51459966 }, Point{ -219312231, -51459966 }, Point{ -219335477, -51459962 }, Point{ -219376095, -51459962 }, Point{ -219412047, -51459966 },
Point{ -219572094, -51459966 }, Point{ -219624814, -51459962 }, Point{ -219642183, -51459962 }, Point{ -219656665, -51459966 }, Point{ -220815482, -51459966 },
Point{ -220815482, -37738966 }, Point{ -221117540, -37738966 }, Point{ -221117540, -51762024 }, Point{ -203064906, -51762024 },
} };
WHEN("creating the medial axis"){
Polylines polylines;
expolygon.medial_axis(819998, 102499.75, &polylines);
double perimeter_len = expolygon.contour.split_at_first_point().length();
THEN("medial axis has reasonable length"){
double polyline_length = 0;
for (Slic3r::Polyline &poly : polylines) polyline_length += poly.length();
REQUIRE(polyline_length > perimeter_len * 3. / 8. - SCALED_EPSILON);
}
}
}
GIVEN("narrow triangle")
{
ExPolygon expolygon;
expolygon.contour = Slic3r::Polygon{ Points{
Point::new_scale(50, 100),
Point::new_scale(1000, 102),
Point::new_scale(50, 104)
} };
WHEN("creating the medial axis"){
Polylines res;
expolygon.medial_axis(scale_(4), scale_(0.5), &res);
THEN("medial axis of a narrow triangle is a single line"){
REQUIRE(res.size() == 1);
THEN("medial axis has reasonable length") {
REQUIRE(res[0].length() > scale_(200 - 100 - (120 - 100)) - SCALED_EPSILON);
}
}
}
}
GIVEN("GH #2474")
{
ExPolygon expolygon;
expolygon.contour = Slic3r::Polygon{ Points{
Point{91294454, 31032190},
Point{11294481, 31032190},
Point{11294481, 29967810},
Point{44969182, 29967810},
Point{89909960, 29967808},
Point{91294454, 29967808}
} };
WHEN("creating the medial axis") {
Polylines res;
expolygon.medial_axis(1871238, 500000, &res);
THEN("medial axis is a single polyline") {
REQUIRE(res.size() == 1);
Slic3r::Polyline polyline = res[0];
THEN("medial axis is horizontal and is centered") {
double sum = 0;
for (Line &l : polyline.lines()) sum += std::abs(l.b.y() - l.a.y());
coord_t expected_y = expolygon.contour.bounding_box().center().y();
REQUIRE((sum / polyline.size()) - expected_y < SCALED_EPSILON);
}
// order polyline from left to right
if (polyline.first_point().x() > polyline.last_point().x()) polyline.reverse();
THEN("expected x_min & x_max") {
BoundingBox polyline_bb = polyline.bounding_box();
REQUIRE(polyline.first_point().x() == polyline_bb.min.x());
REQUIRE(polyline.last_point().x() == polyline_bb.max.x());
}
THEN("medial axis is not self-overlapping") {
//TODO
//REQUIRE(polyline.first_point().x() == polyline_bb.x_min());
std::vector<coord_t> all_x;
for (Point &p : polyline.points) all_x.push_back(p.x());
std::vector<coord_t> sorted_x{ all_x };
std::sort(sorted_x.begin(), sorted_x.end());
for (size_t i = 0; i < all_x.size(); i++) {
REQUIRE(all_x[i] == sorted_x[i]);
}
}
}
}
}
}

View File

@ -18,4 +18,10 @@ inline Slic3r::TriangleMesh load_model(const std::string &obj_filename)
return mesh;
}
inline std::string get_model_path(const std::string &obj_filename)
{
std::string fpath = TEST_DATA_DIR PATH_SEPARATOR + obj_filename;
return fpath;
}
#endif // SLIC3R_TEST_UTILS