diff --git a/lib/Slic3r.pm b/lib/Slic3r.pm index 7bf6f4209..b345f0d7d 100644 --- a/lib/Slic3r.pm +++ b/lib/Slic3r.pm @@ -159,4 +159,8 @@ sub open { return CORE::open $$fh, $mode, encode_path($filename); } +# this package declaration prevents an ugly fatal warning to be emitted when +# spawning a new thread +package GLUquadricObjPtr; + 1; diff --git a/lib/Slic3r/Format/AMF/Parser.pm b/lib/Slic3r/Format/AMF/Parser.pm index 24732dbc8..d392a8915 100644 --- a/lib/Slic3r/Format/AMF/Parser.pm +++ b/lib/Slic3r/Format/AMF/Parser.pm @@ -120,7 +120,7 @@ sub end_document { foreach my $instance (@{ $self->{_instances}{$object_id} }) { $self->{_model}->objects->[$new_object_id]->add_instance( rotation => $instance->{rz} || 0, - offset => Slic3r::Point->new($instance->{deltax} || 0, $instance->{deltay} || 0), + offset => [ $instance->{deltax} || 0, $instance->{deltay} || 0 ], ); } } diff --git a/lib/Slic3r/GUI/Plater.pm b/lib/Slic3r/GUI/Plater.pm index ee90576d9..24286303b 100644 --- a/lib/Slic3r/GUI/Plater.pm +++ b/lib/Slic3r/GUI/Plater.pm @@ -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 base 'Wx::Panel'; -use constant TB_LOAD => &Wx::NewId; +use constant TB_ADD => &Wx::NewId; use constant TB_REMOVE => &Wx::NewId; use constant TB_RESET => &Wx::NewId; use constant TB_ARRANGE => &Wx::NewId; @@ -48,8 +48,8 @@ sub new { $self->{config} = Slic3r::Config->new_from_defaults(qw( bed_size print_center complete_objects extruder_clearance_radius skirts skirt_distance )); + $self->{model} = Slic3r::Model->new; $self->{objects} = []; - $self->{selected_objects} = []; $self->{canvas} = Wx::Panel->new($self, -1, wxDefaultPosition, CANVAS_SIZE, wxTAB_TRAVERSAL); $self->{canvas}->SetBackgroundColour(Wx::wxWHITE); @@ -69,7 +69,7 @@ sub new { if (!&Wx::wxMSW) { Wx::ToolTip::Enable(1); $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_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), ''); @@ -159,7 +159,7 @@ sub new { EVT_BUTTON($self, $self->{btn_export_stl}, \&export_stl); 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_RESET, \&reset); 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_SETTINGS, sub { $_[0]->object_settings_dialog }); } 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_reset}, \&reset); EVT_BUTTON($self, $self->{btn_arrange}, \&arrange); @@ -357,7 +357,7 @@ sub filament_presets { return map $_->GetSelection, @{ $self->{preset_choosers}{filament} }; } -sub load { +sub add { my $self = shift; 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 ($input_file) = @_; - my $basename = basename($input_file); $Slic3r::GUI::Settings->{recent}{skein_directory} = dirname($input_file); Slic3r::GUI->save_settings; @@ -384,31 +383,32 @@ sub load_file { local $SIG{__WARN__} = Slic3r::GUI::warning_catcher($self); my $model = Slic3r::Model->read_from_file($input_file); - for my $i (0 .. $#{$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)); - } + $self->load_model_object($_) for @{$model->objects}; $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 { @@ -416,15 +416,17 @@ sub object_loaded { my ($obj_idx, %params) = @_; my $object = $self->{objects}[$obj_idx]; + my $model_object = $self->{model}->objects->[$obj_idx]; $self->{list}->InsertStringItem($obj_idx, $object->name); $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() - $self->{list}->SetItem($obj_idx, 1, $object->instances_count); - $self->{list}->SetItem($obj_idx, 2, ($object->scale * 100) . "%"); + $self->{list}->SetItem($obj_idx, 1, $model_object->instances_count); + $self->{list}->SetItem($obj_idx, 2, ($model_object->instances->[0]->scaling_factor * 100) . "%"); $self->make_thumbnail($obj_idx); $self->arrange unless $params{no_arrange}; + $self->recenter; $self->{list}->Update; $self->{list}->Select($obj_idx, 1); $self->object_list_changed; @@ -440,11 +442,11 @@ sub remove { } splice @{$self->{objects}}, $obj_idx, 1; + $self->{model}->delete_object($obj_idx); $self->{list}->DeleteItem($obj_idx); - - $self->{selected_objects} = []; - $self->selection_changed(0); $self->object_list_changed; + + $self->select_object(undef); $self->recenter; $self->{canvas}->Refresh; } @@ -453,11 +455,11 @@ sub reset { my $self = shift; @{$self->{objects}} = (); + $self->{model}->delete_all_objects; $self->{list}->DeleteAllItems; - - $self->{selected_objects} = []; - $self->selection_changed(0); $self->object_list_changed; + + $self->select_object(undef); $self->{canvas}->Refresh; } @@ -465,10 +467,14 @@ sub increase { my $self = shift; my ($obj_idx, $object) = $self->selected_object; - my $instances = $object->instances; - push @$instances, my $new_instance = $instances->[-1]->clone; - $new_instance->translate(scale(10), scale(10)); - $self->{list}->SetItem($obj_idx, 1, $object->instances_count); + my $model_object = $self->{model}->objects->[$obj_idx]; + my $last_instance = $model_object->instances->[-1]; + $model_object->add_instance( + 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; } @@ -476,9 +482,10 @@ sub decrease { my $self = shift; my ($obj_idx, $object) = $self->selected_object; - if ($object->instances_count >= 2) { - pop @{$object->instances}; - $self->{list}->SetItem($obj_idx, 1, $object->instances_count); + my $model_object = $self->{model}->objects->[$obj_idx]; + if ($model_object->instances_count >= 2) { + $model_object->delete_last_instance; + $self->{list}->SetItem($obj_idx, 1, $model_object->instances_count); } else { $self->remove; } @@ -496,18 +503,25 @@ sub rotate { my ($angle) = @_; 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 return if !$object->thumbnail; 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; $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->{canvas}->Refresh; } @@ -516,16 +530,28 @@ sub changescale { my $self = shift; 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 return if !$object->thumbnail; # 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; $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->arrange; } @@ -533,19 +559,8 @@ sub changescale { sub arrange { 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 { - my $config = $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}}; + $self->{model}->arrange_objects($self->skeinpanel->config); }; # ignore arrange warnings on purpose @@ -556,54 +571,56 @@ sub arrange { sub split_object { my $self = shift; - my ($obj_idx, $current_object) = $self->selected_object; - my $current_copies_num = $current_object->instances_count; - my $model_object = $current_object->get_model_object; + my ($obj_idx, $current_object) = $self->selected_object; + my $current_model_object = $self->{model}->objects->[$obj_idx]; - 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."); return; } - my $mesh = $model_object->mesh; - $mesh->align_to_origin; - - $mesh->repair; - my @new_meshes = @{$mesh->split}; + my @new_meshes = @{$current_model_object->volumes->[0]->mesh->split}; if (@new_meshes == 1) { Slic3r::GUI::warning_catcher($self)->("The selected object couldn't be split because it already contains a single part."); return; } - + # 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 # thumbnail thread returns) $self->remove($obj_idx); + $current_object = $obj_idx = undef; # create a bogus Model object, we only need to instantiate the new Model::Object objects my $new_model = Slic3r::Model->new; foreach my $mesh (@new_meshes) { - $mesh->repair; - my $bb = $mesh->bounding_box; - my $model_object = $new_model->add_object; - $model_object->add_volume(mesh => $mesh); - my $object = Slic3r::GUI::Plater::Object->new( - name => basename($current_object->input_file), - input_file => $current_object->input_file, - input_file_object_id => undef, - model => $new_model, - 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 ], + my $model_object = $new_model->add_object( + input_file => $current_model_object->input_file, + config => $current_model_object->config->clone, + layer_height_ranges => $current_model_object->layer_height_ranges, # TODO: clone this + material_mapping => $current_model_object->material_mapping, # TODO: clone this + ); + $model_object->add_volume( + mesh => $mesh, + material_id => $current_model_object->volumes->[0]->material_id, ); - push @{ $self->{objects} }, $object; - $self->object_loaded($#{ $self->{objects} }, no_arrange => 1); + for my $instance_idx (0..$#{ $current_model_object->instances }) { + 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 { @@ -621,7 +638,7 @@ sub export_gcode { # select output file $self->{output_file} = $main::opt{output}; { - $self->{output_file} = $self->skeinpanel->init_print->expanded_output_filepath($self->{output_file}, $self->{objects}[0]->input_file); + $self->{output_file} = $self->skeinpanel->init_print->expanded_output_filepath($self->{output_file}, $self->{model}->objects->[0]->input_file); my $dlg = Wx::FileDialog->new($self, 'Save G-code file as:', Slic3r::GUI->output_path(dirname($self->{output_file})), basename($self->{output_file}), &Slic3r::GUI::SkeinPanel::FILE_WILDCARDS->{gcode}, wxFD_SAVE); if ($dlg->ShowModal != wxID_OK) { @@ -636,8 +653,6 @@ sub export_gcode { $self->statusbar->StartBusy; - $_->free_model_object for @{$self->{objects}}; - # It looks like declaring a local $SIG{__WARN__} prevents the ugly # "Attempt to free unreferenced scalar" warning... local $SIG{__WARN__} = Slic3r::GUI::warning_catcher($self); @@ -704,12 +719,13 @@ sub export_gcode2 { eval { $print->config->validate; - $print->add_model($self->make_model); + $print->add_model_object($_) for @{ $self->{model}->objects }; $print->validate; { my @warnings = (); local $SIG{__WARN__} = sub { push @warnings, $_[0] }; + my %params = ( output_file => $output_file, status_cb => sub { $params{progressbar}->(@_) }, @@ -764,7 +780,7 @@ sub export_stl { my $self = shift; 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"); } @@ -772,7 +788,7 @@ sub export_amf { my $self = shift; 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"); } @@ -784,7 +800,7 @@ sub _get_export_file { 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; 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); @@ -798,47 +814,14 @@ sub _get_export_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 { my $self = shift; my ($obj_idx) = @_; - my $object = $self->{objects}[$obj_idx]; - $object->thumbnail_scaling_factor($self->{scaling_factor}); - $object->thumbnail(Slic3r::ExPolygon::Collection->new); + my $plater_object = $self->{objects}[$obj_idx]; + $plater_object->thumbnail(Slic3r::ExPolygon::Collection->new); my $cb = sub { - $object->make_thumbnail; + $plater_object->make_thumbnail($self->{model}, $obj_idx); if ($Slic3r::have_threads) { Wx::PostEvent($self, Wx::PlThreadEvent->new(-1, $THUMBNAIL_DONE_EVENT, shared_clone([ $obj_idx ]))); @@ -857,33 +840,38 @@ sub on_thumbnail_made { my $self = shift; my ($obj_idx) = @_; - $self->{objects}[$obj_idx]->_transform_thumbnail; + $self->{objects}[$obj_idx]->transform_thumbnail($self->{model}, $obj_idx); $self->recenter; $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 { my $self = shift; return unless @{$self->{objects}}; - # calculate displacement needed to center the print - my @bbs = (); - foreach my $plobj (@{$self->{objects}}) { - my $bb = $plobj->transformed_bounding_box; # in scaled coordinates - foreach my $inst (@{$plobj->instances}) { - push @bbs, my $inst_bb = $bb->clone; - $inst_bb->translate(@$inst, 0); - } - } - my $print_bb = Slic3r::Geometry::BoundingBox->merge(@bbs); + # get model bounding box in pixels + my $print_bb = $self->{model}->bounding_box; + $print_bb->scale($self->{scaling_factor}); + + # get model size in pixels my $print_size = $print_bb->size; - # $self->{shift} contains the offset in pixels to add to object instances in order to center them - # it is expressed in upwards Y + # $self->{shift} contains the offset in pixels to add to object thumbnails + # in order to center them $self->{shift} = [ - $self->to_pixel(-$print_bb->x_min) + ($self->{canvas}->GetSize->GetWidth - $self->to_pixel($print_size->[X])) / 2, - $self->to_pixel(-$print_bb->y_min) + ($self->{canvas}->GetSize->GetHeight - $self->to_pixel($print_size->[Y])) / 2, + -$print_bb->x_min + ($self->{canvas}->GetSize->GetWidth - $print_size->[X]) / 2, + -$print_bb->y_min + ($self->{canvas}->GetSize->GetHeight - $print_size->[Y]) / 2, ]; } @@ -921,7 +909,6 @@ sub _update_bed_size { # when the canvas is not rendered yet, its GetSize() method returns 0,0 # scaling_factor is expressed in pixel / mm $self->{scaling_factor} = CANVAS_SIZE->[X] / max(@{ $self->{config}->bed_size }); - $_->thumbnail_scaling_factor($self->{scaling_factor}) for @{ $self->{objects} }; $self->recenter; } @@ -971,55 +958,61 @@ sub repaint { # draw thumbnails $dc->SetPen(wxBLACK_PEN); - @{$parent->{object_previews}} = (); + $parent->clean_instance_thumbnails; for my $obj_idx (0 .. $#{$parent->{objects}}) { my $object = $parent->{objects}[$obj_idx]; + my $model_object = $parent->{model}->objects->[$obj_idx]; next unless defined $object->thumbnail; - for my $instance_idx (0 .. $#{$object->instances}) { - my $instance = $object->instances->[$instance_idx]; + for my $instance_idx (0 .. $#{$model_object->instances}) { + my $instance = $model_object->instances->[$instance_idx]; + next if !defined $object->transformed_thumbnail; - my $thumbnail = $object->transformed_thumbnail->clone; - $thumbnail->translate(map $_ * $parent->{scaling_factor}, @$instance); - $thumbnail->translate(map scale($parent->{shift}[$_]), (X,Y)); - push @{$parent->{object_previews}}, [ $obj_idx, $instance_idx, $thumbnail ]; # $thumbnail has scaled coordinates + my $thumbnail = $object->transformed_thumbnail->clone; # in scaled coordinates + $thumbnail->scale(&Slic3r::SCALING_FACTOR * $parent->{scaling_factor}); # in unscaled pixels + $thumbnail->translate(map $_ * $parent->{scaling_factor}, @{$instance->offset}); + $thumbnail->translate(@{$parent->{shift}}); - my $drag_object = $self->{drag_object}; - if (defined $drag_object && $obj_idx == $drag_object->[0] && $instance_idx == $drag_object->[1]) { + $object->instance_thumbnails->[$instance_idx] = $thumbnail; + + if (defined $self->{drag_object} && $self->{drag_object}[0] == $obj_idx && $self->{drag_object}[1] == $instance_idx) { $dc->SetBrush($parent->{dragged_brush}); - } elsif (grep { $_->[0] == $obj_idx } @{$parent->{selected_objects}}) { + } elsif ($object->selected) { $dc->SetBrush($parent->{selected_brush}); } else { $dc->SetBrush($parent->{objects_brush}); } foreach my $expolygon (@$thumbnail) { 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); } - # if sequential printing is enabled and we have more than one object - if ($parent->{config}->complete_objects && (map @{$_->instances}, @{$parent->{objects}}) > 1) { - my @points = map @{$_->contour}, @{$parent->{object_previews}->[-1][2]}; - if (@points >= 3) { - my ($clearance) = @{offset([convex_hull(\@points)], scale($parent->{config}->extruder_clearance_radius / 2) * $parent->{scaling_factor}, 100, JT_ROUND)}; - $clearance->scale(&Slic3r::SCALING_FACTOR); - $dc->SetPen($parent->{clearance_pen}); - $dc->SetBrush($parent->{transparent_brush}); - $dc->DrawPolygon($parent->_y($clearance), 0, 0); - } + if (0) { + # draw bounding box for debugging purposes + my $bb = $model_object->instance_bounding_box($instance_idx); + $bb->scale($parent->{scaling_factor}); + # no need to translate by instance offset because instance_bounding_box() does that + $bb->translate(@{$parent->{shift}}, 0); + my $points = $bb->polygon->pp; + $dc->SetPen($parent->{clearance_pen}); + $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 - if (@{$parent->{object_previews}} && $parent->{config}->skirts) { - my @points = map @{$_->contour}, map @{$_->[2]}, @{$parent->{object_previews}}; + if (@{$parent->{objects}} && $parent->{config}->skirts) { + my @points = map @{$_->contour}, map @$_, map @{$_->instance_thumbnails}, @{$parent->{objects}}; if (@points >= 3) { - my ($convex_hull) = @{offset([convex_hull(\@points)], scale($parent->{config}->skirt_distance) * $parent->{scaling_factor}, 1, JT_ROUND)}; - $convex_hull->scale(&Slic3r::SCALING_FACTOR); + my ($convex_hull) = @{offset([convex_hull(\@points)], $parent->{config}->skirt_distance * $parent->{scaling_factor}, 100, JT_ROUND)}; $dc->SetPen($parent->{skirt_pen}); $dc->SetBrush($parent->{transparent_brush}); $dc->DrawPolygon($parent->_y($convex_hull), 0, 0); @@ -1034,20 +1027,22 @@ sub mouse_event { my $parent = $self->GetParent; 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)) { - $parent->{selected_objects} = []; - $parent->{list}->Select($parent->{list}->GetFirstSelected, 0); - $parent->selection_changed(0); - for my $preview (@{$parent->{object_previews}}) { - my ($obj_idx, $instance_idx, $thumbnail) = @$preview; - if (defined first { $_->contour->contains_point($pos) } @$thumbnail) { - $parent->{selected_objects} = [ [$obj_idx, $instance_idx] ]; - $parent->{list}->Select($obj_idx, 1); - $parent->selection_changed(1); - my $instance = $parent->{objects}[$obj_idx]->instances->[$instance_idx]; - $self->{drag_start_pos} = [ map $pos->[$_] - scale($parent->{shift}[$_]) - scale($parent->to_pixel($instance->[$_])), X,Y ]; # displacement between the click and the instance origin - $self->{drag_object} = $preview; + $parent->select_object(undef); + for my $obj_idx (0 .. $#{$parent->{objects}}) { + my $object = $parent->{objects}->[$obj_idx]; + for my $instance_idx (0 .. $#{ $object->instance_thumbnails }) { + my $thumbnail = $object->instance_thumbnails->[$instance_idx]; + if ($thumbnail->contains_point($pos)) { + $parent->select_object($obj_idx); + my $instance = $parent->{model}->objects->[$obj_idx]->instances->[$instance_idx]; + $self->{drag_start_pos} = [ # displacement between the click and the instance origin + $pos->x - $parent->{shift}[X] - ($instance->offset->[X] * $parent->{scaling_factor}), + $pos->y - $parent->{shift}[Y] - ($instance->offset->[Y] * $parent->{scaling_factor}), + ]; + $self->{drag_object} = [ $obj_idx, $instance_idx ]; + } } } $parent->Refresh; @@ -1058,23 +1053,21 @@ sub mouse_event { $self->{drag_object} = undef; $self->SetCursor(wxSTANDARD_CURSOR); } elsif ($event->ButtonDClick) { - $parent->object_preview_dialog if @{$parent->{selected_objects}}; + $parent->object_preview_dialog if $parent->selected_object; } elsif ($event->Dragging) { return if !$self->{drag_start_pos}; # concurrency problems - for my $preview ($self->{drag_object}) { - my ($obj_idx, $instance_idx, $thumbnail) = @$preview; - $parent->{objects}[$obj_idx]->instances->[$instance_idx] = Slic3r::Point->new( - map { $parent->to_units(unscale($pos->[$_] - $self->{drag_start_pos}[$_]) - $parent->{shift}[$_]) } (X,Y), - ); - $parent->Refresh; - } + my ($obj_idx, $instance_idx) = @{ $self->{drag_object} }; + my $model_object = $parent->{model}->objects->[$obj_idx]; + $model_object->instances->[$instance_idx]->offset([ + ($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}, + ]); + $model_object->update_bounding_box; + $parent->Refresh; } elsif ($event->Moving) { my $cursor = wxSTANDARD_CURSOR; - for my $preview (@{$parent->{object_previews}}) { - if (defined first { $_->contour->contains_point($pos) } @{ $preview->[2] }) { - $cursor = Wx::Cursor->new(wxCURSOR_HAND); - last; - } + if (defined first { $_->contains_point($pos) } map @{$_->instance_thumbnails}, @{ $parent->{objects} }) { + $cursor = Wx::Cursor->new(wxCURSOR_HAND); } $self->SetCursor($cursor); } @@ -1084,9 +1077,8 @@ sub list_item_deselected { my ($self, $event) = @_; if ($self->{list}->GetFirstSelected == -1) { - $self->{selected_objects} = []; + $self->select_object(undef); $self->{canvas}->Refresh; - $self->selection_changed(0); } } @@ -1094,9 +1086,8 @@ sub list_item_selected { my ($self, $event) = @_; my $obj_idx = $event->GetIndex; - $self->{selected_objects} = [ grep $_->[0] == $obj_idx, @{$self->{object_previews}} ]; + $self->select_object($obj_idx); $self->{canvas}->Refresh; - $self->selection_changed(1); } sub list_item_activated { @@ -1120,7 +1111,8 @@ sub object_preview_dialog { } 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; } @@ -1139,7 +1131,8 @@ sub object_settings_dialog { return unless $self->validate_config; 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; } @@ -1160,7 +1153,9 @@ sub object_list_changed { sub selection_changed { 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'; $self->{"btn_$_"}->$method @@ -1173,13 +1168,14 @@ sub selection_changed { if ($self->{object_info_size}) { # have we already loaded the info pane? if ($have_sel) { - my ($obj_idx, $object) = $self->selected_object; - $self->{object_info_size}->SetLabel(sprintf("%.2f x %.2f x %.2f", map unscale($_), @{$object->transformed_size})); - $self->{object_info_materials}->SetLabel($object->materials); + my $model_object = $self->{model}->objects->[$obj_idx]; + my $model_instance = $model_object->instances->[0]; + $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) { - $self->{object_info_volume}->SetLabel(sprintf('%.2f', $stats->{volume} * ($object->scale**3))); - $self->{object_info_facets}->SetLabel(sprintf('%d (%d shells)', $object->facets, $stats->{number_of_parts})); + if (my $stats = $model_object->mesh_stats) { + $self->{object_info_volume}->SetLabel(sprintf('%.2f', $stats->{volume} * ($model_instance->scaling_factor**3))); + $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)})) { $self->{object_info_manifold}->SetLabel(sprintf("Auto-repaired (%d errors)", $errors)); $self->{object_info_manifold_warning_icon}->Show; @@ -1205,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 { 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]), } @@ -1276,155 +1287,47 @@ use List::Util qw(first); use Slic3r::Geometry qw(X Y Z MIN MAX deg2rad); has 'name' => (is => 'rw', required => 1); -has 'input_file' => (is => 'rw', required => 1); -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 'thumbnail' => (is => 'rw'); # ExPolygon::Collection in scaled model units with no transforms has 'transformed_thumbnail' => (is => 'rw'); -has 'thumbnail_scaling_factor' => (is => 'rw', trigger => \&_transform_thumbnail); -has 'config' => (is => 'rw', default => sub { Slic3r::Config->new }); -has 'layer_height_ranges' => (is => 'rw', default => sub { [] }); # [ z_min, z_max, layer_height ] -has 'material_mapping' => (is => 'rw', default => sub { {} }); # { material_id => extruder_idx } -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}; -} +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 'selected' => (is => 'rw', default => sub { 0 }); 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 - $mesh->repair; - if (@{$mesh->facets} <= 5000) { + my $mesh = $model->objects->[$obj_idx]->raw_mesh; + + if ($mesh->facets_count <= 5000) { # remove polygons with area <= 1mm my $area_threshold = Slic3r::Geometry::scale 1; $self->thumbnail->append( grep $_->area >= $area_threshold, - @{ $mesh->horizontal_projection }, + @{ $mesh->horizontal_projection }, # horizontal_projection returns scaled expolygons ); $self->thumbnail->simplify(0.5); } else { - my $convex_hull = Slic3r::ExPolygon->new($self->convex_hull); + my $convex_hull = Slic3r::ExPolygon->new($mesh->convex_hull); $self->thumbnail->append($convex_hull); } return $self->thumbnail; } -sub _transform_thumbnail { - my $self = shift; +sub transform_thumbnail { + my ($self, $model, $obj_idx) = @_; 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); } -# 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; diff --git a/lib/Slic3r/GUI/Plater/ObjectPreviewDialog.pm b/lib/Slic3r/GUI/Plater/ObjectPreviewDialog.pm index 671fe430b..57edcedb4 100644 --- a/lib/Slic3r/GUI/Plater/ObjectPreviewDialog.pm +++ b/lib/Slic3r/GUI/Plater/ObjectPreviewDialog.pm @@ -11,10 +11,10 @@ sub new { my $class = shift; my ($parent, %params) = @_; 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); - $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->SetMinSize($self->GetSize); diff --git a/lib/Slic3r/GUI/Plater/ObjectSettingsDialog.pm b/lib/Slic3r/GUI/Plater/ObjectSettingsDialog.pm index cf559f57e..30a5dadeb 100644 --- a/lib/Slic3r/GUI/Plater/ObjectSettingsDialog.pm +++ b/lib/Slic3r/GUI/Plater/ObjectSettingsDialog.pm @@ -11,12 +11,12 @@ sub new { my $class = shift; my ($parent, %params) = @_; 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}->AddPage($self->{settings} = Slic3r::GUI::Plater::ObjectDialog::SettingsTab->new($self->{tabpanel}, object => $self->{object}), "Settings"); - $self->{tabpanel}->AddPage($self->{layers} = Slic3r::GUI::Plater::ObjectDialog::LayersTab->new($self->{tabpanel}, object => $self->{object}), "Layers"); - $self->{tabpanel}->AddPage($self->{materials} = Slic3r::GUI::Plater::ObjectDialog::MaterialsTab->new($self->{tabpanel}, object => $self->{object}), "Materials"); + $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}), "Layers"); + $self->{tabpanel}->AddPage($self->{materials} = Slic3r::GUI::Plater::ObjectDialog::MaterialsTab->new($self->{tabpanel}), "Materials"); my $buttons = $self->CreateStdDialogButtonSizer(wxOK); EVT_BUTTON($self, wxID_OK, sub { @@ -42,17 +42,24 @@ sub new { 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; use Wx qw(:dialog :id :misc :sizer :systemsettings :button :icon); use Wx::Grid; use Wx::Event qw(EVT_BUTTON); -use base 'Wx::Panel'; +use base 'Slic3r::GUI::Plater::ObjectDialog::BaseTab'; sub new { my $class = shift; my ($parent, %params) = @_; my $self = $class->SUPER::new($parent, -1, wxDefaultPosition, wxDefaultSize); - $self->{object} = $params{object}; $self->{sizer} = Wx::BoxSizer->new(wxVERTICAL); @@ -79,7 +86,7 @@ sub new { my $idx = $choice->GetSelection; return if $idx == -1; # lack of selected item, can happen on Windows 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; }); @@ -105,7 +112,7 @@ sub update_optgroup { $self->{options_sizer}->Clear(1); - my $config = $self->{object}->config; + my $config = $self->model_object->config; my %categories = (); foreach my $opt_key (keys %$config) { 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 $btn = Wx::BitmapButton->new($self, -1, Wx::Bitmap->new("$Slic3r::var/delete.png", wxBITMAP_TYPE_PNG)); 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 }); }); return $btn; @@ -141,7 +148,7 @@ sub CanClose { # validate options before allowing user to dismiss the dialog # the validate method only works on full configs so we have # 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 { $config->validate; }; @@ -153,13 +160,12 @@ package Slic3r::GUI::Plater::ObjectDialog::LayersTab; use Wx qw(:dialog :id :misc :sizer :systemsettings); use Wx::Grid; use Wx::Event qw(EVT_GRID_CELL_CHANGED); -use base 'Wx::Panel'; +use base 'Slic3r::GUI::Plater::ObjectDialog::BaseTab'; sub new { my $class = shift; my ($parent, %params) = @_; my $self = $class->SUPER::new($parent, -1, wxDefaultPosition, wxDefaultSize); - $self->{object} = $params{object}; my $sizer = Wx::BoxSizer->new(wxVERTICAL); @@ -182,7 +188,7 @@ sub new { $grid->SetDefaultCellAlignment(wxALIGN_CENTRE, wxALIGN_CENTRE); # load data - foreach my $range (@{ $self->{object}->layer_height_ranges }) { + foreach my $range (@{ $self->model_object->layer_height_ranges }) { $grid->AppendRows(1); my $i = $grid->GetNumberRows-1; $grid->SetCellValue($i, $_, $range->[$_]) for 0..2; @@ -242,7 +248,7 @@ sub Closing { my $self = shift; # 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 { @@ -261,7 +267,7 @@ package Slic3r::GUI::Plater::ObjectDialog::MaterialsTab; use Wx qw(:dialog :id :misc :sizer :systemsettings :button :icon); use Wx::Grid; use Wx::Event qw(EVT_BUTTON); -use base 'Wx::Panel'; +use base 'Slic3r::GUI::Plater::ObjectDialog::BaseTab'; sub new { my $class = shift; @@ -280,12 +286,12 @@ sub new { } # 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 $self->{mapping} = { (map { $self->{materials}[$_] => $_+1 } 0..$#{ $self->{materials} }), # defaults - %{$self->{object}->material_mapping}, + %{$self->model_object->material_mapping}, }; my $optgroup = Slic3r::GUI::OptionsGroup->new( parent => $self, @@ -298,7 +304,7 @@ sub new { { opt_key => "material_extruder_$_", 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, default => $self->{mapping}{$material_id}, on_change => sub { $self->{mapping}{$material_id} = $_[0] }, @@ -318,7 +324,7 @@ sub Closing { my $self = shift; # save mappings into the plater object - $self->{object}->material_mapping($self->{mapping}); + $self->model_object->material_mapping($self->{mapping}); } 1; diff --git a/lib/Slic3r/GUI/PreviewCanvas.pm b/lib/Slic3r/GUI/PreviewCanvas.pm index 69626d44c..9985d882d 100644 --- a/lib/Slic3r/GUI/PreviewCanvas.pm +++ b/lib/Slic3r/GUI/PreviewCanvas.pm @@ -28,16 +28,15 @@ sub new { $self->sphi(45); $self->stheta(-45); - $object->align_to_origin; - $self->object_center($object->center); - $self->object_size($object->size); + my $bounding_box = $object->raw_mesh->bounding_box; + $self->object_center($bounding_box->center); + $self->object_size($bounding_box->size); # group mesh(es) by material my @materials = (); $self->volumes([]); foreach my $volume (@{$object->volumes}) { my $mesh = $volume->mesh; - $mesh->repair; my $material_id = $volume->material_id // '_'; my $color_idx = first { $materials[$_] eq $material_id } 0..$#materials; diff --git a/lib/Slic3r/GUI/SkeinPanel.pm b/lib/Slic3r/GUI/SkeinPanel.pm index 2ea3dd765..efb3f3946 100644 --- a/lib/Slic3r/GUI/SkeinPanel.pm +++ b/lib/Slic3r/GUI/SkeinPanel.pm @@ -134,7 +134,17 @@ sub quick_slice { Slic3r::GUI->save_settings; my $print = $self->init_print; - $print->add_model(Slic3r::Model->read_from_file($input_file)); + my $model = Slic3r::Model->read_from_file($input_file); + + if ($model->has_objects_with_no_instances) { + # 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; + } + $model->arrange_objects($config); + } + + $print->add_model_object($_) for @{ $model->objects }; $print->validate; # select output file diff --git a/lib/Slic3r/Geometry/BoundingBox.pm b/lib/Slic3r/Geometry/BoundingBox.pm index 27d6fd275..a05039c7c 100644 --- a/lib/Slic3r/Geometry/BoundingBox.pm +++ b/lib/Slic3r/Geometry/BoundingBox.pm @@ -130,6 +130,16 @@ sub min_point { return Slic3r::Point->new($self->extents->[X][MIN], $self->extents->[Y][MIN]); } +sub min_point3 { + my $self = shift; + return [ map $self->extents->[$_][MIN], (X,Y,Z) ]; +} + +sub vector_to_origin { + my $self = shift; + return [ map -$_, @{$self->min_point3} ]; +} + sub max_point { my $self = shift; return Slic3r::Point->new($self->extents->[X][MAX], $self->extents->[Y][MAX]); diff --git a/lib/Slic3r/Model.pm b/lib/Slic3r/Model.pm index 22126602d..b960b4926 100644 --- a/lib/Slic3r/Model.pm +++ b/lib/Slic3r/Model.pm @@ -6,7 +6,6 @@ use Slic3r::Geometry qw(X Y Z MIN move_points); has 'materials' => (is => 'ro', default => sub { {} }); has 'objects' => (is => 'ro', default => sub { [] }); -has '_bounding_box' => (is => 'rw'); sub read_from_file { my $class = shift; @@ -28,40 +27,61 @@ sub merge { my $new_model = ref($class) ? $class : $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; } sub add_object { my $self = shift; - my $object = Slic3r::Model::Object->new(model => $self, @_); - push @{$self->objects}, $object; - $self->_bounding_box(undef); - return $object; + my $new_object; + if (@_ == 1) { + # we have a Model::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 { @@ -74,104 +94,111 @@ sub set_material { ); } -sub arrange_objects { - my $self = shift; - my ($config) = @_; +sub duplicate_objects_grid { + my ($self, $grid, $distance) = @_; - # do we have objects with no position? - if (first { !defined $_->instances } @{$self->objects}) { - # we shall redefine positions for all objects - - my ($copies, @positions) = $self->_arrange( - config => $config, - items => $self->objects, - ); - - # apply positions to objects - foreach my $object (@{$self->objects}) { - $object->align_to_origin; - - $object->instances([]); + die "Grid duplication is not supported with multiple objects\n" + if @{$self->objects} > 1; + + my $object = $self->objects->[0]; + @{$object->instances} = (); + + my $size = $object->bounding_box->size; + for my $x_copy (1..$grid->[X]) { + for my $y_copy (1..$grid->[Y]) { $object->add_instance( - offset => $_, - rotation => 0, - ) for splice @positions, 0, $copies; + offset => [ + ($size->[X] + $distance) * ($x_copy-1), + ($size->[Y] + $distance) * ($y_copy-1), + ], + ); } - - } else { - # we only have objects with defined position - - # align the whole model to origin as it is - $self->align_to_origin; - - # arrange this model as a whole - my ($copies, @positions) = $self->_arrange( - config => $config, - items => [$self], - ); - - # apply positions to objects by translating the current positions - foreach my $object (@{$self->objects}) { - my @old_instances = @{$object->instances}; - $object->instances([]); - foreach my $instance (@old_instances) { + } +} + +# this will append more instances to each object +# and then automatically rearrange everything +sub duplicate_objects { + my ($self, $config, $copies_num) = @_; + + foreach my $object (@{$self->objects}) { + my @instances = @{$object->instances}; + foreach my $instance (@instances) { + ### $object->add_instance($instance->clone); if we had clone() + $object->add_instance( + offset => [ @{$instance->offset} ], + rotation => $instance->rotation, + scaling_factor => $instance->scaling_factor, + ) for 2..$copies_num; + } + } + + $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( - offset => $_, - rotation => $instance->rotation, - scaling_factor => $instance->scaling_factor, - ) for move_points($instance->offset, @positions); + offset => [ $instance->offset->[X] + $pos->[X], $instance->offset->[Y] + $pos->[Y] ], + rotation => $instance->rotation, + scaling_factor => $instance->scaling_factor, + ); } } + $object->update_bounding_box; } } sub _arrange { - my $self = shift; - my %params = @_; + my ($self, $config, $sizes) = @_; - my $config = $params{config}; - my @items = @{$params{items}}; # can be Model or Object objects, they have to implement size() - - if ($config->duplicate_grid->[X] > 1 || $config->duplicate_grid->[Y] > 1) { - if (@items > 1) { - 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); - } + my $partx = max(map $_->[X], @$sizes); + my $party = max(map $_->[Y], @$sizes); + return Slic3r::Geometry::arrange + (scalar(@$sizes), $partx, $party, (map $_, @{$config->bed_size}), + $config->min_object_distance, $config); } -sub size { - my $self = shift; - return $self->bounding_box->size; +sub has_objects_with_no_instances { + my ($self) = @_; + return (first { !defined $_->instances } @{$self->objects}) ? 1 : 0; } +# this returns the bounding box of the *transformed* instances sub bounding_box { my $self = shift; - - if (!defined $self->_bounding_box) { - $self->_bounding_box(Slic3r::Geometry::BoundingBox->merge(map $_->bounding_box, @{$self->objects})); - } - return $self->_bounding_box->clone; + return Slic3r::Geometry::BoundingBox->merge(map $_->bounding_box, @{ $self->objects }); } sub align_to_origin { @@ -181,52 +208,33 @@ sub align_to_origin { # have lowest value for each axis at coordinate 0 { 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 { my @instances = map @{$_->instances}, @{$self->objects}; 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 { - my $self = shift; - $_->scale(@_) for @{$self->objects}; - $self->_bounding_box->scale(@_) if defined $self->_bounding_box; -} - -sub move { +sub translate { my $self = shift; my @shift = @_; - $_->move(@shift) for @{$self->objects}; - $self->_bounding_box->translate(@shift) if defined $self->_bounding_box; + $_->translate(@shift) for @{$self->objects}; } # flattens everything to a single mesh sub mesh { 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->align_to_origin; - $mesh->rotate($instance->rotation, $object->center_2D); - $mesh->scale($instance->scaling_factor); - $mesh->translate(@{$instance->offset}, 0); - } - push @meshes, $mesh; - } - } - my $mesh = Slic3r::TriangleMesh->new; - $mesh->merge($_) for @meshes; + $mesh->merge($_->mesh) for @{$self->objects}; return $mesh; } @@ -249,24 +257,19 @@ sub split_meshes { foreach my $mesh (@{$volume->mesh->split}) { my $new_object = $self->add_object( input_file => $object->input_file, - config => $object->config, - layer_height_ranges => $object->layer_height_ranges, + config => $object->config->clone, + layer_height_ranges => $object->layer_height_ranges, # TODO: this needs to be cloned ); $new_object->add_volume( mesh => $mesh, material_id => $volume->material_id, ); - # let's now align the new object to the origin and put its displacement - # (extents) in the instances info - my $bb = $mesh->bounding_box; - $new_object->align_to_origin; - - # add one instance per original instance applying the displacement + # add one instance per original instance $new_object->add_instance( - offset => [ $_->offset->[X] + $bb->x_min, $_->offset->[Y] + $bb->y_min ], - rotation => $_->rotation, - scaling_factor => $_->scaling_factor, + offset => [ @{$_->offset} ], + rotation => $_->rotation, + scaling_factor => $_->scaling_factor, ) for @{ $object->instances // [] }; } } @@ -302,17 +305,16 @@ use Moo; use File::Basename qw(basename); use List::Util qw(first sum); -use Slic3r::Geometry qw(X Y Z MIN MAX move_points move_points_3D); -use Storable qw(dclone); +use Slic3r::Geometry qw(X Y Z MIN MAX); -has 'input_file' => (is => 'rw'); -has 'model' => (is => 'ro', weak_ref => 1, required => 1); -has 'volumes' => (is => 'ro', default => sub { [] }); -has 'instances' => (is => 'rw'); # in unscaled coordinates -has 'config' => (is => 'rw', default => sub { Slic3r::Config->new }); -has 'layer_height_ranges' => (is => 'rw', default => sub { [] }); # [ z_min, z_max, layer_height ] -has 'material_mapping' => (is => 'rw', default => sub { {} }); # { material_id => extruder_idx } -has '_bounding_box' => (is => 'rw'); +has 'input_file' => (is => 'rw'); +has 'model' => (is => 'ro', weak_ref => 1, required => 1); +has 'volumes' => (is => 'ro', default => sub { [] }); +has 'instances' => (is => 'rw'); +has 'config' => (is => 'rw', default => sub { Slic3r::Config->new }); +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 '_bounding_box' => (is => 'rw'); sub add_volume { my $self = shift; @@ -323,54 +325,78 @@ sub add_volume { %args, ); $self->_bounding_box(undef); - $self->model->_bounding_box(undef); return $volume; } sub add_instance { my $self = shift; + my %params = @_; $self->instances([]) if !defined $self->instances; - push @{$self->instances}, Slic3r::Model::Instance->new(object => $self, @_); - $self->model->_bounding_box(undef); - return $self->instances->[-1]; + push @{$self->instances}, my $i = Slic3r::Model::Instance->new(object => $self, %params); + $self->_bounding_box(undef); + 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 $mesh = Slic3r::TriangleMesh->new; - $mesh->merge($_->mesh) for @{$self->volumes}; + $mesh->merge($_->mesh) for @{ $self->volumes }; return $mesh; } -sub size { +# flattens all volumes and instances into a single mesh +sub mesh { 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 { - my $self = shift; - return $self->bounding_box->center; -} - -sub center_2D { - my $self = shift; - return $self->bounding_box->center_2D; +sub update_bounding_box { + my ($self) = @_; + $self->_bounding_box($self->mesh->bounding_box); } +# this returns the bounding box of the *transformed* instances sub bounding_box { my $self = shift; - 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); - } + $self->update_bounding_box if !defined $self->_bounding_box; 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 { my $self = shift; @@ -378,11 +404,38 @@ sub align_to_origin { # have lowest value for each axis at coordinate 0 my $bb = $self->bounding_box; my @shift = map -$bb->extents->[$_][MIN], X,Y,Z; - $self->move(@shift); + $self->translate(@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 @shift = @_; @@ -390,24 +443,6 @@ sub move { $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 { my $self = shift; @@ -425,7 +460,7 @@ sub unique_materials { sub facets_count { my $self = shift; - return sum(map $_->facets_count, @{$self->volumes}); + return sum(map $_->mesh->facets_count, @{$self->volumes}); } sub needed_repair { @@ -465,21 +500,27 @@ sub print_info { } } -sub clone { dclone($_[0]) } - package Slic3r::Model::Volume; use Moo; -has 'object' => (is => 'ro', weak_ref => 1, required => 1); -has 'material_id' => (is => 'rw'); -has 'mesh' => (is => 'rw', required => 1); +has 'object' => (is => 'ro', weak_ref => 1, required => 1); +has 'material_id' => (is => 'rw'); +has 'mesh' => (is => 'rw', required => 1); package Slic3r::Model::Instance; use Moo; -has 'object' => (is => 'ro', weak_ref => 1, required => 1); -has 'rotation' => (is => 'rw', default => sub { 0 }); # around mesh center point -has 'scaling_factor' => (is => 'rw', default => sub { 1 }); -has 'offset' => (is => 'rw'); # must be Slic3r::Point object in scaled coordinates +has 'object' => (is => 'ro', weak_ref => 1, required => 1); +has 'rotation' => (is => 'rw', default => sub { 0 }); # around mesh center point +has 'scaling_factor' => (is => 'rw', default => sub { 1 }); +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; diff --git a/lib/Slic3r/Print.pm b/lib/Slic3r/Print.pm index e79585b1a..bfb7eaf1a 100644 --- a/lib/Slic3r/Print.pm +++ b/lib/Slic3r/Print.pm @@ -77,89 +77,76 @@ sub _build_has_support_material { # caller is responsible for supplying models whose objects don't collide # and have explicit instance positions -sub add_model { +sub add_model_object { my $self = shift; - my ($model) = @_; + my ($object) = @_; - # optimization: if avoid_crossing_perimeters is enabled, split - # this mesh into distinct objects so that we reduce the complexity - # of the graphs - # -- Disabling this one because there are too many legit objects having nested shells - # -- It also caused a bug where plater rotation was applied to each single object by the - # -- code below (thus around its own center), instead of being applied to the whole - # -- thing before the split. - ###$model->split_meshes if $Slic3r::Config->avoid_crossing_perimeters && !$Slic3r::Config->complete_objects; + # read the material mapping provided by the model object, if any + my %matmap = %{ $object->material_mapping || {} }; + $_-- for values %matmap; # extruders in the mapping are 1-indexed but we want 0-indexed - my %unmapped_materials = (); - foreach my $object (@{ $model->objects }) { - # we align object to origin before applying transformations - my @align = $object->align_to_origin; - - # extract meshes by material - my @meshes = (); # by region_id - foreach my $volume (@{$object->volumes}) { - my $region_id; - if (defined $volume->material_id) { - if ($object->material_mapping) { - $region_id = $object->material_mapping->{$volume->material_id} - 1 - if defined $object->material_mapping->{$volume->material_id}; - } - $region_id //= $unmapped_materials{$volume->material_id}; - if (!defined $region_id) { - $region_id = $unmapped_materials{$volume->material_id} = scalar(keys %unmapped_materials); - } + my %meshes = (); # region_id => TriangleMesh + foreach my $volume (@{$object->volumes}) { + my $region_id; + if (defined $volume->material_id) { + if (!exists $matmap{ $volume->material_id }) { + # there's no mapping between this material and a region + $matmap{ $volume->material_id } = scalar(@{ $self->regions }); } - $region_id //= 0; - - my $mesh = $volume->mesh->clone; - # should the object contain multiple volumes of the same material, merge them - $meshes[$region_id] = $meshes[$region_id] - ? Slic3r::TriangleMesh->merge($meshes[$region_id], $mesh) - : $mesh; - } - $self->regions->[$_] //= Slic3r::Print::Region->new for 0..$#meshes; - - foreach my $mesh (grep $_, @meshes) { - # 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 - - # we ignore the per-instance transformations currently and only - # consider the first one - if ($object->instances && @{$object->instances}) { - $mesh->rotate($object->instances->[0]->rotation, $object->center_2D); - $mesh->scale($object->instances->[0]->scaling_factor); - } - $mesh->repair; + $region_id = $matmap{ $volume->material_id }; + } else { + $region_id = 0; } - # we also align object after transformations so that we only work with positive coordinates - # and the assumption that bounding_box === size works - my $bb = Slic3r::Geometry::BoundingBox->merge(map $_->bounding_box, grep $_, @meshes); - my @align2 = map -$bb->extents->[$_][MIN], (X,Y,Z); - $_->translate(@align2) for grep $_, @meshes; + # instantiate region if it does not exist + $self->regions->[$region_id] //= Slic3r::Print::Region->new; - my $scaled_bb = $bb->clone; - $scaled_bb->scale(1 / &Slic3r::SCALING_FACTOR); - - # initialize print object - push @{$self->objects}, Slic3r::Print::Object->new( - print => $self, - meshes => [ @meshes ], - copies => [ - map Slic3r::Point->new(@$_), - $object->instances - ? (map [ scale($_->offset->[X] - $align[X]) - $align2[X], scale($_->offset->[Y] - $align[Y]) - $align2[Y] ], @{$object->instances}) - : [0,0], - ], - size => $scaled_bb->size, # transformed size - input_file => $object->input_file, - config_overrides => $object->config, - layer_height_ranges => $object->layer_height_ranges, + # if a mesh is already associated to this region, append this one to it + $meshes{$region_id} //= Slic3r::TriangleMesh->new; + $meshes{$region_id}->merge($volume->mesh); + } + + # bounding box of the original meshes in original position in unscaled coordinates + my $bb1 = Slic3r::Geometry::BoundingBox->merge(map $_->bounding_box, values %meshes); + + foreach my $mesh (values %meshes) { + # we ignore the per-instance transformations currently and only + # consider the first one + $object->instances->[0]->transform_mesh($mesh); + } + + # we align object also after transformations so that we only work with positive coordinates + # and the assumption that bounding_box === size works + my $bb2 = Slic3r::Geometry::BoundingBox->merge(map $_->bounding_box, values %meshes); + $_->translate(@{$bb2->vector_to_origin}) for values %meshes; + + # prepare scaled object size + my $scaled_bb = $bb2->clone; + $scaled_bb->translate(@{$bb2->vector_to_origin}); # not needed for getting size, but who knows + $scaled_bb->scale(1 / &Slic3r::SCALING_FACTOR); + + # prepare copies + my @copies = (); + foreach my $instance (@{ $object->instances }) { + push @copies, Slic3r::Point->new( + scale($instance->offset->[X] - $bb1->extents->[X][MIN]), + scale($instance->offset->[Y] - $bb1->extents->[Y][MIN]), ); } + # initialize print object + push @{$self->objects}, Slic3r::Print::Object->new( + print => $self, + meshes => [ map $meshes{$_}, 0..$#{$self->regions} ], + copies => [ @copies ], + size => $scaled_bb->size, # transformed size + input_file => $object->input_file, + config_overrides => $object->config, + layer_height_ranges => $object->layer_height_ranges, + ); + if (!defined $self->extra_variables->{input_filename}) { - if (defined (my $input_file = $self->objects->[0]->input_file)) { + if (defined (my $input_file = $object->input_file)) { @{$self->extra_variables}{qw(input_filename input_filename_base)} = parse_filename($input_file); } } diff --git a/lib/Slic3r/Print/Object.pm b/lib/Slic3r/Print/Object.pm index 4d5a5e600..0942e5bfc 100644 --- a/lib/Slic3r/Print/Object.pm +++ b/lib/Slic3r/Print/Object.pm @@ -118,7 +118,6 @@ sub slice { # process facets for my $region_id (0 .. $#{$self->meshes}) { my $mesh = $self->meshes->[$region_id] // next; # ignore undef meshes - $mesh->repair; { my $loops = $mesh->slice([ map $_->slice_z, @{$self->layers} ]); diff --git a/lib/Slic3r/Test.pm b/lib/Slic3r/Test.pm index 6f2b5d403..c857f3496 100644 --- a/lib/Slic3r/Test.pm +++ b/lib/Slic3r/Test.pm @@ -108,7 +108,7 @@ sub init_print { $model_name = [$model_name] if ref($model_name) ne 'ARRAY'; for my $model (map model($_, %params), @$model_name) { $model->arrange_objects($config); - $print->add_model($model); + $print->add_model_object($_) for @{$model->objects}; } $print->validate; diff --git a/slic3r.pl b/slic3r.pl index d573a4735..4b90d725f 100755 --- a/slic3r.pl +++ b/slic3r.pl @@ -12,6 +12,7 @@ use Getopt::Long qw(:config no_auto_abbrev); use List::Util qw(first); use POSIX qw(setlocale LC_NUMERIC); use Slic3r; +use Slic3r::Geometry qw(X Y); $|++; our %opt = (); @@ -119,9 +120,30 @@ if (@ARGV) { # slicing from command line } else { $model = Slic3r::Model->read_from_file($input_file); } - $_->scale($config->scale) for @{$model->objects}; - $_->rotate($config->rotate) for @{$model->objects}; - $model->arrange_objects($config); + + my $need_arrange = $model->has_objects_with_no_instances; + 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}) { $model->print_info; @@ -129,7 +151,7 @@ if (@ARGV) { # slicing from command line } my $print = Slic3r::Print->new(config => $config); - $print->add_model($model); + $print->add_model_object($_) for @{$model->objects}; $print->validate; my %params = ( output_file => $opt{output}, diff --git a/xs/src/ExPolygonCollection.cpp b/xs/src/ExPolygonCollection.cpp index c6a22e56f..8d339d952 100644 --- a/xs/src/ExPolygonCollection.cpp +++ b/xs/src/ExPolygonCollection.cpp @@ -1,4 +1,5 @@ #include "ExPolygonCollection.hpp" +#include "Geometry.hpp" namespace Slic3r { @@ -57,4 +58,13 @@ ExPolygonCollection::simplify(double tolerance) 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); +} + } diff --git a/xs/src/ExPolygonCollection.hpp b/xs/src/ExPolygonCollection.hpp index 5a5c4d8d4..09f56a162 100644 --- a/xs/src/ExPolygonCollection.hpp +++ b/xs/src/ExPolygonCollection.hpp @@ -16,6 +16,7 @@ class ExPolygonCollection void rotate(double angle, Point* center); bool contains_point(const Point* point) const; void simplify(double tolerance); + void convex_hull(Polygon* hull) const; }; } diff --git a/xs/src/Geometry.cpp b/xs/src/Geometry.cpp index 26ceeabd6..be7848a05 100644 --- a/xs/src/Geometry.cpp +++ b/xs/src/Geometry.cpp @@ -2,6 +2,7 @@ #include "clipper.hpp" #include #include +#include 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); } -/* This implementation is based on Steffen Mueller's work for - the Perl module Math::ConvexHull::MonotoneChain (available - on CPAN under the GPL terms) */ +/* This implementation is based on Andrew's monotone chain 2D convex hull algorithm */ void -convex_hull(Points points, Polygon &hull) +convex_hull(Points &points, Polygon* hull) { assert(points.size() >= 3); // sort input points std::sort(points.begin(), points.end(), sort_points); - typedef const Point* PointPtr; - PointPtr* out_hull = (PointPtr*)malloc(points.size()*2*sizeof(PointPtr)); - - /* lower hull */ - size_t k = 0; - for (Points::const_iterator it = points.begin(); it != points.end(); ++it) { - while (k >= 2 && it->ccw(out_hull[k-2], out_hull[k-1]) <= 0) --k; - Point pz = *&*it; - out_hull[k++] = &*it; + int n = points.size(), k = 0; + hull->points.resize(2*n); + + // Build lower hull + for (int i = 0; i < n; i++) { + while (k >= 2 && points[i].ccw(hull->points[k-2], hull->points[k-1]) <= 0) k--; + hull->points[k++] = points[i]; } - - /* upper hull */ - size_t t = k+1; - for (Points::const_iterator it = points.end() - 2; it != points.begin(); --it) { - while (k >= t && it->ccw(out_hull[k-2], out_hull[k-1]) <= 0) --k; - out_hull[k++] = &*it; + + // Build upper hull + for (int i = n-2, t = k+1; i >= 0; i--) { + while (k >= t && points[i].ccw(hull->points[k-2], hull->points[k-1]) <= 0) k--; + hull->points[k++] = points[i]; } + + hull->points.resize(k); - // we assume hull is empty - hull.points.reserve(k); - 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); + assert( hull->points.front().coincides_with(hull->points.back()) ); + hull->points.pop_back(); } /* accepts an arrayref of points and returns a list of indices diff --git a/xs/src/Geometry.hpp b/xs/src/Geometry.hpp index d998b7aba..ea6cb3242 100644 --- a/xs/src/Geometry.hpp +++ b/xs/src/Geometry.hpp @@ -5,7 +5,7 @@ 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 &retval, Point start_near); void chained_path(Points &points, std::vector &retval); template void chained_path_items(Points &points, T &items, T &retval); diff --git a/xs/src/Point.cpp b/xs/src/Point.cpp index 13e7d2361..db9c1417b 100644 --- a/xs/src/Point.cpp +++ b/xs/src/Point.cpp @@ -109,6 +109,9 @@ Point::distance_to(const Line &line) const /* 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 * 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 Point::ccw(const Point &p1, const Point &p2) const diff --git a/xs/src/TriangleMesh.cpp b/xs/src/TriangleMesh.cpp index b59d67ca9..63d33b7c4 100644 --- a/xs/src/TriangleMesh.cpp +++ b/xs/src/TriangleMesh.cpp @@ -191,6 +191,8 @@ TriangleMesh::slice(const std::vector &z, std::vector &layers) FUTURE: parallelize slice_facet() and make_loops() */ + if (!this->repaired) this->repair(); + // build a table to map a facet_idx to its three edge indices if (this->stl.v_shared == NULL) stl_generate_shared_vertices(&(this->stl)); typedef std::pair t_edge; @@ -603,7 +605,7 @@ TriangleMesh::horizontal_projection(ExPolygons &retval) const } void -TriangleMesh::convex_hull(Polygon &hull) +TriangleMesh::convex_hull(Polygon* hull) { if (this->stl.v_shared == NULL) stl_generate_shared_vertices(&(this->stl)); Points pp; diff --git a/xs/src/TriangleMesh.hpp b/xs/src/TriangleMesh.hpp index 06ecca273..0056d02a7 100644 --- a/xs/src/TriangleMesh.hpp +++ b/xs/src/TriangleMesh.hpp @@ -33,7 +33,7 @@ class TriangleMesh TriangleMeshPtrs split() const; void merge(const TriangleMesh* mesh); void horizontal_projection(ExPolygons &retval) const; - void convex_hull(Polygon &hull); + void convex_hull(Polygon* hull); stl_file stl; bool repaired; diff --git a/xs/src/myinit.h b/xs/src/myinit.h index d1659823e..1ef522b44 100644 --- a/xs/src/myinit.h +++ b/xs/src/myinit.h @@ -20,6 +20,7 @@ extern "C" { #define EPSILON 1e-4 #define SCALING_FACTOR 0.000001 +#define scale_(val) (val / SCALING_FACTOR) #define unscale(val) (val * SCALING_FACTOR) namespace Slic3r {} diff --git a/xs/xsp/ExPolygonCollection.xsp b/xs/xsp/ExPolygonCollection.xsp index ec404528b..793c30b9a 100644 --- a/xs/xsp/ExPolygonCollection.xsp +++ b/xs/xsp/ExPolygonCollection.xsp @@ -68,5 +68,15 @@ ExPolygonCollection::append(...) 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 + %} }; diff --git a/xs/xsp/Geometry.xsp b/xs/xsp/Geometry.xsp index e800e4eca..e771a937b 100644 --- a/xs/xsp/Geometry.xsp +++ b/xs/xsp/Geometry.xsp @@ -17,7 +17,7 @@ convex_hull(points) const char* CLASS = "Slic3r::Polygon"; CODE: RETVAL = new Polygon (); - Slic3r::Geometry::convex_hull(points, *RETVAL); + Slic3r::Geometry::convex_hull(points, RETVAL); OUTPUT: RETVAL diff --git a/xs/xsp/TriangleMesh.xsp b/xs/xsp/TriangleMesh.xsp index db98a633e..80160926d 100644 --- a/xs/xsp/TriangleMesh.xsp +++ b/xs/xsp/TriangleMesh.xsp @@ -167,7 +167,7 @@ TriangleMesh::convex_hull() const char* CLASS = "Slic3r::Polygon"; CODE: RETVAL = new Polygon (); - THIS->convex_hull(*RETVAL); + THIS->convex_hull(RETVAL); OUTPUT: RETVAL