Refactoring. Use Model class for representing the plate in GUI

This commit is contained in:
Alessandro Ranellucci 2013-12-12 20:19:33 +01:00
parent f55e057504
commit 0e8a0ef1ca
19 changed files with 610 additions and 620 deletions

View File

@ -120,7 +120,7 @@ sub end_document {
foreach my $instance (@{ $self->{_instances}{$object_id} }) { foreach my $instance (@{ $self->{_instances}{$object_id} }) {
$self->{_model}->objects->[$new_object_id]->add_instance( $self->{_model}->objects->[$new_object_id]->add_instance(
rotation => $instance->{rz} || 0, rotation => $instance->{rz} || 0,
offset => Slic3r::Point->new($instance->{deltax} || 0, $instance->{deltay} || 0), offset => [ $instance->{deltax} || 0, $instance->{deltay} || 0 ],
); );
} }
} }

View File

@ -12,7 +12,7 @@ use Wx qw(:bitmap :brush :button :cursor :dialog :filedialog :font :keycode :ico
use Wx::Event qw(EVT_BUTTON EVT_COMMAND EVT_KEY_DOWN EVT_LIST_ITEM_ACTIVATED EVT_LIST_ITEM_DESELECTED EVT_LIST_ITEM_SELECTED EVT_MOUSE_EVENTS EVT_PAINT EVT_TOOL EVT_CHOICE); use Wx::Event qw(EVT_BUTTON EVT_COMMAND EVT_KEY_DOWN EVT_LIST_ITEM_ACTIVATED EVT_LIST_ITEM_DESELECTED EVT_LIST_ITEM_SELECTED EVT_MOUSE_EVENTS EVT_PAINT EVT_TOOL EVT_CHOICE);
use base 'Wx::Panel'; use base 'Wx::Panel';
use constant TB_LOAD => &Wx::NewId; use constant TB_ADD => &Wx::NewId;
use constant TB_REMOVE => &Wx::NewId; use constant TB_REMOVE => &Wx::NewId;
use constant TB_RESET => &Wx::NewId; use constant TB_RESET => &Wx::NewId;
use constant TB_ARRANGE => &Wx::NewId; use constant TB_ARRANGE => &Wx::NewId;
@ -48,8 +48,8 @@ sub new {
$self->{config} = Slic3r::Config->new_from_defaults(qw( $self->{config} = Slic3r::Config->new_from_defaults(qw(
bed_size print_center complete_objects extruder_clearance_radius skirts skirt_distance bed_size print_center complete_objects extruder_clearance_radius skirts skirt_distance
)); ));
$self->{model} = Slic3r::Model->new;
$self->{objects} = []; $self->{objects} = [];
$self->{selected_objects} = [];
$self->{canvas} = Wx::Panel->new($self, -1, wxDefaultPosition, CANVAS_SIZE, wxTAB_TRAVERSAL); $self->{canvas} = Wx::Panel->new($self, -1, wxDefaultPosition, CANVAS_SIZE, wxTAB_TRAVERSAL);
$self->{canvas}->SetBackgroundColour(Wx::wxWHITE); $self->{canvas}->SetBackgroundColour(Wx::wxWHITE);
@ -69,7 +69,7 @@ sub new {
if (!&Wx::wxMSW) { if (!&Wx::wxMSW) {
Wx::ToolTip::Enable(1); Wx::ToolTip::Enable(1);
$self->{htoolbar} = Wx::ToolBar->new($self, -1, wxDefaultPosition, wxDefaultSize, wxTB_HORIZONTAL | wxTB_TEXT | wxBORDER_SIMPLE | wxTAB_TRAVERSAL); $self->{htoolbar} = Wx::ToolBar->new($self, -1, wxDefaultPosition, wxDefaultSize, wxTB_HORIZONTAL | wxTB_TEXT | wxBORDER_SIMPLE | wxTAB_TRAVERSAL);
$self->{htoolbar}->AddTool(TB_LOAD, "Add…", Wx::Bitmap->new("$Slic3r::var/brick_add.png", wxBITMAP_TYPE_PNG), ''); $self->{htoolbar}->AddTool(TB_ADD, "Add…", Wx::Bitmap->new("$Slic3r::var/brick_add.png", wxBITMAP_TYPE_PNG), '');
$self->{htoolbar}->AddTool(TB_REMOVE, "Delete", Wx::Bitmap->new("$Slic3r::var/brick_delete.png", wxBITMAP_TYPE_PNG), ''); $self->{htoolbar}->AddTool(TB_REMOVE, "Delete", Wx::Bitmap->new("$Slic3r::var/brick_delete.png", wxBITMAP_TYPE_PNG), '');
$self->{htoolbar}->AddTool(TB_RESET, "Delete All", Wx::Bitmap->new("$Slic3r::var/cross.png", wxBITMAP_TYPE_PNG), ''); $self->{htoolbar}->AddTool(TB_RESET, "Delete All", Wx::Bitmap->new("$Slic3r::var/cross.png", wxBITMAP_TYPE_PNG), '');
$self->{htoolbar}->AddTool(TB_ARRANGE, "Arrange", Wx::Bitmap->new("$Slic3r::var/bricks.png", wxBITMAP_TYPE_PNG), ''); $self->{htoolbar}->AddTool(TB_ARRANGE, "Arrange", Wx::Bitmap->new("$Slic3r::var/bricks.png", wxBITMAP_TYPE_PNG), '');
@ -159,7 +159,7 @@ sub new {
EVT_BUTTON($self, $self->{btn_export_stl}, \&export_stl); EVT_BUTTON($self, $self->{btn_export_stl}, \&export_stl);
if ($self->{htoolbar}) { if ($self->{htoolbar}) {
EVT_TOOL($self, TB_LOAD, \&load); EVT_TOOL($self, TB_ADD, \&add);
EVT_TOOL($self, TB_REMOVE, sub { $self->remove() }); # explicitly pass no argument to remove EVT_TOOL($self, TB_REMOVE, sub { $self->remove() }); # explicitly pass no argument to remove
EVT_TOOL($self, TB_RESET, \&reset); EVT_TOOL($self, TB_RESET, \&reset);
EVT_TOOL($self, TB_ARRANGE, \&arrange); EVT_TOOL($self, TB_ARRANGE, \&arrange);
@ -173,7 +173,7 @@ sub new {
EVT_TOOL($self, TB_VIEW, sub { $_[0]->object_preview_dialog }); EVT_TOOL($self, TB_VIEW, sub { $_[0]->object_preview_dialog });
EVT_TOOL($self, TB_SETTINGS, sub { $_[0]->object_settings_dialog }); EVT_TOOL($self, TB_SETTINGS, sub { $_[0]->object_settings_dialog });
} else { } else {
EVT_BUTTON($self, $self->{btn_load}, \&load); EVT_BUTTON($self, $self->{btn_add}, \&add);
EVT_BUTTON($self, $self->{btn_remove}, sub { $self->remove() }); # explicitly pass no argument to remove EVT_BUTTON($self, $self->{btn_remove}, sub { $self->remove() }); # explicitly pass no argument to remove
EVT_BUTTON($self, $self->{btn_reset}, \&reset); EVT_BUTTON($self, $self->{btn_reset}, \&reset);
EVT_BUTTON($self, $self->{btn_arrange}, \&arrange); EVT_BUTTON($self, $self->{btn_arrange}, \&arrange);
@ -357,7 +357,7 @@ sub filament_presets {
return map $_->GetSelection, @{ $self->{preset_choosers}{filament} }; return map $_->GetSelection, @{ $self->{preset_choosers}{filament} };
} }
sub load { sub add {
my $self = shift; my $self = shift;
my $dir = $Slic3r::GUI::Settings->{recent}{skein_directory} || $Slic3r::GUI::Settings->{recent}{config_directory} || ''; my $dir = $Slic3r::GUI::Settings->{recent}{skein_directory} || $Slic3r::GUI::Settings->{recent}{config_directory} || '';
@ -375,7 +375,6 @@ sub load_file {
my $self = shift; my $self = shift;
my ($input_file) = @_; my ($input_file) = @_;
my $basename = basename($input_file);
$Slic3r::GUI::Settings->{recent}{skein_directory} = dirname($input_file); $Slic3r::GUI::Settings->{recent}{skein_directory} = dirname($input_file);
Slic3r::GUI->save_settings; Slic3r::GUI->save_settings;
@ -384,31 +383,32 @@ sub load_file {
local $SIG{__WARN__} = Slic3r::GUI::warning_catcher($self); local $SIG{__WARN__} = Slic3r::GUI::warning_catcher($self);
my $model = Slic3r::Model->read_from_file($input_file); my $model = Slic3r::Model->read_from_file($input_file);
for my $i (0 .. $#{$model->objects}) { $self->load_model_object($_) for @{$model->objects};
my $object = Slic3r::GUI::Plater::Object->new(
name => $basename,
input_file => $input_file,
input_file_object_id => $i,
model => $model,
model_object_idx => $i,
mesh_stats => $model->objects->[$i]->mesh_stats, # so that we can free model
instances => [
$model->objects->[$i]->instances
? (map $_->offset, @{$model->objects->[$i]->instances})
: Slic3r::Point->new(0,0),
],
);
# we only consider the rotation of the first instance for now
$object->rotate($model->objects->[$i]->instances->[0]->rotation)
if $model->objects->[$i]->instances;
push @{ $self->{objects} }, $object;
$self->object_loaded($#{ $self->{objects} }, no_arrange => (@{$object->instances} > 1));
}
$process_dialog->Destroy; $process_dialog->Destroy;
$self->statusbar->SetStatusText("Loaded $basename"); $self->statusbar->SetStatusText("Loaded " . basename($input_file));
}
sub load_model_object {
my ($self, $model_object) = @_;
my $o = $self->{model}->add_object($model_object);
push @{ $self->{objects} }, Slic3r::GUI::Plater::Object->new(
name => basename($model_object->input_file),
);
my $need_arrange = 0;
if (!defined $model_object->instances) {
# if object has no defined position(s) we need to rearrange everything after loading
$need_arrange = 1;
# add a default instance and center object around origin
$o->center_around_origin;
$o->add_instance(offset => [30,30]);
}
$self->object_loaded($#{ $self->{objects} }, no_arrange => !$need_arrange);
} }
sub object_loaded { sub object_loaded {
@ -416,15 +416,17 @@ sub object_loaded {
my ($obj_idx, %params) = @_; my ($obj_idx, %params) = @_;
my $object = $self->{objects}[$obj_idx]; my $object = $self->{objects}[$obj_idx];
my $model_object = $self->{model}->objects->[$obj_idx];
$self->{list}->InsertStringItem($obj_idx, $object->name); $self->{list}->InsertStringItem($obj_idx, $object->name);
$self->{list}->SetItemFont($obj_idx, Wx::Font->new(10, wxDEFAULT, wxNORMAL, wxNORMAL)) $self->{list}->SetItemFont($obj_idx, Wx::Font->new(10, wxDEFAULT, wxNORMAL, wxNORMAL))
if $self->{list}->can('SetItemFont'); # legacy code for wxPerl < 0.9918 not supporting SetItemFont() if $self->{list}->can('SetItemFont'); # legacy code for wxPerl < 0.9918 not supporting SetItemFont()
$self->{list}->SetItem($obj_idx, 1, $object->instances_count); $self->{list}->SetItem($obj_idx, 1, $model_object->instances_count);
$self->{list}->SetItem($obj_idx, 2, ($object->scale * 100) . "%"); $self->{list}->SetItem($obj_idx, 2, ($model_object->instances->[0]->scaling_factor * 100) . "%");
$self->make_thumbnail($obj_idx); $self->make_thumbnail($obj_idx);
$self->arrange unless $params{no_arrange}; $self->arrange unless $params{no_arrange};
$self->recenter;
$self->{list}->Update; $self->{list}->Update;
$self->{list}->Select($obj_idx, 1); $self->{list}->Select($obj_idx, 1);
$self->object_list_changed; $self->object_list_changed;
@ -440,11 +442,11 @@ sub remove {
} }
splice @{$self->{objects}}, $obj_idx, 1; splice @{$self->{objects}}, $obj_idx, 1;
$self->{model}->delete_object($obj_idx);
$self->{list}->DeleteItem($obj_idx); $self->{list}->DeleteItem($obj_idx);
$self->{selected_objects} = [];
$self->selection_changed(0);
$self->object_list_changed; $self->object_list_changed;
$self->select_object(undef);
$self->recenter; $self->recenter;
$self->{canvas}->Refresh; $self->{canvas}->Refresh;
} }
@ -453,11 +455,11 @@ sub reset {
my $self = shift; my $self = shift;
@{$self->{objects}} = (); @{$self->{objects}} = ();
$self->{model}->delete_all_objects;
$self->{list}->DeleteAllItems; $self->{list}->DeleteAllItems;
$self->{selected_objects} = [];
$self->selection_changed(0);
$self->object_list_changed; $self->object_list_changed;
$self->select_object(undef);
$self->{canvas}->Refresh; $self->{canvas}->Refresh;
} }
@ -465,10 +467,14 @@ sub increase {
my $self = shift; my $self = shift;
my ($obj_idx, $object) = $self->selected_object; my ($obj_idx, $object) = $self->selected_object;
my $instances = $object->instances; my $model_object = $self->{model}->objects->[$obj_idx];
push @$instances, my $new_instance = $instances->[-1]->clone; my $last_instance = $model_object->instances->[-1];
$new_instance->translate(scale(10), scale(10)); $model_object->add_instance(
$self->{list}->SetItem($obj_idx, 1, $object->instances_count); offset => [ map 10+$_, @{$last_instance->offset} ],
scaling_factor => $last_instance->scaling_factor,
rotation => $last_instance->rotation,
);
$self->{list}->SetItem($obj_idx, 1, $model_object->instances_count);
$self->arrange; $self->arrange;
} }
@ -476,9 +482,10 @@ sub decrease {
my $self = shift; my $self = shift;
my ($obj_idx, $object) = $self->selected_object; my ($obj_idx, $object) = $self->selected_object;
if ($object->instances_count >= 2) { my $model_object = $self->{model}->objects->[$obj_idx];
pop @{$object->instances}; if ($model_object->instances_count >= 2) {
$self->{list}->SetItem($obj_idx, 1, $object->instances_count); $model_object->delete_last_instance;
$self->{list}->SetItem($obj_idx, 1, $model_object->instances_count);
} else { } else {
$self->remove; $self->remove;
} }
@ -496,18 +503,25 @@ sub rotate {
my ($angle) = @_; my ($angle) = @_;
my ($obj_idx, $object) = $self->selected_object; my ($obj_idx, $object) = $self->selected_object;
my $model_object = $self->{model}->objects->[$obj_idx];
my $model_instance = $model_object->instances->[0];
# we need thumbnail to be computed before allowing rotation # we need thumbnail to be computed before allowing rotation
return if !$object->thumbnail; return if !$object->thumbnail;
if (!defined $angle) { if (!defined $angle) {
$angle = Wx::GetNumberFromUser("", "Enter the rotation angle:", "Rotate", $object->rotate, -364, 364, $self); $angle = Wx::GetNumberFromUser("", "Enter the rotation angle:", "Rotate", $model_instance->rotation, -364, 364, $self);
return if !$angle || $angle == -1; return if !$angle || $angle == -1;
$angle = 0 - $angle; # rotate clockwise (be consistent with button icon) $angle = 0 - $angle; # rotate clockwise (be consistent with button icon)
} }
$object->rotate($object->rotate + $angle); {
$self->selection_changed(1); # refresh info (size etc.) my $new_angle = $model_instance->rotation + $angle;
$_->rotation($new_angle) for @{ $model_object->instances };
$model_object->update_bounding_box;
$object->transform_thumbnail($self->{model}, $obj_idx);
}
$self->selection_changed; # refresh info (size etc.)
$self->recenter; $self->recenter;
$self->{canvas}->Refresh; $self->{canvas}->Refresh;
} }
@ -516,16 +530,28 @@ sub changescale {
my $self = shift; my $self = shift;
my ($obj_idx, $object) = $self->selected_object; my ($obj_idx, $object) = $self->selected_object;
my $model_object = $self->{model}->objects->[$obj_idx];
my $model_instance = $model_object->instances->[0];
# we need thumbnail to be computed before allowing scaling # we need thumbnail to be computed before allowing scaling
return if !$object->thumbnail; return if !$object->thumbnail;
# max scale factor should be above 2540 to allow importing files exported in inches # max scale factor should be above 2540 to allow importing files exported in inches
my $scale = Wx::GetNumberFromUser("", "Enter the scale % for the selected object:", "Scale", $object->scale*100, 0, 100000, $self); my $scale = Wx::GetNumberFromUser("", "Enter the scale % for the selected object:", "Scale", $model_instance->scaling_factor*100, 0, 100000, $self);
return if !$scale || $scale == -1; return if !$scale || $scale == -1;
$self->{list}->SetItem($obj_idx, 2, "$scale%"); $self->{list}->SetItem($obj_idx, 2, "$scale%");
$object->changescale($scale / 100); $scale /= 100; # turn percent into factor
{
my $variation = $scale / $model_instance->scaling_factor;
foreach my $range (@{ $model_object->layer_height_ranges }) {
$range->[0] *= $variation;
$range->[1] *= $variation;
}
$_->scaling_factor($scale) for @{ $model_object->instances };
$model_object->update_bounding_box;
$object->transform_thumbnail($self->{model}, $obj_idx);
}
$self->selection_changed(1); # refresh info (size, volume etc.) $self->selection_changed(1); # refresh info (size, volume etc.)
$self->arrange; $self->arrange;
} }
@ -533,19 +559,8 @@ sub changescale {
sub arrange { sub arrange {
my $self = shift; my $self = shift;
my $total_parts = sum(map $_->instances_count, @{$self->{objects}}) or return;
my @size = ();
for my $a (X,Y) {
$size[$a] = max(map $_->transformed_size->[$a], @{$self->{objects}});
}
eval { eval {
my $config = $self->skeinpanel->config; $self->{model}->arrange_objects($self->skeinpanel->config);
my @positions = Slic3r::Geometry::arrange
($total_parts, @size, @{$config->bed_size}, $config->min_object_distance, $self->skeinpanel->config);
@$_ = @{shift @positions}
for map @{$_->instances}, @{$self->{objects}};
}; };
# ignore arrange warnings on purpose # ignore arrange warnings on purpose
@ -556,54 +571,56 @@ sub arrange {
sub split_object { sub split_object {
my $self = shift; my $self = shift;
my ($obj_idx, $current_object) = $self->selected_object; my ($obj_idx, $current_object) = $self->selected_object;
my $current_copies_num = $current_object->instances_count; my $current_model_object = $self->{model}->objects->[$obj_idx];
my $model_object = $current_object->get_model_object;
if (@{$model_object->volumes} > 1) { if (@{$current_model_object->volumes} > 1) {
Slic3r::GUI::warning_catcher($self)->("The selected object couldn't be split because it contains more than one volume/material."); Slic3r::GUI::warning_catcher($self)->("The selected object couldn't be split because it contains more than one volume/material.");
return; return;
} }
my $mesh = $model_object->mesh; my @new_meshes = @{$current_model_object->volumes->[0]->mesh->split};
$mesh->align_to_origin;
$mesh->repair;
my @new_meshes = @{$mesh->split};
if (@new_meshes == 1) { if (@new_meshes == 1) {
Slic3r::GUI::warning_catcher($self)->("The selected object couldn't be split because it already contains a single part."); Slic3r::GUI::warning_catcher($self)->("The selected object couldn't be split because it already contains a single part.");
return; return;
} }
# remove the original object before spawning the object_loaded event, otherwise # remove the original object before spawning the object_loaded event, otherwise
# we'll pass the wrong $obj_idx to it (which won't be recognized after the # we'll pass the wrong $obj_idx to it (which won't be recognized after the
# thumbnail thread returns) # thumbnail thread returns)
$self->remove($obj_idx); $self->remove($obj_idx);
$current_object = $obj_idx = undef;
# create a bogus Model object, we only need to instantiate the new Model::Object objects # create a bogus Model object, we only need to instantiate the new Model::Object objects
my $new_model = Slic3r::Model->new; my $new_model = Slic3r::Model->new;
foreach my $mesh (@new_meshes) { foreach my $mesh (@new_meshes) {
$mesh->repair; my $model_object = $new_model->add_object(
my $bb = $mesh->bounding_box; input_file => $current_model_object->input_file,
my $model_object = $new_model->add_object; config => $current_model_object->config->clone,
$model_object->add_volume(mesh => $mesh); layer_height_ranges => $current_model_object->layer_height_ranges, # TODO: clone this
my $object = Slic3r::GUI::Plater::Object->new( material_mapping => $current_model_object->material_mapping, # TODO: clone this
name => basename($current_object->input_file), );
input_file => $current_object->input_file, $model_object->add_volume(
input_file_object_id => undef, mesh => $mesh,
model => $new_model, material_id => $current_model_object->volumes->[0]->material_id,
model_object_idx => $#{$new_model->objects},
mesh_stats => $mesh->stats, # so that we can free model
instances => [ map $bb->min_point->pp, 1..$current_copies_num ],
); );
push @{ $self->{objects} }, $object; for my $instance_idx (0..$#{ $current_model_object->instances }) {
$self->object_loaded($#{ $self->{objects} }, no_arrange => 1); my $current_instance = $current_model_object->instances->[$instance_idx];
$model_object->add_instance(
offset => [
$current_instance->offset->[X] + ($instance_idx * 10),
$current_instance->offset->[Y] + ($instance_idx * 10),
],
rotation => $current_instance->rotation,
scaling_factor => $current_instance->scaling_factor,
);
}
# we need to center this single object around origin
$model_object->center_around_origin;
$self->load_model_object($model_object);
} }
$self->recenter;
$self->{canvas}->Refresh;
} }
sub export_gcode { sub export_gcode {
@ -704,8 +721,7 @@ sub export_gcode2 {
eval { eval {
$print->config->validate; $print->config->validate;
my $model = $self->make_model; $print->add_model_object($_) for @{ $self->{model}->objects };
$print->add_model_object($_) for @{$model->objects};
$print->validate; $print->validate;
{ {
@ -765,7 +781,7 @@ sub export_stl {
my $self = shift; my $self = shift;
my $output_file = $self->_get_export_file('STL') or return; my $output_file = $self->_get_export_file('STL') or return;
Slic3r::Format::STL->write_file($output_file, $self->make_model, binary => 1); Slic3r::Format::STL->write_file($output_file, $self->{model}, binary => 1);
$self->statusbar->SetStatusText("STL file exported to $output_file"); $self->statusbar->SetStatusText("STL file exported to $output_file");
} }
@ -773,7 +789,7 @@ sub export_amf {
my $self = shift; my $self = shift;
my $output_file = $self->_get_export_file('AMF') or return; my $output_file = $self->_get_export_file('AMF') or return;
Slic3r::Format::AMF->write_file($output_file, $self->make_model); Slic3r::Format::AMF->write_file($output_file, $self->{model});
$self->statusbar->SetStatusText("AMF file exported to $output_file"); $self->statusbar->SetStatusText("AMF file exported to $output_file");
} }
@ -785,7 +801,7 @@ sub _get_export_file {
my $output_file = $main::opt{output}; my $output_file = $main::opt{output};
{ {
$output_file = $self->skeinpanel->init_print->expanded_output_filepath($output_file, $self->{objects}[0]->input_file); $output_file = $self->skeinpanel->init_print->expanded_output_filepath($output_file, $self->{model}->objects->[0]->input_file);
$output_file =~ s/\.gcode$/$suffix/i; $output_file =~ s/\.gcode$/$suffix/i;
my $dlg = Wx::FileDialog->new($self, "Save $format file as:", dirname($output_file), my $dlg = Wx::FileDialog->new($self, "Save $format file as:", dirname($output_file),
basename($output_file), &Slic3r::GUI::SkeinPanel::MODEL_WILDCARD, wxFD_SAVE | wxFD_OVERWRITE_PROMPT); basename($output_file), &Slic3r::GUI::SkeinPanel::MODEL_WILDCARD, wxFD_SAVE | wxFD_OVERWRITE_PROMPT);
@ -799,47 +815,14 @@ sub _get_export_file {
return $output_file; return $output_file;
} }
sub make_model {
my $self = shift;
my $model = Slic3r::Model->new;
foreach my $plater_object (@{$self->{objects}}) {
my $model_object = $plater_object->get_model_object;
my $new_model_object = $model->add_object(
input_file => $plater_object->input_file,
config => $plater_object->config,
layer_height_ranges => $plater_object->layer_height_ranges,
material_mapping => $plater_object->material_mapping,
);
foreach my $volume (@{$model_object->volumes}) {
$new_model_object->add_volume(
material_id => $volume->material_id,
mesh => $volume->mesh,
);
$model->set_material($volume->material_id || 0, {});
}
$new_model_object->align_to_origin;
$new_model_object->add_instance(
rotation => $plater_object->rotate, # around center point
scaling_factor => $plater_object->scale,
offset => Slic3r::Point->new(@$_),
) for @{$plater_object->instances};
}
$model->align_to_origin;
return $model;
}
sub make_thumbnail { sub make_thumbnail {
my $self = shift; my $self = shift;
my ($obj_idx) = @_; my ($obj_idx) = @_;
my $object = $self->{objects}[$obj_idx]; my $plater_object = $self->{objects}[$obj_idx];
$object->thumbnail_scaling_factor($self->{scaling_factor}); $plater_object->thumbnail(Slic3r::ExPolygon::Collection->new);
$object->thumbnail(Slic3r::ExPolygon::Collection->new);
my $cb = sub { my $cb = sub {
$object->make_thumbnail; $plater_object->make_thumbnail($self->{model}, $obj_idx);
if ($Slic3r::have_threads) { if ($Slic3r::have_threads) {
Wx::PostEvent($self, Wx::PlThreadEvent->new(-1, $THUMBNAIL_DONE_EVENT, shared_clone([ $obj_idx ]))); Wx::PostEvent($self, Wx::PlThreadEvent->new(-1, $THUMBNAIL_DONE_EVENT, shared_clone([ $obj_idx ])));
@ -858,33 +841,38 @@ sub on_thumbnail_made {
my $self = shift; my $self = shift;
my ($obj_idx) = @_; my ($obj_idx) = @_;
$self->{objects}[$obj_idx]->_transform_thumbnail; $self->{objects}[$obj_idx]->transform_thumbnail($self->{model}, $obj_idx);
$self->recenter; $self->recenter;
$self->{canvas}->Refresh; $self->{canvas}->Refresh;
} }
sub clean_instance_thumbnails {
my ($self) = @_;
foreach my $object (@{ $self->{objects} }) {
@{ $object->instance_thumbnails } = ();
}
}
# this method gets called whenever bed is resized or the objects' bounding box changes
# (i.e. when an object is added/removed/moved/rotated/scaled)
sub recenter { sub recenter {
my $self = shift; my $self = shift;
return unless @{$self->{objects}}; return unless @{$self->{objects}};
# calculate displacement needed to center the print # get model bounding box in pixels
my @bbs = (); my $print_bb = $self->{model}->bounding_box;
foreach my $plobj (@{$self->{objects}}) { $print_bb->scale($self->{scaling_factor});
my $bb = $plobj->transformed_bounding_box; # in scaled coordinates
foreach my $inst (@{$plobj->instances}) { # get model size in pixels
push @bbs, my $inst_bb = $bb->clone;
$inst_bb->translate(@$inst, 0);
}
}
my $print_bb = Slic3r::Geometry::BoundingBox->merge(@bbs);
my $print_size = $print_bb->size; my $print_size = $print_bb->size;
# $self->{shift} contains the offset in pixels to add to object instances in order to center them # $self->{shift} contains the offset in pixels to add to object thumbnails
# it is expressed in upwards Y # in order to center them
$self->{shift} = [ $self->{shift} = [
$self->to_pixel(-$print_bb->x_min) + ($self->{canvas}->GetSize->GetWidth - $self->to_pixel($print_size->[X])) / 2, -$print_bb->x_min + ($self->{canvas}->GetSize->GetWidth - $print_size->[X]) / 2,
$self->to_pixel(-$print_bb->y_min) + ($self->{canvas}->GetSize->GetHeight - $self->to_pixel($print_size->[Y])) / 2, -$print_bb->y_min + ($self->{canvas}->GetSize->GetHeight - $print_size->[Y]) / 2,
]; ];
} }
@ -922,7 +910,6 @@ sub _update_bed_size {
# when the canvas is not rendered yet, its GetSize() method returns 0,0 # when the canvas is not rendered yet, its GetSize() method returns 0,0
# scaling_factor is expressed in pixel / mm # scaling_factor is expressed in pixel / mm
$self->{scaling_factor} = CANVAS_SIZE->[X] / max(@{ $self->{config}->bed_size }); $self->{scaling_factor} = CANVAS_SIZE->[X] / max(@{ $self->{config}->bed_size });
$_->thumbnail_scaling_factor($self->{scaling_factor}) for @{ $self->{objects} };
$self->recenter; $self->recenter;
} }
@ -972,55 +959,60 @@ sub repaint {
# draw thumbnails # draw thumbnails
$dc->SetPen(wxBLACK_PEN); $dc->SetPen(wxBLACK_PEN);
@{$parent->{object_previews}} = (); $parent->clean_instance_thumbnails;
for my $obj_idx (0 .. $#{$parent->{objects}}) { for my $obj_idx (0 .. $#{$parent->{objects}}) {
my $object = $parent->{objects}[$obj_idx]; my $object = $parent->{objects}[$obj_idx];
my $model_object = $parent->{model}->objects->[$obj_idx];
next unless defined $object->thumbnail; next unless defined $object->thumbnail;
for my $instance_idx (0 .. $#{$object->instances}) { for my $instance_idx (0 .. $#{$model_object->instances}) {
my $instance = $object->instances->[$instance_idx]; my $instance = $model_object->instances->[$instance_idx];
my $thumbnail = $object->transformed_thumbnail->clone; my $thumbnail = $object->transformed_thumbnail->clone; # in scaled coordinates
$thumbnail->translate(map $_ * $parent->{scaling_factor}, @$instance); $thumbnail->scale(&Slic3r::SCALING_FACTOR * $parent->{scaling_factor}); # in unscaled pixels
$thumbnail->translate(map scale($parent->{shift}[$_]), (X,Y)); $thumbnail->translate(map $_ * $parent->{scaling_factor}, @{$instance->offset});
push @{$parent->{object_previews}}, [ $obj_idx, $instance_idx, $thumbnail ]; # $thumbnail has scaled coordinates $thumbnail->translate(@{$parent->{shift}});
my $drag_object = $self->{drag_object}; $object->instance_thumbnails->[$instance_idx] = $thumbnail;
if (defined $drag_object && $obj_idx == $drag_object->[0] && $instance_idx == $drag_object->[1]) {
if (defined $self->{drag_object} && $self->{drag_object}[0] == $obj_idx && $self->{drag_object}[1] == $instance_idx) {
$dc->SetBrush($parent->{dragged_brush}); $dc->SetBrush($parent->{dragged_brush});
} elsif (grep { $_->[0] == $obj_idx } @{$parent->{selected_objects}}) { } elsif ($object->selected) {
$dc->SetBrush($parent->{selected_brush}); $dc->SetBrush($parent->{selected_brush});
} else { } else {
$dc->SetBrush($parent->{objects_brush}); $dc->SetBrush($parent->{objects_brush});
} }
foreach my $expolygon (@$thumbnail) { foreach my $expolygon (@$thumbnail) {
my $points = $expolygon->contour->pp; my $points = $expolygon->contour->pp;
foreach my $point (@$points) {
$point->[X] *= &Slic3r::SCALING_FACTOR;
$point->[Y] *= &Slic3r::SCALING_FACTOR;
}
$dc->DrawPolygon($parent->_y($points), 0, 0); $dc->DrawPolygon($parent->_y($points), 0, 0);
} }
# if sequential printing is enabled and we have more than one object if (0) {
if ($parent->{config}->complete_objects && (map @{$_->instances}, @{$parent->{objects}}) > 1) { # draw bounding box for debugging purposes
my @points = map @{$_->contour}, @{$parent->{object_previews}->[-1][2]}; my $bb = $model_object->instance_bounding_box($instance_idx);
if (@points >= 3) { $bb->scale($parent->{scaling_factor});
my ($clearance) = @{offset([convex_hull(\@points)], scale($parent->{config}->extruder_clearance_radius / 2) * $parent->{scaling_factor}, 100, JT_ROUND)}; # no need to translate by instance offset because instance_bounding_box() does that
$clearance->scale(&Slic3r::SCALING_FACTOR); $bb->translate(@{$parent->{shift}}, 0);
$dc->SetPen($parent->{clearance_pen}); my $points = $bb->polygon->pp;
$dc->SetBrush($parent->{transparent_brush}); $dc->SetPen($parent->{clearance_pen});
$dc->DrawPolygon($parent->_y($clearance), 0, 0); $dc->SetBrush($parent->{transparent_brush});
} $dc->DrawPolygon($parent->_y($points), 0, 0);
}
# if sequential printing is enabled and we have more than one object, draw clearance area
if ($parent->{config}->complete_objects && (map @{$_->instances}, @{$parent->{model}->objects}) > 1) {
my ($clearance) = @{offset([$thumbnail->convex_hull], ($parent->{config}->extruder_clearance_radius / 2) * $parent->{scaling_factor}, 100, JT_ROUND)};
$dc->SetPen($parent->{clearance_pen});
$dc->SetBrush($parent->{transparent_brush});
$dc->DrawPolygon($parent->_y($clearance), 0, 0);
} }
} }
} }
# draw skirt # draw skirt
if (@{$parent->{object_previews}} && $parent->{config}->skirts) { if (@{$parent->{objects}} && $parent->{config}->skirts) {
my @points = map @{$_->contour}, map @{$_->[2]}, @{$parent->{object_previews}}; my @points = map @{$_->contour}, map @$_, map @{$_->instance_thumbnails}, @{$parent->{objects}};
if (@points >= 3) { if (@points >= 3) {
my ($convex_hull) = @{offset([convex_hull(\@points)], scale($parent->{config}->skirt_distance) * $parent->{scaling_factor}, 1, JT_ROUND)}; my ($convex_hull) = @{offset([convex_hull(\@points)], $parent->{config}->skirt_distance * $parent->{scaling_factor}, 100, JT_ROUND)};
$convex_hull->scale(&Slic3r::SCALING_FACTOR);
$dc->SetPen($parent->{skirt_pen}); $dc->SetPen($parent->{skirt_pen});
$dc->SetBrush($parent->{transparent_brush}); $dc->SetBrush($parent->{transparent_brush});
$dc->DrawPolygon($parent->_y($convex_hull), 0, 0); $dc->DrawPolygon($parent->_y($convex_hull), 0, 0);
@ -1035,20 +1027,22 @@ sub mouse_event {
my $parent = $self->GetParent; my $parent = $self->GetParent;
my $point = $event->GetPosition; my $point = $event->GetPosition;
my $pos = Slic3r::Point->new_scale(@{$parent->_y([[$point->x, $point->y]])->[0]}); #]] in scaled pixels my $pos = Slic3r::Point->new(@{$parent->_y([[$point->x, $point->y]])->[0]}); # in pixels
if ($event->ButtonDown(&Wx::wxMOUSE_BTN_LEFT)) { if ($event->ButtonDown(&Wx::wxMOUSE_BTN_LEFT)) {
$parent->{selected_objects} = []; $parent->select_object(undef);
$parent->{list}->Select($parent->{list}->GetFirstSelected, 0); for my $obj_idx (0 .. $#{$parent->{objects}}) {
$parent->selection_changed(0); my $object = $parent->{objects}->[$obj_idx];
for my $preview (@{$parent->{object_previews}}) { for my $instance_idx (0 .. $#{ $object->instance_thumbnails }) {
my ($obj_idx, $instance_idx, $thumbnail) = @$preview; my $thumbnail = $object->instance_thumbnails->[$instance_idx];
if (defined first { $_->contour->contains_point($pos) } @$thumbnail) { if ($thumbnail->contains_point($pos)) {
$parent->{selected_objects} = [ [$obj_idx, $instance_idx] ]; $parent->select_object($obj_idx);
$parent->{list}->Select($obj_idx, 1); my $instance = $parent->{model}->objects->[$obj_idx]->instances->[$instance_idx];
$parent->selection_changed(1); $self->{drag_start_pos} = [ # displacement between the click and the instance origin
my $instance = $parent->{objects}[$obj_idx]->instances->[$instance_idx]; $pos->x - $parent->{shift}[X] - ($instance->offset->[X] * $parent->{scaling_factor}),
$self->{drag_start_pos} = [ map $pos->[$_] - scale($parent->{shift}[$_]) - scale($parent->to_pixel($instance->[$_])), X,Y ]; # displacement between the click and the instance origin $pos->y - $parent->{shift}[Y] - ($instance->offset->[Y] * $parent->{scaling_factor}),
$self->{drag_object} = $preview; ];
$self->{drag_object} = [ $obj_idx, $instance_idx ];
}
} }
} }
$parent->Refresh; $parent->Refresh;
@ -1059,23 +1053,21 @@ sub mouse_event {
$self->{drag_object} = undef; $self->{drag_object} = undef;
$self->SetCursor(wxSTANDARD_CURSOR); $self->SetCursor(wxSTANDARD_CURSOR);
} elsif ($event->ButtonDClick) { } elsif ($event->ButtonDClick) {
$parent->object_preview_dialog if @{$parent->{selected_objects}}; $parent->object_preview_dialog if $parent->selected_object;
} elsif ($event->Dragging) { } elsif ($event->Dragging) {
return if !$self->{drag_start_pos}; # concurrency problems return if !$self->{drag_start_pos}; # concurrency problems
for my $preview ($self->{drag_object}) { my ($obj_idx, $instance_idx) = @{ $self->{drag_object} };
my ($obj_idx, $instance_idx, $thumbnail) = @$preview; my $model_object = $parent->{model}->objects->[$obj_idx];
$parent->{objects}[$obj_idx]->instances->[$instance_idx] = Slic3r::Point->new( $model_object->instances->[$instance_idx]->offset([
map { $parent->to_units(unscale($pos->[$_] - $self->{drag_start_pos}[$_]) - $parent->{shift}[$_]) } (X,Y), ($pos->[X] - $self->{drag_start_pos}[X] - $parent->{shift}[X]) / $parent->{scaling_factor},
); ($pos->[Y] - $self->{drag_start_pos}[Y] - $parent->{shift}[Y]) / $parent->{scaling_factor},
$parent->Refresh; ]);
} $model_object->update_bounding_box;
$parent->Refresh;
} elsif ($event->Moving) { } elsif ($event->Moving) {
my $cursor = wxSTANDARD_CURSOR; my $cursor = wxSTANDARD_CURSOR;
for my $preview (@{$parent->{object_previews}}) { if (defined first { $_->contains_point($pos) } map @{$_->instance_thumbnails}, @{ $parent->{objects} }) {
if (defined first { $_->contour->contains_point($pos) } @{ $preview->[2] }) { $cursor = Wx::Cursor->new(wxCURSOR_HAND);
$cursor = Wx::Cursor->new(wxCURSOR_HAND);
last;
}
} }
$self->SetCursor($cursor); $self->SetCursor($cursor);
} }
@ -1085,9 +1077,8 @@ sub list_item_deselected {
my ($self, $event) = @_; my ($self, $event) = @_;
if ($self->{list}->GetFirstSelected == -1) { if ($self->{list}->GetFirstSelected == -1) {
$self->{selected_objects} = []; $self->select_object(undef);
$self->{canvas}->Refresh; $self->{canvas}->Refresh;
$self->selection_changed(0);
} }
} }
@ -1095,9 +1086,8 @@ sub list_item_selected {
my ($self, $event) = @_; my ($self, $event) = @_;
my $obj_idx = $event->GetIndex; my $obj_idx = $event->GetIndex;
$self->{selected_objects} = [ grep $_->[0] == $obj_idx, @{$self->{object_previews}} ]; $self->select_object($obj_idx);
$self->{canvas}->Refresh; $self->{canvas}->Refresh;
$self->selection_changed(1);
} }
sub list_item_activated { sub list_item_activated {
@ -1121,7 +1111,8 @@ sub object_preview_dialog {
} }
my $dlg = Slic3r::GUI::Plater::ObjectPreviewDialog->new($self, my $dlg = Slic3r::GUI::Plater::ObjectPreviewDialog->new($self,
object => $self->{objects}[$obj_idx], object => $self->{objects}[$obj_idx],
model_object => $self->{model}->objects->[$obj_idx],
); );
$dlg->ShowModal; $dlg->ShowModal;
} }
@ -1140,7 +1131,8 @@ sub object_settings_dialog {
return unless $self->validate_config; return unless $self->validate_config;
my $dlg = Slic3r::GUI::Plater::ObjectSettingsDialog->new($self, my $dlg = Slic3r::GUI::Plater::ObjectSettingsDialog->new($self,
object => $self->{objects}[$obj_idx], object => $self->{objects}[$obj_idx],
model_object => $self->{model}->objects->[$obj_idx],
); );
$dlg->ShowModal; $dlg->ShowModal;
} }
@ -1161,7 +1153,9 @@ sub object_list_changed {
sub selection_changed { sub selection_changed {
my $self = shift; my $self = shift;
my ($have_sel) = @_;
my ($obj_idx, $object) = $self->selected_object;
my $have_sel = defined $obj_idx;
my $method = $have_sel ? 'Enable' : 'Disable'; my $method = $have_sel ? 'Enable' : 'Disable';
$self->{"btn_$_"}->$method $self->{"btn_$_"}->$method
@ -1174,13 +1168,14 @@ sub selection_changed {
if ($self->{object_info_size}) { # have we already loaded the info pane? if ($self->{object_info_size}) { # have we already loaded the info pane?
if ($have_sel) { if ($have_sel) {
my ($obj_idx, $object) = $self->selected_object; my $model_object = $self->{model}->objects->[$obj_idx];
$self->{object_info_size}->SetLabel(sprintf("%.2f x %.2f x %.2f", map unscale($_), @{$object->transformed_size})); my $model_instance = $model_object->instances->[0];
$self->{object_info_materials}->SetLabel($object->materials); $self->{object_info_size}->SetLabel(sprintf("%.2f x %.2f x %.2f", @{$model_object->instance_bounding_box(0)->size}));
$self->{object_info_materials}->SetLabel($model_object->materials_count);
if (my $stats = $object->mesh_stats) { if (my $stats = $model_object->mesh_stats) {
$self->{object_info_volume}->SetLabel(sprintf('%.2f', $stats->{volume} * ($object->scale**3))); $self->{object_info_volume}->SetLabel(sprintf('%.2f', $stats->{volume} * ($model_instance->scaling_factor**3)));
$self->{object_info_facets}->SetLabel(sprintf('%d (%d shells)', $object->facets, $stats->{number_of_parts})); $self->{object_info_facets}->SetLabel(sprintf('%d (%d shells)', $model_object->facets_count, $stats->{number_of_parts}));
if (my $errors = sum(@$stats{qw(degenerate_facets edges_fixed facets_removed facets_added facets_reversed backwards_edges)})) { if (my $errors = sum(@$stats{qw(degenerate_facets edges_fixed facets_removed facets_added facets_reversed backwards_edges)})) {
$self->{object_info_manifold}->SetLabel(sprintf("Auto-repaired (%d errors)", $errors)); $self->{object_info_manifold}->SetLabel(sprintf("Auto-repaired (%d errors)", $errors));
$self->{object_info_manifold_warning_icon}->Show; $self->{object_info_manifold_warning_icon}->Show;
@ -1206,9 +1201,24 @@ sub selection_changed {
} }
} }
sub select_object {
my ($self, $obj_idx) = @_;
$_->selected(0) for @{ $self->{objects} };
if (defined $obj_idx) {
$self->{objects}->[$obj_idx]->selected(1);
$self->{list}->Select($obj_idx, 1);
} else {
# TODO: deselect all in list
}
$self->selection_changed(1);
}
sub selected_object { sub selected_object {
my $self = shift; my $self = shift;
my $obj_idx = $self->{selected_objects}[0] ? $self->{selected_objects}[0][0] : $self->{list}->GetFirstSelected;
my $obj_idx = first { $self->{objects}[$_]->selected } 0..$#{ $self->{objects} };
return undef if !defined $obj_idx;
return ($obj_idx, $self->{objects}[$obj_idx]), return ($obj_idx, $self->{objects}[$obj_idx]),
} }
@ -1277,155 +1287,47 @@ use List::Util qw(first);
use Slic3r::Geometry qw(X Y Z MIN MAX deg2rad); use Slic3r::Geometry qw(X Y Z MIN MAX deg2rad);
has 'name' => (is => 'rw', required => 1); has 'name' => (is => 'rw', required => 1);
has 'input_file' => (is => 'rw', required => 1); has 'thumbnail' => (is => 'rw'); # ExPolygon::Collection in scaled model units with no transforms
has 'input_file_object_id' => (is => 'rw'); # undef means keep model object
has 'model' => (is => 'rw', required => 1, trigger => \&_trigger_model_object);
has 'model_object_idx' => (is => 'rw', required => 1, trigger => \&_trigger_model_object);
has 'bounding_box' => (is => 'rw'); # scaled 3D bb of original object (aligned to origin) with no rotation or scaling
has 'convex_hull' => (is => 'rw'); # scaled 2D convex hull of original object (aligned to origin) with no rotation or scaling
has 'scale' => (is => 'rw', default => sub { 1 }, trigger => \&_transform_thumbnail);
has 'rotate' => (is => 'rw', default => sub { 0 }, trigger => \&_transform_thumbnail); # around object center point
has 'instances' => (is => 'rw', default => sub { [] }); # upward Y axis
has 'thumbnail' => (is => 'rw', trigger => \&_transform_thumbnail);
has 'transformed_thumbnail' => (is => 'rw'); has 'transformed_thumbnail' => (is => 'rw');
has 'thumbnail_scaling_factor' => (is => 'rw', trigger => \&_transform_thumbnail); has 'instance_thumbnails' => (is => 'ro', default => sub { [] }); # array of ExPolygon::Collection objects, each one representing the actual placed thumbnail of each instance in pixel units
has 'config' => (is => 'rw', default => sub { Slic3r::Config->new }); has 'selected' => (is => 'rw', default => sub { 0 });
has 'layer_height_ranges' => (is => 'rw', default => sub { [] }); # [ z_min, z_max, layer_height ]
has 'material_mapping' => (is => 'rw', default => sub { {} }); # { material_id => region_id }
has 'mesh_stats' => (is => 'ro', required => 1);
# statistics
has 'facets' => (is => 'rw');
has 'vertices' => (is => 'rw');
has 'materials' => (is => 'rw');
has 'is_manifold' => (is => 'rw');
sub _trigger_model_object {
my $self = shift;
if ($self->model && defined $self->model_object_idx) {
my $model_object = $self->model->objects->[$self->model_object_idx];
$model_object->align_to_origin;
$self->bounding_box($model_object->bounding_box);
$self->bounding_box->scale(1 / &Slic3r::SCALING_FACTOR);
my $mesh = $model_object->mesh;
$mesh->repair;
$self->convex_hull($mesh->convex_hull);
$self->facets($mesh->facets_count);
$self->vertices(scalar @{$mesh->vertices});
$self->materials($model_object->materials_count);
}
}
sub changescale {
my $self = shift;
my ($scale) = @_;
my $variation = $scale / $self->scale;
foreach my $range (@{ $self->layer_height_ranges }) {
$range->[0] *= $variation;
$range->[1] *= $variation;
}
$self->scale($scale);
}
sub needed_repair {
my $self = shift;
if ($self->get_model_object->needed_repair) {
warn "Warning: the input file contains manifoldness errors. "
. "Slic3r repaired it successfully by guessing what the correct shape should be, "
. "but you might still want to inspect the G-code before printing.\n";
$self->is_manifold(0);
} else {
$self->is_manifold(1);
}
return $self->is_manifold;
}
sub free_model_object {
my $self = shift;
# only delete mesh from memory if we can retrieve it from the original file
return unless $self->input_file && defined $self->input_file_object_id;
$self->model(undef);
$self->model_object_idx(undef);
}
sub get_model_object {
my $self = shift;
if ($self->model) {
return $self->model->objects->[$self->model_object_idx];
}
return Slic3r::Model
->read_from_file($self->input_file)
->objects
->[$self->input_file_object_id];
}
sub instances_count {
my $self = shift;
return scalar @{$self->instances};
}
sub make_thumbnail { sub make_thumbnail {
my $self = shift; my ($self, $model, $obj_idx) = @_;
my $mesh = $self->get_model_object->mesh; # $self->model_object is already aligned to origin my $mesh = $model->objects->[$obj_idx]->raw_mesh;
$mesh->repair;
if (@{$mesh->facets} <= 5000) { if ($mesh->facets_count <= 5000) {
# remove polygons with area <= 1mm # remove polygons with area <= 1mm
my $area_threshold = Slic3r::Geometry::scale 1; my $area_threshold = Slic3r::Geometry::scale 1;
$self->thumbnail->append( $self->thumbnail->append(
grep $_->area >= $area_threshold, grep $_->area >= $area_threshold,
@{ $mesh->horizontal_projection }, @{ $mesh->horizontal_projection }, # horizontal_projection returns scaled expolygons
); );
$self->thumbnail->simplify(0.5); $self->thumbnail->simplify(0.5);
} else { } else {
my $convex_hull = Slic3r::ExPolygon->new($self->convex_hull); my $convex_hull = Slic3r::ExPolygon->new($mesh->convex_hull);
$self->thumbnail->append($convex_hull); $self->thumbnail->append($convex_hull);
} }
return $self->thumbnail; return $self->thumbnail;
} }
sub _transform_thumbnail { sub transform_thumbnail {
my $self = shift; my ($self, $model, $obj_idx) = @_;
return unless defined $self->thumbnail; return unless defined $self->thumbnail;
my $t = $self->_apply_transform($self->thumbnail);
$t->scale($self->thumbnail_scaling_factor); my $model_object = $model->objects->[$obj_idx];
my $model_instance = $model_object->instances->[0];
# the order of these transformations MUST be the same everywhere, including
# in Slic3r::Print->add_model_object()
my $t = $self->thumbnail->clone;
$t->rotate(deg2rad($model_instance->rotation), Slic3r::Point->new(0,0));
$t->scale($model_instance->scaling_factor);
$self->transformed_thumbnail($t); $self->transformed_thumbnail($t);
} }
# bounding box with applied rotation and scaling
sub transformed_bounding_box {
my $self = shift;
my $bb = Slic3r::Geometry::BoundingBox->new_from_points($self->_apply_transform($self->convex_hull));
$bb->extents->[Z] = $self->bounding_box->clone->extents->[Z];
$bb->extents->[Z][MAX] *= $self->scale;
return $bb;
}
sub _apply_transform {
my $self = shift;
my ($entity) = @_; # can be anything that implements ->clone(), ->rotate() and ->scale()
# the order of these transformations MUST be the same everywhere, including
# in Slic3r::Print->add_model()
my $result = $entity->clone;
$result->rotate(deg2rad($self->rotate), $self->bounding_box->center_2D);
$result->scale($self->scale);
return $result;
}
sub transformed_size {
my $self = shift;
return $self->transformed_bounding_box->size;
}
1; 1;

View File

@ -11,10 +11,10 @@ sub new {
my $class = shift; my $class = shift;
my ($parent, %params) = @_; my ($parent, %params) = @_;
my $self = $class->SUPER::new($parent, -1, $params{object}->name, wxDefaultPosition, [500,500], wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER); my $self = $class->SUPER::new($parent, -1, $params{object}->name, wxDefaultPosition, [500,500], wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER);
$self->{object} = $params{object}; $self->{model_object} = $params{model_object};
my $sizer = Wx::BoxSizer->new(wxVERTICAL); my $sizer = Wx::BoxSizer->new(wxVERTICAL);
$sizer->Add(Slic3r::GUI::PreviewCanvas->new($self, $self->{object}->get_model_object), 1, wxEXPAND, 0); $sizer->Add(Slic3r::GUI::PreviewCanvas->new($self, $self->{model_object}), 1, wxEXPAND, 0);
$self->SetSizer($sizer); $self->SetSizer($sizer);
$self->SetMinSize($self->GetSize); $self->SetMinSize($self->GetSize);

View File

@ -11,12 +11,12 @@ sub new {
my $class = shift; my $class = shift;
my ($parent, %params) = @_; my ($parent, %params) = @_;
my $self = $class->SUPER::new($parent, -1, "Settings for " . $params{object}->name, wxDefaultPosition, [500,500], wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER); my $self = $class->SUPER::new($parent, -1, "Settings for " . $params{object}->name, wxDefaultPosition, [500,500], wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER);
$self->{object} = $params{object}; $self->{$_} = $params{$_} for keys %params;
$self->{tabpanel} = Wx::Notebook->new($self, -1, wxDefaultPosition, wxDefaultSize, wxNB_TOP | wxTAB_TRAVERSAL); $self->{tabpanel} = Wx::Notebook->new($self, -1, wxDefaultPosition, wxDefaultSize, wxNB_TOP | wxTAB_TRAVERSAL);
$self->{tabpanel}->AddPage($self->{settings} = Slic3r::GUI::Plater::ObjectDialog::SettingsTab->new($self->{tabpanel}, object => $self->{object}), "Settings"); $self->{tabpanel}->AddPage($self->{settings} = Slic3r::GUI::Plater::ObjectDialog::SettingsTab->new($self->{tabpanel}), "Settings");
$self->{tabpanel}->AddPage($self->{layers} = Slic3r::GUI::Plater::ObjectDialog::LayersTab->new($self->{tabpanel}, object => $self->{object}), "Layers"); $self->{tabpanel}->AddPage($self->{layers} = Slic3r::GUI::Plater::ObjectDialog::LayersTab->new($self->{tabpanel}), "Layers");
$self->{tabpanel}->AddPage($self->{materials} = Slic3r::GUI::Plater::ObjectDialog::MaterialsTab->new($self->{tabpanel}, object => $self->{object}), "Materials"); $self->{tabpanel}->AddPage($self->{materials} = Slic3r::GUI::Plater::ObjectDialog::MaterialsTab->new($self->{tabpanel}), "Materials");
my $buttons = $self->CreateStdDialogButtonSizer(wxOK); my $buttons = $self->CreateStdDialogButtonSizer(wxOK);
EVT_BUTTON($self, wxID_OK, sub { EVT_BUTTON($self, wxID_OK, sub {
@ -42,17 +42,24 @@ sub new {
return $self; return $self;
} }
package Slic3r::GUI::Plater::ObjectDialog::BaseTab;
use base 'Wx::Panel';
sub model_object {
my ($self) = @_;
return $self->GetParent->GetParent->{model_object};
}
package Slic3r::GUI::Plater::ObjectDialog::SettingsTab; package Slic3r::GUI::Plater::ObjectDialog::SettingsTab;
use Wx qw(:dialog :id :misc :sizer :systemsettings :button :icon); use Wx qw(:dialog :id :misc :sizer :systemsettings :button :icon);
use Wx::Grid; use Wx::Grid;
use Wx::Event qw(EVT_BUTTON); use Wx::Event qw(EVT_BUTTON);
use base 'Wx::Panel'; use base 'Slic3r::GUI::Plater::ObjectDialog::BaseTab';
sub new { sub new {
my $class = shift; my $class = shift;
my ($parent, %params) = @_; my ($parent, %params) = @_;
my $self = $class->SUPER::new($parent, -1, wxDefaultPosition, wxDefaultSize); my $self = $class->SUPER::new($parent, -1, wxDefaultPosition, wxDefaultSize);
$self->{object} = $params{object};
$self->{sizer} = Wx::BoxSizer->new(wxVERTICAL); $self->{sizer} = Wx::BoxSizer->new(wxVERTICAL);
@ -79,7 +86,7 @@ sub new {
my $idx = $choice->GetSelection; my $idx = $choice->GetSelection;
return if $idx == -1; # lack of selected item, can happen on Windows return if $idx == -1; # lack of selected item, can happen on Windows
my $opt_key = $self->{options}[$idx]; my $opt_key = $self->{options}[$idx];
$self->{object}->config->apply(Slic3r::Config->new_from_defaults($opt_key)); $self->model_object->config->apply(Slic3r::Config->new_from_defaults($opt_key));
$self->update_optgroup; $self->update_optgroup;
}); });
@ -105,7 +112,7 @@ sub update_optgroup {
$self->{options_sizer}->Clear(1); $self->{options_sizer}->Clear(1);
my $config = $self->{object}->config; my $config = $self->model_object->config;
my %categories = (); my %categories = ();
foreach my $opt_key (keys %$config) { foreach my $opt_key (keys %$config) {
my $category = $Slic3r::Config::Options->{$opt_key}{category}; my $category = $Slic3r::Config::Options->{$opt_key}{category};
@ -124,7 +131,7 @@ sub update_optgroup {
my ($opt_key) = @{$line->{options}}; # we assume that we have one option per line my ($opt_key) = @{$line->{options}}; # we assume that we have one option per line
my $btn = Wx::BitmapButton->new($self, -1, Wx::Bitmap->new("$Slic3r::var/delete.png", wxBITMAP_TYPE_PNG)); my $btn = Wx::BitmapButton->new($self, -1, Wx::Bitmap->new("$Slic3r::var/delete.png", wxBITMAP_TYPE_PNG));
EVT_BUTTON($self, $btn, sub { EVT_BUTTON($self, $btn, sub {
delete $self->{object}->config->{$opt_key}; delete $self->model_object->config->{$opt_key};
Slic3r::GUI->CallAfter(sub { $self->update_optgroup }); Slic3r::GUI->CallAfter(sub { $self->update_optgroup });
}); });
return $btn; return $btn;
@ -141,7 +148,7 @@ sub CanClose {
# validate options before allowing user to dismiss the dialog # validate options before allowing user to dismiss the dialog
# the validate method only works on full configs so we have # the validate method only works on full configs so we have
# to merge our settings with the default ones # to merge our settings with the default ones
my $config = Slic3r::Config->merge($self->GetParent->GetParent->GetParent->GetParent->GetParent->config, $self->{object}->config); my $config = Slic3r::Config->merge($self->GetParent->GetParent->GetParent->GetParent->GetParent->config, $self->model_object->config);
eval { eval {
$config->validate; $config->validate;
}; };
@ -153,13 +160,12 @@ package Slic3r::GUI::Plater::ObjectDialog::LayersTab;
use Wx qw(:dialog :id :misc :sizer :systemsettings); use Wx qw(:dialog :id :misc :sizer :systemsettings);
use Wx::Grid; use Wx::Grid;
use Wx::Event qw(EVT_GRID_CELL_CHANGED); use Wx::Event qw(EVT_GRID_CELL_CHANGED);
use base 'Wx::Panel'; use base 'Slic3r::GUI::Plater::ObjectDialog::BaseTab';
sub new { sub new {
my $class = shift; my $class = shift;
my ($parent, %params) = @_; my ($parent, %params) = @_;
my $self = $class->SUPER::new($parent, -1, wxDefaultPosition, wxDefaultSize); my $self = $class->SUPER::new($parent, -1, wxDefaultPosition, wxDefaultSize);
$self->{object} = $params{object};
my $sizer = Wx::BoxSizer->new(wxVERTICAL); my $sizer = Wx::BoxSizer->new(wxVERTICAL);
@ -182,7 +188,7 @@ sub new {
$grid->SetDefaultCellAlignment(wxALIGN_CENTRE, wxALIGN_CENTRE); $grid->SetDefaultCellAlignment(wxALIGN_CENTRE, wxALIGN_CENTRE);
# load data # load data
foreach my $range (@{ $self->{object}->layer_height_ranges }) { foreach my $range (@{ $self->model_object->layer_height_ranges }) {
$grid->AppendRows(1); $grid->AppendRows(1);
my $i = $grid->GetNumberRows-1; my $i = $grid->GetNumberRows-1;
$grid->SetCellValue($i, $_, $range->[$_]) for 0..2; $grid->SetCellValue($i, $_, $range->[$_]) for 0..2;
@ -242,7 +248,7 @@ sub Closing {
my $self = shift; my $self = shift;
# save ranges into the plater object # save ranges into the plater object
$self->{object}->layer_height_ranges([ $self->_get_ranges ]); $self->model_object->layer_height_ranges([ $self->_get_ranges ]);
} }
sub _get_ranges { sub _get_ranges {
@ -261,7 +267,7 @@ package Slic3r::GUI::Plater::ObjectDialog::MaterialsTab;
use Wx qw(:dialog :id :misc :sizer :systemsettings :button :icon); use Wx qw(:dialog :id :misc :sizer :systemsettings :button :icon);
use Wx::Grid; use Wx::Grid;
use Wx::Event qw(EVT_BUTTON); use Wx::Event qw(EVT_BUTTON);
use base 'Wx::Panel'; use base 'Slic3r::GUI::Plater::ObjectDialog::BaseTab';
sub new { sub new {
my $class = shift; my $class = shift;
@ -280,12 +286,12 @@ sub new {
} }
# get unique materials used in this object # get unique materials used in this object
$self->{materials} = [ $self->{object}->get_model_object->unique_materials ]; $self->{materials} = [ $self->model_object->unique_materials ];
# build an OptionsGroup # build an OptionsGroup
$self->{mapping} = { $self->{mapping} = {
(map { $self->{materials}[$_] => $_+1 } 0..$#{ $self->{materials} }), # defaults (map { $self->{materials}[$_] => $_+1 } 0..$#{ $self->{materials} }), # defaults
%{$self->{object}->material_mapping}, %{$self->model_object->material_mapping},
}; };
my $optgroup = Slic3r::GUI::OptionsGroup->new( my $optgroup = Slic3r::GUI::OptionsGroup->new(
parent => $self, parent => $self,
@ -298,7 +304,7 @@ sub new {
{ {
opt_key => "material_extruder_$_", opt_key => "material_extruder_$_",
type => 'i', type => 'i',
label => $self->{object}->get_model_object->model->get_material_name($material_id), label => $self->model_object->model->get_material_name($material_id),
min => 1, min => 1,
default => $self->{mapping}{$material_id}, default => $self->{mapping}{$material_id},
on_change => sub { $self->{mapping}{$material_id} = $_[0] }, on_change => sub { $self->{mapping}{$material_id} = $_[0] },
@ -318,7 +324,7 @@ sub Closing {
my $self = shift; my $self = shift;
# save mappings into the plater object # save mappings into the plater object
$self->{object}->material_mapping($self->{mapping}); $self->model_object->material_mapping($self->{mapping});
} }
1; 1;

View File

@ -28,16 +28,15 @@ sub new {
$self->sphi(45); $self->sphi(45);
$self->stheta(-45); $self->stheta(-45);
$object->align_to_origin; my $bounding_box = $object->raw_mesh->bounding_box;
$self->object_center($object->center); $self->object_center($bounding_box->center);
$self->object_size($object->size); $self->object_size($bounding_box->size);
# group mesh(es) by material # group mesh(es) by material
my @materials = (); my @materials = ();
$self->volumes([]); $self->volumes([]);
foreach my $volume (@{$object->volumes}) { foreach my $volume (@{$object->volumes}) {
my $mesh = $volume->mesh; my $mesh = $volume->mesh;
$mesh->repair;
my $material_id = $volume->material_id // '_'; my $material_id = $volume->material_id // '_';
my $color_idx = first { $materials[$_] eq $material_id } 0..$#materials; my $color_idx = first { $materials[$_] eq $material_id } 0..$#materials;

View File

@ -6,7 +6,6 @@ use Slic3r::Geometry qw(X Y Z MIN move_points);
has 'materials' => (is => 'ro', default => sub { {} }); has 'materials' => (is => 'ro', default => sub { {} });
has 'objects' => (is => 'ro', default => sub { [] }); has 'objects' => (is => 'ro', default => sub { [] });
has '_bounding_box' => (is => 'rw');
sub read_from_file { sub read_from_file {
my $class = shift; my $class = shift;
@ -28,40 +27,61 @@ sub merge {
my $new_model = ref($class) my $new_model = ref($class)
? $class ? $class
: $class->new; : $class->new;
foreach my $model (@models) {
# merge material attributes (should we rename them in case of duplicates?)
$new_model->set_material($_, { %{$model->materials->{$_}}, %{$model->materials->{$_} || {}} })
for keys %{$model->materials};
foreach my $object (@{$model->objects}) {
my $new_object = $new_model->add_object(
input_file => $object->input_file,
config => $object->config,
layer_height_ranges => $object->layer_height_ranges,
);
$new_object->add_volume(
material_id => $_->material_id,
mesh => $_->mesh->clone,
) for @{$object->volumes};
$new_object->add_instance(
offset => $_->offset,
rotation => $_->rotation,
) for @{ $object->instances // [] };
}
}
$new_model->add_object($_) for map @{$_->objects}, @models;
return $new_model; return $new_model;
} }
sub add_object { sub add_object {
my $self = shift; my $self = shift;
my $object = Slic3r::Model::Object->new(model => $self, @_); my $new_object;
push @{$self->objects}, $object; if (@_ == 1) {
$self->_bounding_box(undef); # we have a Model::Object
return $object; my ($object) = @_;
$new_object = $self->add_object(
input_file => $object->input_file,
config => $object->config,
layer_height_ranges => $object->layer_height_ranges, # TODO: clone!
material_mapping => $object->material_mapping, # TODO: clone!
);
foreach my $volume (@{$object->volumes}) {
$new_object->add_volume(
material_id => $volume->material_id,
mesh => $volume->mesh->clone,
);
if (defined $volume->material_id) {
# merge material attributes (should we rename materials in case of duplicates?)
$self->set_material($volume->material_id, {
%{ $object->model->materials->{$volume->material_id} },
%{ $self->materials->{$volume->material_id} || {} },
});
}
}
$new_object->add_instance(
offset => $_->offset,
rotation => $_->rotation,
scaling_factor => $_->scaling_factor,
) for @{ $object->instances // [] };
} else {
push @{$self->objects}, $new_object = Slic3r::Model::Object->new(model => $self, @_);
}
return $new_object;
}
sub delete_object {
my ($self, $obj_idx) = @_;
splice @{$self->objects}, $obj_idx, 1;
}
sub delete_all_objects {
my ($self) = @_;
@{$self->objects} = ();
} }
sub set_material { sub set_material {
@ -74,104 +94,111 @@ sub set_material {
); );
} }
sub arrange_objects { sub duplicate_objects_grid {
my $self = shift; my ($self, $grid, $distance) = @_;
my ($config) = @_;
# do we have objects with no position? die "Grid duplication is not supported with multiple objects\n"
if (first { !defined $_->instances } @{$self->objects}) { if @{$self->objects} > 1;
# we shall redefine positions for all objects
my $object = $self->objects->[0];
my ($copies, @positions) = $self->_arrange( @{$object->instances} = ();
config => $config,
items => $self->objects, my $size = $object->bounding_box->size;
); for my $x_copy (1..$grid->[X]) {
for my $y_copy (1..$grid->[Y]) {
# apply positions to objects
foreach my $object (@{$self->objects}) {
$object->align_to_origin;
$object->instances([]);
$object->add_instance( $object->add_instance(
offset => $_, offset => [
rotation => 0, ($size->[X] + $distance) * ($x_copy-1),
) for splice @positions, 0, $copies; ($size->[Y] + $distance) * ($y_copy-1),
],
);
} }
}
} else { }
# we only have objects with defined position
# this will append more instances to each object
# align the whole model to origin as it is # and then automatically rearrange everything
$self->align_to_origin; sub duplicate_objects {
my ($self, $config, $copies_num) = @_;
# arrange this model as a whole
my ($copies, @positions) = $self->_arrange( foreach my $object (@{$self->objects}) {
config => $config, my @instances = @{$object->instances};
items => [$self], foreach my $instance (@instances) {
); ### $object->add_instance($instance->clone); if we had clone()
$object->add_instance(
# apply positions to objects by translating the current positions offset => [ @{$instance->offset} ],
foreach my $object (@{$self->objects}) { rotation => $instance->rotation,
my @old_instances = @{$object->instances}; scaling_factor => $instance->scaling_factor,
$object->instances([]); ) for 2..$copies_num;
foreach my $instance (@old_instances) { }
}
$self->arrange_objects($config);
}
# arrange objects preserving their instance count
# but altering their instance positions
sub arrange_objects {
my ($self, $config) = @_;
# get the (transformed) size of each instance so that we take
# into account their different transformations when packing
my @instance_sizes = ();
foreach my $object (@{$self->objects}) {
push @instance_sizes, map $object->instance_bounding_box($_)->size, 0..$#{$object->instances};
}
my @positions = $self->_arrange($config, \@instance_sizes);
foreach my $object (@{$self->objects}) {
$_->offset(shift @positions) for @{$object->instances};
$object->update_bounding_box;
}
}
# duplicate the entire model preserving instance relative positions
sub duplicate {
my ($self, $config, $copies_num) = @_;
my $model_size = $self->bounding_box->size;
my @positions = $self->_arrange($config, [ map $model_size, 2..$copies_num ]);
# note that this will leave the object count unaltered
foreach my $object (@{$self->objects}) {
foreach my $instance (@{$object->instances}) {
foreach my $pos (@positions) {
### $object->add_instance($instance->clone); if we had clone()
$object->add_instance( $object->add_instance(
offset => $_, offset => [ $instance->offset->[X] + $pos->[X], $instance->offset->[Y] + $pos->[Y] ],
rotation => $instance->rotation, rotation => $instance->rotation,
scaling_factor => $instance->scaling_factor, scaling_factor => $instance->scaling_factor,
) for move_points($instance->offset, @positions); );
} }
} }
$object->update_bounding_box;
} }
} }
sub _arrange { sub _arrange {
my $self = shift; my ($self, $config, $sizes) = @_;
my %params = @_;
my $config = $params{config}; my $partx = max(map $_->[X], @$sizes);
my @items = @{$params{items}}; # can be Model or Object objects, they have to implement size() my $party = max(map $_->[Y], @$sizes);
return Slic3r::Geometry::arrange
if ($config->duplicate_grid->[X] > 1 || $config->duplicate_grid->[Y] > 1) { (scalar(@$sizes), $partx, $party, (map $_, @{$config->bed_size}),
if (@items > 1) { $config->min_object_distance, $config);
die "Grid duplication is not supported with multiple objects\n";
}
my @positions = ();
my $size = $items[0]->size;
my $dist = $config->duplicate_distance;
for my $x_copy (1..$config->duplicate_grid->[X]) {
for my $y_copy (1..$config->duplicate_grid->[Y]) {
push @positions, [
($size->[X] + $dist) * ($x_copy-1),
($size->[Y] + $dist) * ($y_copy-1),
];
}
}
return ($config->duplicate_grid->[X] * $config->duplicate_grid->[Y]), @positions;
} else {
my $total_parts = $config->duplicate * @items;
my @sizes = map $_->size, @items;
my $partx = max(map $_->[X], @sizes);
my $party = max(map $_->[Y], @sizes);
return $config->duplicate,
Slic3r::Geometry::arrange
($total_parts, $partx, $party, (map $_, @{$config->bed_size}),
$config->min_object_distance, $config);
}
} }
sub size { sub has_objects_with_no_instances {
my $self = shift; my ($self) = @_;
return $self->bounding_box->size; return (first { !defined $_->instances } @{$self->objects}) ? 1 : 0;
} }
# this returns the bounding box of the *transformed* instances
sub bounding_box { sub bounding_box {
my $self = shift; my $self = shift;
return Slic3r::Geometry::BoundingBox->merge(map $_->bounding_box, @{ $self->objects });
if (!defined $self->_bounding_box) {
$self->_bounding_box(Slic3r::Geometry::BoundingBox->merge(map $_->bounding_box, @{$self->objects}));
}
return $self->_bounding_box->clone;
} }
sub align_to_origin { sub align_to_origin {
@ -181,52 +208,33 @@ sub align_to_origin {
# have lowest value for each axis at coordinate 0 # have lowest value for each axis at coordinate 0
{ {
my $bb = $self->bounding_box; my $bb = $self->bounding_box;
$self->move(map -$bb->extents->[$_][MIN], X,Y,Z); $self->translate(map -$bb->extents->[$_][MIN], X,Y,Z);
} }
# align all instances to 0,0 as well # align all instances to 0,0 as well
{ {
my @instances = map @{$_->instances}, @{$self->objects}; my @instances = map @{$_->instances}, @{$self->objects};
my @extents = Slic3r::Geometry::bounding_box_3D([ map $_->offset, @instances ]); my @extents = Slic3r::Geometry::bounding_box_3D([ map $_->offset, @instances ]);
$_->offset->translate(-$extents[X][MIN], -$extents[Y][MIN]) for @instances; foreach my $instance (@instances) {
$instance->offset->[X] -= $extents[X][MIN];
$instance->offset->[Y] -= $extents[Y][MIN];
}
} }
} }
sub scale { sub translate {
my $self = shift;
$_->scale(@_) for @{$self->objects};
$self->_bounding_box->scale(@_) if defined $self->_bounding_box;
}
sub move {
my $self = shift; my $self = shift;
my @shift = @_; my @shift = @_;
$_->move(@shift) for @{$self->objects}; $_->translate(@shift) for @{$self->objects};
$self->_bounding_box->translate(@shift) if defined $self->_bounding_box;
} }
# flattens everything to a single mesh # flattens everything to a single mesh
sub mesh { sub mesh {
my $self = shift; my $self = shift;
my @meshes = ();
foreach my $object (@{$self->objects}) {
my @instances = $object->instances ? @{$object->instances} : (undef);
foreach my $instance (@instances) {
my $mesh = $object->mesh->clone;
if ($instance) {
$mesh->rotate($instance->rotation, Slic3r::Point->new(0,0));
$mesh->scale($instance->scaling_factor);
$mesh->align_to_origin;
$mesh->translate(@{$instance->offset}, 0);
}
push @meshes, $mesh;
}
}
my $mesh = Slic3r::TriangleMesh->new; my $mesh = Slic3r::TriangleMesh->new;
$mesh->merge($_) for @meshes; $mesh->merge($_->mesh) for @{$self->objects};
return $mesh; return $mesh;
} }
@ -249,24 +257,19 @@ sub split_meshes {
foreach my $mesh (@{$volume->mesh->split}) { foreach my $mesh (@{$volume->mesh->split}) {
my $new_object = $self->add_object( my $new_object = $self->add_object(
input_file => $object->input_file, input_file => $object->input_file,
config => $object->config, config => $object->config->clone,
layer_height_ranges => $object->layer_height_ranges, layer_height_ranges => $object->layer_height_ranges, # TODO: this needs to be cloned
); );
$new_object->add_volume( $new_object->add_volume(
mesh => $mesh, mesh => $mesh,
material_id => $volume->material_id, material_id => $volume->material_id,
); );
# let's now align the new object to the origin and put its displacement # add one instance per original instance
# (extents) in the instances info
my $bb = $mesh->bounding_box;
$new_object->align_to_origin;
# add one instance per original instance applying the displacement
$new_object->add_instance( $new_object->add_instance(
offset => [ $_->offset->[X] + $bb->x_min, $_->offset->[Y] + $bb->y_min ], offset => [ @{$_->offset} ],
rotation => $_->rotation, rotation => $_->rotation,
scaling_factor => $_->scaling_factor, scaling_factor => $_->scaling_factor,
) for @{ $object->instances // [] }; ) for @{ $object->instances // [] };
} }
} }
@ -302,17 +305,16 @@ use Moo;
use File::Basename qw(basename); use File::Basename qw(basename);
use List::Util qw(first sum); use List::Util qw(first sum);
use Slic3r::Geometry qw(X Y Z MIN MAX move_points move_points_3D); use Slic3r::Geometry qw(X Y Z MIN MAX);
use Storable qw(dclone);
has 'input_file' => (is => 'rw'); has 'input_file' => (is => 'rw');
has 'model' => (is => 'ro', weak_ref => 1, required => 1); has 'model' => (is => 'ro', weak_ref => 1, required => 1);
has 'volumes' => (is => 'ro', default => sub { [] }); has 'volumes' => (is => 'ro', default => sub { [] });
has 'instances' => (is => 'rw'); # in unscaled coordinates has 'instances' => (is => 'rw');
has 'config' => (is => 'rw', default => sub { Slic3r::Config->new }); has 'config' => (is => 'rw', default => sub { Slic3r::Config->new });
has 'layer_height_ranges' => (is => 'rw', default => sub { [] }); # [ z_min, z_max, layer_height ] has 'layer_height_ranges' => (is => 'rw', default => sub { [] }); # [ z_min, z_max, layer_height ]
has 'material_mapping' => (is => 'rw', default => sub { {} }); # { material_id => region_idx } has 'material_mapping' => (is => 'rw', default => sub { {} }); # { material_id => region_idx }
has '_bounding_box' => (is => 'rw'); has '_bounding_box' => (is => 'rw');
sub add_volume { sub add_volume {
my $self = shift; my $self = shift;
@ -323,54 +325,78 @@ sub add_volume {
%args, %args,
); );
$self->_bounding_box(undef); $self->_bounding_box(undef);
$self->model->_bounding_box(undef);
return $volume; return $volume;
} }
sub add_instance { sub add_instance {
my $self = shift; my $self = shift;
my %params = @_;
$self->instances([]) if !defined $self->instances; $self->instances([]) if !defined $self->instances;
push @{$self->instances}, Slic3r::Model::Instance->new(object => $self, @_); push @{$self->instances}, my $i = Slic3r::Model::Instance->new(object => $self, %params);
$self->model->_bounding_box(undef); $self->_bounding_box(undef);
return $self->instances->[-1]; return $i;
} }
sub mesh { sub delete_last_instance {
my ($self) = @_;
pop @{$self->instances};
$self->_bounding_box(undef);
}
sub instances_count {
my $self = shift;
return scalar(@{ $self->instances // [] });
}
sub raw_mesh {
my $self = shift; my $self = shift;
my $mesh = Slic3r::TriangleMesh->new; my $mesh = Slic3r::TriangleMesh->new;
$mesh->merge($_->mesh) for @{$self->volumes}; $mesh->merge($_->mesh) for @{ $self->volumes };
return $mesh; return $mesh;
} }
sub size { # flattens all volumes and instances into a single mesh
sub mesh {
my $self = shift; my $self = shift;
return $self->bounding_box->size;
my $mesh = $self->raw_mesh;
my @instance_meshes = ();
foreach my $instance (@{ $self->instances }) {
my $m = $mesh->clone;
$instance->transform_mesh($m);
push @instance_meshes, $m;
}
my $full_mesh = Slic3r::TriangleMesh->new;
$full_mesh->merge($_) for @instance_meshes;
return $full_mesh;
} }
sub center { sub update_bounding_box {
my $self = shift; my ($self) = @_;
return $self->bounding_box->center; $self->_bounding_box($self->mesh->bounding_box);
}
sub center_2D {
my $self = shift;
return $self->bounding_box->center_2D;
} }
# this returns the bounding box of the *transformed* instances
sub bounding_box { sub bounding_box {
my $self = shift; my $self = shift;
if (!defined $self->_bounding_box) { $self->update_bounding_box if !defined $self->_bounding_box;
my @meshes = map $_->mesh, @{$self->volumes};
my $bounding_box = Slic3r::Geometry::BoundingBox->new_from_bb((shift @meshes)->bb3);
$bounding_box->merge(Slic3r::Geometry::BoundingBox->new_from_bb($_->bb3)) for @meshes;
$self->_bounding_box($bounding_box);
}
return $self->_bounding_box->clone; return $self->_bounding_box->clone;
} }
# this returns the bounding box of the *transformed* given instance
sub instance_bounding_box {
my ($self, $instance_idx) = @_;
my $mesh = $self->raw_mesh;
$self->instances->[$instance_idx]->transform_mesh($mesh);
return $mesh->bounding_box;
}
sub align_to_origin { sub align_to_origin {
my $self = shift; my $self = shift;
@ -378,11 +404,38 @@ sub align_to_origin {
# have lowest value for each axis at coordinate 0 # have lowest value for each axis at coordinate 0
my $bb = $self->bounding_box; my $bb = $self->bounding_box;
my @shift = map -$bb->extents->[$_][MIN], X,Y,Z; my @shift = map -$bb->extents->[$_][MIN], X,Y,Z;
$self->move(@shift); $self->translate(@shift);
return @shift; return @shift;
} }
sub move { sub center_around_origin {
my $self = shift;
# calculate the displacements needed to
# center this object around the origin
my $bb = $self->raw_mesh->bounding_box;
# first align to origin on XYZ
my @shift = @{ $bb->vector_to_origin };
# then center it on XY
my $size = $bb->size;
$shift[$_] -= $size->[$_]/2 for X,Y;
$self->translate(@shift);
if (defined $self->instances) {
foreach my $instance (@{ $self->instances }) {
$instance->offset->[X] -= $shift[X];
$instance->offset->[Y] -= $shift[Y];
}
$self->update_bounding_box;
}
return @shift;
}
sub translate {
my $self = shift; my $self = shift;
my @shift = @_; my @shift = @_;
@ -390,24 +443,6 @@ sub move {
$self->_bounding_box->translate(@shift) if defined $self->_bounding_box; $self->_bounding_box->translate(@shift) if defined $self->_bounding_box;
} }
sub scale {
my $self = shift;
my ($factor) = @_;
return if $factor == 1;
$_->mesh->scale($factor) for @{$self->volumes};
$self->_bounding_box->scale($factor) if defined $self->_bounding_box;
}
sub rotate {
my $self = shift;
my ($deg) = @_;
return if $deg == 0;
$_->mesh->rotate($deg, Slic3r::Point->(0,0)) for @{$self->volumes};
$self->_bounding_box(undef);
}
sub materials_count { sub materials_count {
my $self = shift; my $self = shift;
@ -425,7 +460,7 @@ sub unique_materials {
sub facets_count { sub facets_count {
my $self = shift; my $self = shift;
return sum(map $_->facets_count, @{$self->volumes}); return sum(map $_->mesh->facets_count, @{$self->volumes});
} }
sub needed_repair { sub needed_repair {
@ -465,21 +500,27 @@ sub print_info {
} }
} }
sub clone { dclone($_[0]) }
package Slic3r::Model::Volume; package Slic3r::Model::Volume;
use Moo; use Moo;
has 'object' => (is => 'ro', weak_ref => 1, required => 1); has 'object' => (is => 'ro', weak_ref => 1, required => 1);
has 'material_id' => (is => 'rw'); has 'material_id' => (is => 'rw');
has 'mesh' => (is => 'rw', required => 1); has 'mesh' => (is => 'rw', required => 1);
package Slic3r::Model::Instance; package Slic3r::Model::Instance;
use Moo; use Moo;
has 'object' => (is => 'ro', weak_ref => 1, required => 1); has 'object' => (is => 'ro', weak_ref => 1, required => 1);
has 'rotation' => (is => 'rw', default => sub { 0 }); # around mesh center point has 'rotation' => (is => 'rw', default => sub { 0 }); # around mesh center point
has 'scaling_factor' => (is => 'rw', default => sub { 1 }); has 'scaling_factor' => (is => 'rw', default => sub { 1 });
has 'offset' => (is => 'rw'); # must be Slic3r::Point object in scaled coordinates has 'offset' => (is => 'rw'); # must be arrayref in *unscaled* coordinates
sub transform_mesh {
my ($self, $mesh) = @_;
$mesh->rotate($self->rotation, Slic3r::Point->new(0,0)); # rotate around mesh origin
$mesh->scale($self->scaling_factor); # scale around mesh origin
$mesh->translate(@{$self->offset}, 0);
}
1; 1;

View File

@ -110,19 +110,15 @@ sub add_model_object {
my $bb1 = Slic3r::Geometry::BoundingBox->merge(map $_->bounding_box, values %meshes); my $bb1 = Slic3r::Geometry::BoundingBox->merge(map $_->bounding_box, values %meshes);
foreach my $mesh (values %meshes) { foreach my $mesh (values %meshes) {
# align meshes to object origin before applying transformations
$mesh->translate(@{$bb1->vector_to_origin});
# the order of these transformations must be the same as the one used in plater # the order of these transformations must be the same as the one used in plater
# to make the object positioning consistent with the visual preview # to make the object positioning consistent with the visual preview
# we ignore the per-instance transformations currently and only # we ignore the per-instance transformations currently and only
# consider the first one # consider the first one
if ($object->instances && @{$object->instances}) { if ($object->instances && @{$object->instances}) {
$mesh->rotate($object->instances->[0]->rotation, $object->center_2D); $mesh->rotate($object->instances->[0]->rotation, Slic3r::Point->new(0,0));
$mesh->scale($object->instances->[0]->scaling_factor); $mesh->scale($object->instances->[0]->scaling_factor);
} }
$mesh->repair; # needed? TODO: try without
} }
# we align object also after transformations so that we only work with positive coordinates # we align object also after transformations so that we only work with positive coordinates

View File

@ -118,8 +118,7 @@ sub slice {
# process facets # process facets
for my $region_id (0 .. $#{$self->meshes}) { for my $region_id (0 .. $#{$self->meshes}) {
my $mesh = $self->meshes->[$region_id] // next; # ignore undef meshes my $mesh = $self->meshes->[$region_id] // next; # ignore undef meshes
$mesh->repair;
{ {
my $loops = $mesh->slice([ map $_->slice_z, @{$self->layers} ]); my $loops = $mesh->slice([ map $_->slice_z, @{$self->layers} ]);
for my $layer_id (0..$#$loops) { for my $layer_id (0..$#$loops) {

View File

@ -12,6 +12,7 @@ use Getopt::Long qw(:config no_auto_abbrev);
use List::Util qw(first); use List::Util qw(first);
use POSIX qw(setlocale LC_NUMERIC); use POSIX qw(setlocale LC_NUMERIC);
use Slic3r; use Slic3r;
use Slic3r::Geometry qw(X Y);
$|++; $|++;
our %opt = (); our %opt = ();
@ -119,9 +120,30 @@ if (@ARGV) { # slicing from command line
} else { } else {
$model = Slic3r::Model->read_from_file($input_file); $model = Slic3r::Model->read_from_file($input_file);
} }
$_->scale($config->scale) for @{$model->objects};
$_->rotate($config->rotate) for @{$model->objects}; my $need_arrange = $model->has_objects_with_no_instances;
$model->arrange_objects($config); if ($need_arrange) {
# apply a default position to all objects not having one
foreach my $object (@{$model->objects}) {
$object->add_instance(offset => [0,0]) if !defined $object->instances;
}
}
# apply scaling and rotation supplied from command line if any
foreach my $instance (map @{$_->instances}, @{$model->objects}) {
$instance->scaling_factor($instance->scaling_factor * $config->scale);
$instance->rotation($instance->rotation + $config->rotate);
}
# TODO: --scale --rotate, --duplicate* shouldn't be stored in config
if ($config->duplicate_grid->[X] > 1 || $config->duplicate_grid->[Y] > 1) {
$model->duplicate_objects_grid($config->duplicate_grid, $config->duplicate_distance);
} elsif ($need_arrange) {
$model->duplicate_objects($config, $config->duplicate);
} elsif ($config->duplicate > 1) {
# if all input objects have defined position(s) apply duplication to the whole model
$model->duplicate($config, $config->duplicate);
}
if ($opt{info}) { if ($opt{info}) {
$model->print_info; $model->print_info;

View File

@ -1,4 +1,5 @@
#include "ExPolygonCollection.hpp" #include "ExPolygonCollection.hpp"
#include "Geometry.hpp"
namespace Slic3r { namespace Slic3r {
@ -57,4 +58,13 @@ ExPolygonCollection::simplify(double tolerance)
this->expolygons = expp; this->expolygons = expp;
} }
void
ExPolygonCollection::convex_hull(Polygon* hull) const
{
Points pp;
for (ExPolygons::const_iterator it = this->expolygons.begin(); it != this->expolygons.end(); ++it)
pp.insert(pp.end(), it->contour.points.begin(), it->contour.points.end());
Slic3r::Geometry::convex_hull(pp, hull);
}
} }

View File

@ -16,6 +16,7 @@ class ExPolygonCollection
void rotate(double angle, Point* center); void rotate(double angle, Point* center);
bool contains_point(const Point* point) const; bool contains_point(const Point* point) const;
void simplify(double tolerance); void simplify(double tolerance);
void convex_hull(Polygon* hull) const;
}; };
} }

View File

@ -2,6 +2,7 @@
#include "clipper.hpp" #include "clipper.hpp"
#include <algorithm> #include <algorithm>
#include <map> #include <map>
#include <vector>
namespace Slic3r { namespace Geometry { namespace Slic3r { namespace Geometry {
@ -11,45 +12,33 @@ sort_points (Point a, Point b)
return (a.x < b.x) || (a.x == b.x && a.y < b.y); return (a.x < b.x) || (a.x == b.x && a.y < b.y);
} }
/* This implementation is based on Steffen Mueller's work for /* This implementation is based on Andrew's monotone chain 2D convex hull algorithm */
the Perl module Math::ConvexHull::MonotoneChain (available
on CPAN under the GPL terms) */
void void
convex_hull(Points points, Polygon &hull) convex_hull(Points &points, Polygon* hull)
{ {
assert(points.size() >= 3); assert(points.size() >= 3);
// sort input points // sort input points
std::sort(points.begin(), points.end(), sort_points); std::sort(points.begin(), points.end(), sort_points);
typedef const Point* PointPtr; int n = points.size(), k = 0;
PointPtr* out_hull = (PointPtr*)malloc(points.size()*2*sizeof(PointPtr)); hull->points.resize(2*n);
/* lower hull */ // Build lower hull
size_t k = 0; for (int i = 0; i < n; i++) {
for (Points::const_iterator it = points.begin(); it != points.end(); ++it) { while (k >= 2 && points[i].ccw(hull->points[k-2], hull->points[k-1]) <= 0) k--;
while (k >= 2 && it->ccw(out_hull[k-2], out_hull[k-1]) <= 0) --k; hull->points[k++] = points[i];
Point pz = *&*it;
out_hull[k++] = &*it;
} }
/* upper hull */ // Build upper hull
size_t t = k+1; for (int i = n-2, t = k+1; i >= 0; i--) {
for (Points::const_iterator it = points.end() - 2; it != points.begin(); --it) { while (k >= t && points[i].ccw(hull->points[k-2], hull->points[k-1]) <= 0) k--;
while (k >= t && it->ccw(out_hull[k-2], out_hull[k-1]) <= 0) --k; hull->points[k++] = points[i];
out_hull[k++] = &*it;
} }
hull->points.resize(k);
// we assume hull is empty assert( hull->points.front().coincides_with(hull->points.back()) );
hull.points.reserve(k); hull->points.pop_back();
for (size_t i = 0; i < k; ++i) {
hull.points.push_back(*(out_hull[i]));
}
// not sure why this happens randomly
if (hull.points.front().coincides_with(hull.points.back()))
hull.points.pop_back();
free(out_hull);
} }
/* accepts an arrayref of points and returns a list of indices /* accepts an arrayref of points and returns a list of indices

View File

@ -5,7 +5,7 @@
namespace Slic3r { namespace Geometry { namespace Slic3r { namespace Geometry {
void convex_hull(Points points, Polygon &hull); void convex_hull(Points &points, Polygon* hull);
void chained_path(Points &points, std::vector<Points::size_type> &retval, Point start_near); void chained_path(Points &points, std::vector<Points::size_type> &retval, Point start_near);
void chained_path(Points &points, std::vector<Points::size_type> &retval); void chained_path(Points &points, std::vector<Points::size_type> &retval);
template<class T> void chained_path_items(Points &points, T &items, T &retval); template<class T> void chained_path_items(Points &points, T &items, T &retval);

View File

@ -109,6 +109,9 @@ Point::distance_to(const Line &line) const
/* Three points are a counter-clockwise turn if ccw > 0, clockwise if /* Three points are a counter-clockwise turn if ccw > 0, clockwise if
* ccw < 0, and collinear if ccw = 0 because ccw is a determinant that * ccw < 0, and collinear if ccw = 0 because ccw is a determinant that
* gives the signed area of the triangle formed by p1, p2 and this point. * gives the signed area of the triangle formed by p1, p2 and this point.
* In other words it is the 2D cross product of p1-p2 and p1-this, i.e.
* z-component of their 3D cross product.
* We return double because it must be big enough to hold 2*max(|coordinate|)^2
*/ */
double double
Point::ccw(const Point &p1, const Point &p2) const Point::ccw(const Point &p1, const Point &p2) const

View File

@ -603,7 +603,7 @@ TriangleMesh::horizontal_projection(ExPolygons &retval) const
} }
void void
TriangleMesh::convex_hull(Polygon &hull) TriangleMesh::convex_hull(Polygon* hull)
{ {
if (this->stl.v_shared == NULL) stl_generate_shared_vertices(&(this->stl)); if (this->stl.v_shared == NULL) stl_generate_shared_vertices(&(this->stl));
Points pp; Points pp;

View File

@ -33,7 +33,7 @@ class TriangleMesh
TriangleMeshPtrs split() const; TriangleMeshPtrs split() const;
void merge(const TriangleMesh* mesh); void merge(const TriangleMesh* mesh);
void horizontal_projection(ExPolygons &retval) const; void horizontal_projection(ExPolygons &retval) const;
void convex_hull(Polygon &hull); void convex_hull(Polygon* hull);
stl_file stl; stl_file stl;
bool repaired; bool repaired;

View File

@ -68,5 +68,15 @@ ExPolygonCollection::append(...)
THIS->expolygons.push_back(expolygon); THIS->expolygons.push_back(expolygon);
} }
Polygon*
ExPolygonCollection::convex_hull()
PREINIT:
const char* CLASS = "Slic3r::Polygon";
CODE:
RETVAL = new Polygon ();
THIS->convex_hull(RETVAL);
OUTPUT:
RETVAL
%} %}
}; };

View File

@ -17,7 +17,7 @@ convex_hull(points)
const char* CLASS = "Slic3r::Polygon"; const char* CLASS = "Slic3r::Polygon";
CODE: CODE:
RETVAL = new Polygon (); RETVAL = new Polygon ();
Slic3r::Geometry::convex_hull(points, *RETVAL); Slic3r::Geometry::convex_hull(points, RETVAL);
OUTPUT: OUTPUT:
RETVAL RETVAL

View File

@ -24,6 +24,8 @@
void rotate(double angle, Point* center); void rotate(double angle, Point* center);
TriangleMeshPtrs split(); TriangleMeshPtrs split();
void merge(TriangleMesh* mesh); void merge(TriangleMesh* mesh);
ExPolygons horizontal_projection()
%code{% THIS->horizontal_projection(RETVAL); %};
%{ %{
SV* SV*
@ -159,6 +161,16 @@ TriangleMesh::bb3()
OUTPUT: OUTPUT:
RETVAL RETVAL
Polygon*
TriangleMesh::convex_hull()
PREINIT:
const char* CLASS = "Slic3r::Polygon";
CODE:
RETVAL = new Polygon ();
THIS->convex_hull(RETVAL);
OUTPUT:
RETVAL
%} %}
}; };