Reworked the Perl unit / integration tests to use the same Print

interface that the application is using. Old interface used just
for the integration tests was removed.
This commit is contained in:
bubnikv 2019-06-20 20:23:05 +02:00
parent 8bf6e69851
commit ac6969c992
12 changed files with 72 additions and 359 deletions

View File

@ -50,7 +50,6 @@ use Slic3r::Point;
use Slic3r::Polygon; use Slic3r::Polygon;
use Slic3r::Polyline; use Slic3r::Polyline;
use Slic3r::Print::Object; use Slic3r::Print::Object;
use Slic3r::Print::Simple;
use Slic3r::Surface; use Slic3r::Surface;
our $build = eval "use Slic3r::Build; 1"; our $build = eval "use Slic3r::Build; 1";

View File

@ -1,104 +0,0 @@
# A simple wrapper to quickly print a single model without a GUI.
# Used by the command line slic3r.pl, by command line utilities pdf-slic3s.pl and view-toolpaths.pl,
# and by the quick slice menu of the Slic3r GUI.
#
# It creates and owns an instance of Slic3r::Print to perform the slicing
# and it accepts an instance of Slic3r::Model from the outside.
package Slic3r::Print::Simple;
use Moo;
use Slic3r::Geometry qw(X Y);
has '_print' => (
is => 'ro',
default => sub { Slic3r::Print->new },
handles => [qw(apply_config_perl_tests_only extruders output_filepath
total_used_filament total_extruded_volume
placeholder_parser process)],
);
has 'duplicate' => (
is => 'rw',
default => sub { 1 },
);
has 'scale' => (
is => 'rw',
default => sub { 1 },
);
has 'rotate' => (
is => 'rw',
default => sub { 0 },
);
has 'duplicate_grid' => (
is => 'rw',
default => sub { [1,1] },
);
has 'print_center' => (
is => 'rw',
default => sub { Slic3r::Pointf->new(100,100) },
);
has 'dont_arrange' => (
is => 'rw',
default => sub { 0 },
);
has 'output_file' => (
is => 'rw',
);
sub set_model {
# $model is of type Slic3r::Model
my ($self, $model) = @_;
# make method idempotent so that the object is reusable
$self->_print->clear_objects;
# make sure all objects have at least one defined instance
my $need_arrange = $model->add_default_instances && ! $self->dont_arrange;
# apply scaling and rotation supplied from command line if any
foreach my $instance (map @{$_->instances}, @{$model->objects}) {
$instance->set_scaling_factor($instance->scaling_factor * $self->scale);
$instance->set_rotation($instance->rotation + $self->rotate);
}
if ($self->duplicate_grid->[X] > 1 || $self->duplicate_grid->[Y] > 1) {
$model->duplicate_objects_grid($self->duplicate_grid->[X], $self->duplicate_grid->[Y], $self->_print->config->duplicate_distance);
} elsif ($need_arrange) {
$model->duplicate_objects($self->duplicate, $self->_print->config->min_object_distance);
} elsif ($self->duplicate > 1) {
# if all input objects have defined position(s) apply duplication to the whole model
$model->duplicate($self->duplicate, $self->_print->config->min_object_distance);
}
$_->translate(0,0,-$_->bounding_box->z_min) for @{$model->objects};
$model->center_instances_around_point($self->print_center) if (! $self->dont_arrange);
foreach my $model_object (@{$model->objects}) {
$self->_print->auto_assign_extruders($model_object);
$self->_print->add_model_object($model_object);
}
}
sub export_gcode {
my ($self) = @_;
$self->_print->validate;
$self->_print->export_gcode($self->output_file // '');
}
sub export_png {
my ($self) = @_;
$self->_before_export;
$self->_print->export_png(output_file => $self->output_file);
$self->_after_export;
}
1;

View File

@ -146,13 +146,16 @@ sub mesh {
} }
sub model { sub model {
my ($model_name, %params) = @_; my ($model_names, %params) = @_;
$model_names = [ $model_names ] if ! ref($model_names);
my $model = Slic3r::Model->new;
for my $model_name (@$model_names) {
my $input_file = "${model_name}.stl"; my $input_file = "${model_name}.stl";
my $mesh = mesh($model_name, %params); my $mesh = mesh($model_name, %params);
# $mesh->write_ascii("out/$input_file"); # $mesh->write_ascii("out/$input_file");
my $model = Slic3r::Model->new;
my $object = $model->add_object(input_file => $input_file); my $object = $model->add_object(input_file => $input_file);
$model->set_material($model_name); $model->set_material($model_name);
$object->add_volume(mesh => $mesh, material_id => $model_name); $object->add_volume(mesh => $mesh, material_id => $model_name);
@ -165,41 +168,44 @@ sub model {
# rotation => $params{rotation} // 0, # rotation => $params{rotation} // 0,
# scaling_factor => $params{scale} // 1, # scaling_factor => $params{scale} // 1,
); );
}
return $model; return $model;
} }
sub init_print { sub init_print {
my ($models, %params) = @_; my ($models, %params) = @_;
my $model;
if (ref($models) eq 'ARRAY') {
$model = model($models, %params);
} elsif (ref($models)) {
$model = $models;
} else {
$model = model([$models], %params);
}
my $config = Slic3r::Config->new; my $config = Slic3r::Config->new;
$config->apply($params{config}) if $params{config}; $config->apply($params{config}) if $params{config};
$config->set('gcode_comments', 1) if $ENV{SLIC3R_TESTS_GCODE}; $config->set('gcode_comments', 1) if $ENV{SLIC3R_TESTS_GCODE};
my $print = Slic3r::Print->new; my $print = Slic3r::Print->new;
$print->apply_config_perl_tests_only($config);
$models = [$models] if ref($models) ne 'ARRAY';
$models = [ map { ref($_) ? $_ : model($_, %params) } @$models ];
for my $model (@$models) {
die "Unknown model in test" if !defined $model; die "Unknown model in test" if !defined $model;
if (defined $params{duplicate} && $params{duplicate} > 1) { if (defined $params{duplicate} && $params{duplicate} > 1) {
$model->duplicate($params{duplicate} // 1, $print->config->min_object_distance); $model->duplicate($params{duplicate} // 1, $config->min_object_distance);
} }
$model->arrange_objects($print->config->min_object_distance); $model->arrange_objects($config->min_object_distance);
$model->center_instances_around_point($params{print_center} ? Slic3r::Pointf->new(@{$params{print_center}}) : Slic3r::Pointf->new(100,100)); $model->center_instances_around_point($params{print_center} ? Slic3r::Pointf->new(@{$params{print_center}}) : Slic3r::Pointf->new(100,100));
foreach my $model_object (@{$model->objects}) { foreach my $model_object (@{$model->objects}) {
$model_object->ensure_on_bed;
$print->auto_assign_extruders($model_object); $print->auto_assign_extruders($model_object);
$print->add_model_object($model_object);
} }
}
# Call apply_config_perl_tests_only one more time, so that the layer height profiles are updated over all PrintObjects. $print->apply($model, $config);
$print->apply_config_perl_tests_only($config);
$print->validate; $print->validate;
# We return a proxy object in order to keep $models alive as required by the Print API. # We return a proxy object in order to keep $models alive as required by the Print API.
return Slic3r::Test::Print->new( return Slic3r::Test::Print->new(
print => $print, print => $print,
models => $models, model => $model,
); );
} }
@ -250,7 +256,7 @@ sub add_facet {
package Slic3r::Test::Print; package Slic3r::Test::Print;
use Moo; use Moo;
has 'print' => (is => 'ro', required => 1, handles => [qw(process apply_config_perl_tests_only)]); has 'print' => (is => 'ro', required => 1, handles => [qw(process apply)]);
has 'models' => (is => 'ro', required => 1); has 'model' => (is => 'ro', required => 1);
1; 1;

View File

@ -597,8 +597,8 @@ public:
Model() {} Model() {}
~Model() { this->clear_objects(); this->clear_materials(); } ~Model() { this->clear_objects(); this->clear_materials(); }
/* To be able to return an object from own copy / clone methods. Hopefully the compiler will do the "Copy elision" */ // To be able to return an object from own copy / clone methods. Hopefully the compiler will do the "Copy elision"
/* (Omits copy and move(since C++11) constructors, resulting in zero - copy pass - by - value semantics). */ // (Omits copy and move(since C++11) constructors, resulting in zero - copy pass - by - value semantics).
Model(const Model &rhs) : ModelBase(-1) { this->assign_copy(rhs); } Model(const Model &rhs) : ModelBase(-1) { this->assign_copy(rhs); }
explicit Model(Model &&rhs) : ModelBase(-1) { this->assign_copy(std::move(rhs)); } explicit Model(Model &&rhs) : ModelBase(-1) { this->assign_copy(std::move(rhs)); }
Model& operator=(const Model &rhs) { this->assign_copy(rhs); return *this; } Model& operator=(const Model &rhs) { this->assign_copy(rhs); return *this; }

View File

@ -328,198 +328,6 @@ double Print::max_allowed_layer_height() const
return nozzle_diameter_max; return nozzle_diameter_max;
} }
// Caller is responsible for supplying models whose objects don't collide
// and have explicit instance positions.
void Print::add_model_object_perl_tests_only(ModelObject* model_object, int idx)
{
tbb::mutex::scoped_lock lock(this->state_mutex());
// Add a copy of this ModelObject to this Print.
m_model.objects.emplace_back(ModelObject::new_copy(*model_object));
m_model.objects.back()->set_model(&m_model);
// Initialize a new print object and store it at the given position.
PrintObject *object = new PrintObject(this, model_object, true);
if (idx != -1) {
delete m_objects[idx];
m_objects[idx] = object;
} else
m_objects.emplace_back(object);
// Invalidate all print steps.
this->invalidate_all_steps();
// Set the transformation matrix without translation from the first instance.
if (! model_object->instances.empty()) {
// Trafo and bounding box, both in world coordinate system.
Transform3d trafo = model_object->instances.front()->get_matrix();
BoundingBoxf3 bbox = model_object->instance_bounding_box(0);
// Now shift the object up to align it with the print bed.
trafo.data()[14] -= bbox.min(2);
// and reset the XY translation.
trafo.data()[12] = 0;
trafo.data()[13] = 0;
object->set_trafo(trafo);
}
int volume_id = 0;
for (const ModelVolume *volume : model_object->volumes) {
if (! volume->is_model_part() && ! volume->is_modifier())
continue;
// Get the config applied to this volume.
PrintRegionConfig config = PrintObject::region_config_from_model_volume(m_default_region_config, nullptr, *volume, 99999);
// Find an existing print region with the same config.
int region_id = -1;
for (int i = 0; i < (int)m_regions.size(); ++ i)
if (config.equals(m_regions[i]->config())) {
region_id = i;
break;
}
// If no region exists with the same config, create a new one.
if (region_id == -1) {
region_id = (int)m_regions.size();
this->add_region(config);
}
// Assign volume to a region.
object->add_region_volume((unsigned int)region_id, volume_id, t_layer_height_range(0, DBL_MAX));
++ volume_id;
}
// Apply config to print object.
object->config_apply(this->default_object_config());
{
//normalize_and_apply_config(object->config(), model_object->config);
DynamicPrintConfig src_normalized(model_object->config);
src_normalized.normalize();
object->config_apply(src_normalized, true);
}
}
// This function is only called through the Perl-C++ binding from the unit tests, should be
// removed when unit tests are rewritten to C++.
bool Print::apply_config_perl_tests_only(DynamicPrintConfig config)
{
tbb::mutex::scoped_lock lock(this->state_mutex());
// Perl unit tests were failing in case the preset was not normalized (e.g. https://github.com/prusa3d/PrusaSlicer/issues/2288 was caused
// by too short max_layer_height vector. Calling the necessary function Preset::normalize(...) is not currently possible because there is no
// access to preset. This should be solved when the unit tests are rewritten to C++. For now we just copy-pasted code from Preset.cpp
// to make sure the unit tests pass (functions set_num_extruders and nozzle_options()).
auto *nozzle_diameter = dynamic_cast<const ConfigOptionFloats*>(config.option("nozzle_diameter", true));
assert(nozzle_diameter != nullptr);
const auto &defaults = FullPrintConfig::defaults();
for (const std::string &key : { "nozzle_diameter", "min_layer_height", "max_layer_height", "extruder_offset",
"retract_length", "retract_lift", "retract_lift_above", "retract_lift_below", "retract_speed", "deretract_speed",
"retract_before_wipe", "retract_restart_extra", "retract_before_travel", "wipe",
"retract_layer_change", "retract_length_toolchange", "retract_restart_extra_toolchange", "extruder_colour" })
{
auto *opt = config.option(key, true);
assert(opt != nullptr);
assert(opt->is_vector());
unsigned int num_extruders = (unsigned int)nozzle_diameter->values.size();
static_cast<ConfigOptionVectorBase*>(opt)->resize(num_extruders, defaults.option(key));
}
// we get a copy of the config object so we can modify it safely
config.normalize();
// apply variables to placeholder parser
this->placeholder_parser().apply_config(config);
// handle changes to print config
t_config_option_keys print_diff = m_config.diff(config);
m_config.apply_only(config, print_diff, true);
bool invalidated = this->invalidate_state_by_config_options(print_diff);
// handle changes to object config defaults
m_default_object_config.apply(config, true);
for (PrintObject *object : m_objects) {
// we don't assume that config contains a full ObjectConfig,
// so we base it on the current print-wise default
PrintObjectConfig new_config = this->default_object_config();
// we override the new config with object-specific options
normalize_and_apply_config(new_config, object->model_object()->config);
// check whether the new config is different from the current one
t_config_option_keys diff = object->config().diff(new_config);
object->config_apply_only(new_config, diff, true);
invalidated |= object->invalidate_state_by_config_options(diff);
}
// handle changes to regions config defaults
m_default_region_config.apply(config, true);
// All regions now have distinct settings.
// Check whether applying the new region config defaults we'd get different regions.
bool rearrange_regions = false;
{
// Collect the already visited region configs into other_region_configs,
// so one may check for duplicates.
std::vector<PrintRegionConfig> other_region_configs;
for (size_t region_id = 0; region_id < m_regions.size(); ++ region_id) {
PrintRegion &region = *m_regions[region_id];
PrintRegionConfig this_region_config;
bool this_region_config_set = false;
for (PrintObject *object : m_objects) {
if (region_id < object->region_volumes.size()) {
for (const std::pair<t_layer_height_range, int> &volume_and_range : object->region_volumes[region_id]) {
const ModelVolume &volume = *object->model_object()->volumes[volume_and_range.second];
if (this_region_config_set) {
// If the new config for this volume differs from the other
// volume configs currently associated to this region, it means
// the region subdivision does not make sense anymore.
if (! this_region_config.equals(PrintObject::region_config_from_model_volume(m_default_region_config, nullptr, volume, 99999))) {
rearrange_regions = true;
goto exit_for_rearrange_regions;
}
} else {
this_region_config = PrintObject::region_config_from_model_volume(m_default_region_config, nullptr, volume, 99999);
this_region_config_set = true;
}
for (const PrintRegionConfig &cfg : other_region_configs) {
// If the new config for this volume equals any of the other
// volume configs that are not currently associated to this
// region, it means the region subdivision does not make
// sense anymore.
if (cfg.equals(this_region_config)) {
rearrange_regions = true;
goto exit_for_rearrange_regions;
}
}
}
}
}
if (this_region_config_set) {
t_config_option_keys diff = region.config().diff(this_region_config);
if (! diff.empty()) {
region.config_apply_only(this_region_config, diff, false);
for (PrintObject *object : m_objects)
if (region_id < object->region_volumes.size() && ! object->region_volumes[region_id].empty())
invalidated |= object->invalidate_state_by_config_options(diff);
}
other_region_configs.emplace_back(std::move(this_region_config));
}
}
}
exit_for_rearrange_regions:
if (rearrange_regions) {
// The current subdivision of regions does not make sense anymore.
// We need to remove all objects and re-add them.
ModelObjectPtrs model_objects;
model_objects.reserve(m_objects.size());
for (PrintObject *object : m_objects)
model_objects.push_back(object->model_object());
this->clear();
for (ModelObject *mo : model_objects)
this->add_model_object_perl_tests_only(mo);
invalidated = true;
}
for (PrintObject *object : m_objects)
object->update_slicing_parameters();
return invalidated;
}
// Add or remove support modifier ModelVolumes from model_object_dst to match the ModelVolumes of model_object_new // Add or remove support modifier ModelVolumes from model_object_dst to match the ModelVolumes of model_object_new
// in the exact order and with the same IDs. // in the exact order and with the same IDs.
// It is expected, that the model_object_dst already contains the non-support volumes of model_object_new in the correct order. // It is expected, that the model_object_dst already contains the non-support volumes of model_object_new in the correct order.
@ -1247,7 +1055,7 @@ std::string Print::validate() const
Geometry::assemble_transform(Vec3d::Zero(), rotation, model_instance0->get_scaling_factor(), model_instance0->get_mirror())), Geometry::assemble_transform(Vec3d::Zero(), rotation, model_instance0->get_scaling_factor(), model_instance0->get_mirror())),
float(scale_(0.5 * m_config.extruder_clearance_radius.value)), jtRound, float(scale_(0.1))).front(); float(scale_(0.5 * m_config.extruder_clearance_radius.value)), jtRound, float(scale_(0.1))).front();
// Now we check that no instance of convex_hull intersects any of the previously checked object instances. // Now we check that no instance of convex_hull intersects any of the previously checked object instances.
for (const Point &copy : print_object->m_copies) { for (const Point &copy : print_object->copies()) {
Polygon convex_hull = convex_hull0; Polygon convex_hull = convex_hull0;
convex_hull.translate(copy); convex_hull.translate(copy);
if (! intersection(convex_hulls_other, convex_hull).empty()) if (! intersection(convex_hulls_other, convex_hull).empty())

View File

@ -294,10 +294,6 @@ public:
ApplyStatus apply(const Model &model, const DynamicPrintConfig &config) override; ApplyStatus apply(const Model &model, const DynamicPrintConfig &config) override;
// The following three methods are used by the Perl tests only. Get rid of them!
void add_model_object_perl_tests_only(ModelObject* model_object, int idx = -1);
bool apply_config_perl_tests_only(DynamicPrintConfig config);
void process() override; void process() override;
// Exports G-code into a file name based on the path_template, returns the file path of the generated G-code file. // Exports G-code into a file name based on the path_template, returns the file path of the generated G-code file.
// If preview_data is not null, the preview_data is filled in for the G-code visualization (not used by the command line Slic3r). // If preview_data is not null, the preview_data is filled in for the G-code visualization (not used by the command line Slic3r).

View File

@ -89,7 +89,7 @@ plan tests => 8;
# we disable combination after infill has been generated # we disable combination after infill has been generated
$config->set('infill_every_layers', 1); $config->set('infill_every_layers', 1);
$print->apply_config_perl_tests_only($config); $print->apply($print->print->model->clone, $config);
$print->process; $print->process;
ok !(defined first { @{$_->get_region(0)->fill_surfaces} == 0 } ok !(defined first { @{$_->get_region(0)->fill_surfaces} == 0 }

View File

@ -1,4 +1,4 @@
use Test::More tests => 2; use Test::More tests => 6;
use strict; use strict;
use warnings; use warnings;
@ -31,29 +31,32 @@ use Slic3r::Test;
ok abs(unscale($center->[Y]) - $print_center->[Y]) < 0.005, 'print is centered around print_center (Y)'; ok abs(unscale($center->[Y]) - $print_center->[Y]) < 0.005, 'print is centered around print_center (Y)';
} }
# This is really testing a path, which is no more used by the slicer, just by the test cases.
if (0)
{ {
# this represents the aggregate config from presets # this represents the aggregate config from presets
my $config = Slic3r::Config::new_from_defaults; my $config = Slic3r::Config::new_from_defaults;
# Define 4 extruders.
$config->set('nozzle_diameter', [0.4, 0.4, 0.4, 0.4]);
# user adds one object to the plater # user adds one object to the plater
my $print = Slic3r::Test::init_print(my $model = Slic3r::Test::model('20mm_cube'), config => $config); my $print = Slic3r::Test::init_print(my $model = Slic3r::Test::model('20mm_cube'), config => $config);
# user sets a per-region option # user sets a per-region option
$print->print->objects->[0]->model_object->config->set('fill_density', 100); my $model2 = $model->clone;
# $print->print->reload_object(0); $model2->get_object(0)->config->set('fill_density', 100);
$print->apply($model2, $config);
is $print->print->regions->[0]->config->fill_density, 100, 'region config inherits model object config'; is $print->print->regions->[0]->config->fill_density, 100, 'region config inherits model object config';
# user exports G-code, thus the default config is reapplied # user exports G-code, thus the default config is reapplied
$print->print->apply_config_perl_tests_only($config); $model2->get_object(0)->config->erase('fill_density');
$print->apply($model2, $config);
is $print->print->regions->[0]->config->fill_density, 100, 'apply_config() does not override per-object settings'; is $print->print->regions->[0]->config->fill_density, 20, 'region config is resetted';
# user assigns object extruders # user assigns object extruders
$print->print->objects->[0]->model_object->config->set('extruder', 3); $model2->get_object(0)->config->set('extruder', 3);
$print->print->objects->[0]->model_object->config->set('perimeter_extruder', 2); $model2->get_object(0)->config->set('perimeter_extruder', 2);
# $print->print->reload_object(0); $print->apply($model2, $config);
is $print->print->regions->[0]->config->infill_extruder, 3, 'extruder setting is correctly expanded'; is $print->print->regions->[0]->config->infill_extruder, 3, 'extruder setting is correctly expanded';
is $print->print->regions->[0]->config->perimeter_extruder, 2, 'extruder setting does not override explicitely specified extruders'; is $print->print->regions->[0]->config->perimeter_extruder, 2, 'extruder setting does not override explicitely specified extruders';

View File

@ -91,6 +91,8 @@ use Slic3r::Test;
{ {
my $config = Slic3r::Config::new_from_defaults; my $config = Slic3r::Config::new_from_defaults;
# Define 4 extruders.
$config->set('nozzle_diameter', [0.4, 0.4, 0.4, 0.4]);
$config->set('layer_height', 0.4); $config->set('layer_height', 0.4);
$config->set('first_layer_height', 0.4); $config->set('first_layer_height', 0.4);
$config->set('skirts', 1); $config->set('skirts', 1);
@ -106,7 +108,7 @@ use Slic3r::Test;
# we enable support material after skirt has been generated # we enable support material after skirt has been generated
$config->set('support_material', 1); $config->set('support_material', 1);
$print->apply_config_perl_tests_only($config); $print->apply($print->print->model->clone, $config);
my $skirt_length = 0; my $skirt_length = 0;
my @extrusion_points = (); my @extrusion_points = ();

View File

@ -49,7 +49,7 @@
void erase(t_config_option_key opt_key); void erase(t_config_option_key opt_key);
void normalize(); void normalize();
%name{setenv} void setenv_(); %name{setenv} void setenv_();
double min_object_distance() %code{% RETVAL = PrintConfig::min_object_distance(THIS); %}; double min_object_distance() %code{% PrintConfig cfg; cfg.apply(*THIS, true); RETVAL = cfg.min_object_distance(); %};
static DynamicPrintConfig* load(char *path) static DynamicPrintConfig* load(char *path)
%code%{ %code%{
auto config = new DynamicPrintConfig(); auto config = new DynamicPrintConfig();

View File

@ -211,6 +211,7 @@ ModelMaterial::attributes()
void set_origin_translation(Vec3d* point) void set_origin_translation(Vec3d* point)
%code%{ THIS->origin_translation = *point; %}; %code%{ THIS->origin_translation = *point; %};
void ensure_on_bed();
bool needed_repair() const; bool needed_repair() const;
int materials_count() const; int materials_count() const;
int facets_count(); int facets_count();

View File

@ -70,6 +70,8 @@ _constant()
Print(); Print();
~Print(); ~Print();
Ref<Model> model()
%code%{ RETVAL = const_cast<Model*>(&THIS->model()); %};
Ref<StaticPrintConfig> config() Ref<StaticPrintConfig> config()
%code%{ RETVAL = const_cast<GCodeConfig*>(static_cast<const GCodeConfig*>(&THIS->config())); %}; %code%{ RETVAL = const_cast<GCodeConfig*>(static_cast<const GCodeConfig*>(&THIS->config())); %};
Ref<PlaceholderParser> placeholder_parser() Ref<PlaceholderParser> placeholder_parser()
@ -140,8 +142,8 @@ _constant()
} }
%}; %};
bool apply_config_perl_tests_only(DynamicPrintConfig* config) bool apply(Model *model, DynamicPrintConfig* config)
%code%{ RETVAL = THIS->apply_config_perl_tests_only(*config); %}; %code%{ RETVAL = THIS->apply(*model, *config); %};
bool has_infinite_skirt(); bool has_infinite_skirt();
std::vector<unsigned int> extruders() const; std::vector<unsigned int> extruders() const;
int validate() %code%{ int validate() %code%{