diff --git a/lib/Slic3r/Config.pm b/lib/Slic3r/Config.pm index 97992d5b..71363c72 100644 --- a/lib/Slic3r/Config.pm +++ b/lib/Slic3r/Config.pm @@ -1131,6 +1131,15 @@ sub replace_options { return $string; } +# min object distance is max(duplicate_distance, clearance_radius) +sub min_object_distance { + my $self = shift; + + return ($self->complete_objects && $self->extruder_clearance_radius > $self->duplicate_distance) + ? $self->extruder_clearance_radius + : $self->duplicate_distance; +} + # CLASS METHODS: sub write_ini { diff --git a/lib/Slic3r/GUI/Plater.pm b/lib/Slic3r/GUI/Plater.pm index a1b8c340..712b736b 100644 --- a/lib/Slic3r/GUI/Plater.pm +++ b/lib/Slic3r/GUI/Plater.pm @@ -4,7 +4,7 @@ use warnings; use utf8; use File::Basename qw(basename dirname); -use Math::ConvexHull qw(convex_hull); +use List::Util qw(max sum); use Slic3r::Geometry qw(X Y Z X1 Y1 X2 Y2 scale unscale); use Slic3r::Geometry::Clipper qw(JT_ROUND); use threads::shared qw(shared_clone); @@ -155,8 +155,9 @@ sub new { EVT_COMMAND($self, -1, $THUMBNAIL_DONE_EVENT, sub { my ($self, $event) = @_; my ($obj_idx, $thumbnail) = @{$event->GetData}; - $self->{thumbnails}[$obj_idx] = $thumbnail; - $self->make_thumbnail2; + $self->{objects}[$obj_idx]->thumbnail($thumbnail); + $self->mesh(undef); + $self->on_thumbnail_made; }); EVT_COMMAND($self, -1, $PROGRESS_BAR_EVENT, sub { @@ -182,10 +183,7 @@ sub new { }); $self->_update_bed_size; - $self->{print} = Slic3r::Print->new; - $self->{thumbnails} = []; # polygons, each one aligned to 0,0 - $self->{scale} = []; - $self->{object_previews} = []; # [ obj_idx, copy_idx, positioned polygon ] + $self->{objects} = []; $self->{selected_objects} = []; $self->recenter; @@ -295,12 +293,26 @@ sub load_file { my $process_dialog = Wx::ProgressDialog->new('Loading…', "Processing input file…", 100, $self, 0); $process_dialog->Pulse; - local $SIG{__WARN__} = Slic3r::GUI::warning_catcher($self); - $self->{print}->add_objects_from_file($input_file); - my $obj_idx = $#{$self->{print}->objects}; - $process_dialog->Destroy; - $self->object_loaded($obj_idx); + 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 => basebane($input_file), + input_file => $input_file, + input_file_object_id => $i, + mesh => $model->objects->[$i]->mesh, + instances => [ + $model->objects->[$i]->instances + ? (map $_->offset, @{$model->objects->[$i]->instances}) + : [0,0], + ], + ); + push @{ $self->{objects} }, $object; + $self->object_loaded($#{ $self->{objects} }); + } + + $process_dialog->Destroy; $self->statusbar->SetStatusText("Loaded $input_file"); } @@ -308,11 +320,10 @@ sub object_loaded { my $self = shift; my ($obj_idx, %params) = @_; - my $object = $self->{print}->objects->[$obj_idx]; - $self->{list}->InsertStringItem($obj_idx, basename($object->input_file)); - $self->{list}->SetItem($obj_idx, 1, "1"); - $self->{list}->SetItem($obj_idx, 2, "100%"); - push @{$self->{scale}}, 1; + my $object = $self->{objects}[$obj_idx]; + $self->{list}->InsertStringItem($obj_idx, $object->name); + $self->{list}->SetItem($obj_idx, 1, $object->instances_count); + $self->{list}->SetItem($obj_idx, 2, ($object->scale * 100) . "%"); $self->make_thumbnail($obj_idx); $self->arrange unless $params{no_arrange}; @@ -325,36 +336,13 @@ sub remove { my $self = shift; my ($obj_idx) = @_; - if (defined $obj_idx) { - $self->{print}->copies->[$obj_idx][$_] = undef - for 0 .. $#{ $self->{print}->copies->[$obj_idx] }; - } else { - foreach my $pobj (@{$self->{selected_objects}}) { - my ($obj_idx, $copy_idx) = ($pobj->[0], $pobj->[1]); - $self->{print}->copies->[$obj_idx][$copy_idx] = undef; - } + # if no object index is supplied, remove the selected one + if (!defined $obj_idx) { + ($obj_idx, undef) = $self->selected_object; } - my @objects_to_remove = (); - for my $obj_idx (0 .. $#{$self->{print}->objects}) { - my $copies = $self->{print}->copies->[$obj_idx]; - - # filter out removed copies - @$copies = grep defined $_, @$copies; - - # update copies count in list - $self->{list}->SetItem($obj_idx, 1, scalar @$copies); - - # if no copies are left, remove the object itself - push @objects_to_remove, $obj_idx if !@$copies; - } - for my $obj_idx (sort { $b <=> $a } @objects_to_remove) { - splice @{$self->{print}->objects}, $obj_idx, 1; - splice @{$self->{print}->copies}, $obj_idx, 1; - splice @{$self->{thumbnails}}, $obj_idx, 1; - splice @{$self->{scale}}, $obj_idx, 1; - $self->{list}->DeleteItem($obj_idx); - } + splice @{$self->{objects}}, $obj_idx, 1; + $self->{list}->DeleteItem($obj_idx); $self->{selected_objects} = []; $self->selection_changed(0); @@ -366,10 +354,7 @@ sub remove { sub reset { my $self = shift; - @{$self->{print}->objects} = (); - @{$self->{print}->copies} = (); - @{$self->{thumbnails}} = (); - @{$self->{scale}} = (); + @{$self->{objects}} = (); $self->{list}->DeleteAllItems; $self->{selected_objects} = []; @@ -381,21 +366,21 @@ sub reset { sub increase { my $self = shift; - my $obj_idx = $self->selected_object_idx; - my $copies = $self->{print}->copies->[$obj_idx]; - push @$copies, [ $copies->[-1]->[X] + scale 10, $copies->[-1]->[Y] + scale 10 ]; - $self->{list}->SetItem($obj_idx, 1, scalar @$copies); + my ($obj_idx, $object) = $self->selected_object; + my $instances = $object->instances; + push @$instances, [ $instances->[-1]->[X] + scale 10, $instances->[-1]->[Y] + scale 10 ]; + $self->{list}->SetItem($obj_idx, 1, $object->instances_count); $self->arrange; } sub decrease { my $self = shift; - my $obj_idx = $self->selected_object_idx; + my ($obj_idx, $object) = $self->selected_object; $self->{selected_objects} = [ +(grep { $_->[0] == $obj_idx } @{$self->{object_previews}})[-1] ]; $self->remove; - if ($self->{print}->objects->[$obj_idx]) { + if ($self->{objects}[$obj_idx]) { $self->{list}->Select($obj_idx, 0); $self->{list}->Select($obj_idx, 1); } @@ -405,37 +390,14 @@ sub rotate { my $self = shift; my ($angle) = @_; - my $obj_idx = $self->selected_object_idx; - my $object = $self->{print}->objects->[$obj_idx]; + my ($obj_idx, $object) = $self->selected_object; if (!defined $angle) { - $angle = Wx::GetNumberFromUser("", "Enter the rotation angle:", "Rotate", 0, -364, 364, $self); + $angle = Wx::GetNumberFromUser("", "Enter the rotation angle:", "Rotate", $object->rotate, -364, 364, $self); return if !$angle || $angle == -1; } - $self->statusbar->SetStatusText("Rotating object…"); - $self->statusbar->StartBusy; - - # rotate, realign to 0,0 and update size - $object->mesh->rotate($angle); - $object->mesh->align_to_origin; - $object->size([ $object->mesh->size ]); - - $self->make_thumbnail($obj_idx); - $self->recenter; - $self->{canvas}->Refresh; - $self->statusbar->StopBusy; - $self->statusbar->SetStatusText(""); -} - -sub arrange { - my $self = shift; - - eval { - $self->{print}->arrange_objects; - }; - # ignore arrange warnings on purpose - + $object->set_rotation($angle); $self->recenter; $self->{canvas}->Refresh; } @@ -443,36 +405,41 @@ sub arrange { sub changescale { my $self = shift; - my $obj_idx = $self->selected_object_idx; - my $scale = $self->{scale}[$obj_idx]; + my ($obj_idx, $object) = $self->selected_object; + # max scale factor should be above 2540 to allow importing files exported in inches - $scale = Wx::GetNumberFromUser("", "Enter the scale % for the selected object:", "Scale", $scale*100, 0, 5000, $self); + $scale = Wx::GetNumberFromUser("", "Enter the scale % for the selected object:", "Scale", $object->scale*100, 0, 5000, $self); return if !$scale || $scale == -1; - $self->statusbar->SetStatusText("Scaling object…"); - $self->statusbar->StartBusy; - - my $object = $self->{print}->objects->[$obj_idx]; - my $mesh = $object->mesh; - $mesh->scale($scale/100 / $self->{scale}[$obj_idx]); - $object->mesh->align_to_origin; - $object->size([ $object->mesh->size ]); - - $self->{scale}[$obj_idx] = $scale/100; - $self->{list}->SetItem($obj_idx, 2, "$scale%"); - - $self->make_thumbnail($obj_idx); + $object->set_scale($scale); $self->arrange; - $self->statusbar->StopBusy; - $self->statusbar->SetStatusText(""); +} + +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 $_->thumbnail->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); + }; + # ignore arrange warnings on purpose + + $self->recenter; + $self->{canvas}->Refresh; } sub split_object { my $self = shift; - my $obj_idx = $self->selected_object_idx; - my $current_object = $self->{print}->objects->[$obj_idx]; - my $current_copies_num = @{$self->{print}->copies->[$obj_idx]}; + my ($obj_idx, $current_object) = $self->selected_object; + my $current_copies_num = $current_object->instances_count; my $mesh = $current_object->mesh->clone; $mesh->scale(&Slic3r::SCALING_FACTOR); @@ -670,8 +637,10 @@ sub make_model { my $self = shift; my $model = Slic3r::Model->new; - for my $obj_idx (0 .. $#{$self->{print}->objects}) { - my $mesh = $self->{print}->objects->[$obj_idx]->mesh->clone; + for my $obj_idx (0 .. $#{$self->{objects}}) { + my $object = $self->{objects}[$obj_idx]; + # TODO: reload file + my $mesh = $self->{print}->[$obj_idx]->mesh->clone; $mesh->scale(&Slic3r::SCALING_FACTOR); my $object = $model->add_object(vertices => $mesh->vertices); $object->add_volume(facets => $mesh->facets); @@ -689,27 +658,21 @@ sub make_thumbnail { my ($obj_idx) = @_; my $cb = sub { - my $object = $self->{print}->objects->[$obj_idx]; - my @points = map [ @$_[X,Y] ], @{$object->mesh->vertices}; - my $convex_hull = Slic3r::Polygon->new(convex_hull(\@points)); - for (@$convex_hull) { - @$_ = map $self->to_pixel($_), @$_; - } - $convex_hull->simplify(0.3); - $self->{thumbnails}->[$obj_idx] = $convex_hull; # ignored in multithread environment + my $object = $self->{objects}[$obj_idx]; + my $thumbnail = $object->make_thumbnail; if ($Slic3r::have_threads) { - Wx::PostEvent($self, Wx::PlThreadEvent->new(-1, $THUMBNAIL_DONE_EVENT, shared_clone([ $obj_idx, $convex_hull ]))); + Wx::PostEvent($self, Wx::PlThreadEvent->new(-1, $THUMBNAIL_DONE_EVENT, shared_clone([ $obj_idx, $thumbnail ]))); threads->exit; } else { - $self->make_thumbnail2; + $self->on_thumbnail_made; } }; $Slic3r::have_threads ? threads->create($cb)->detach : $cb->(); } -sub make_thumbnail2 { +sub on_thumbnail_made { my $self = shift; $self->recenter; $self->{canvas}->Refresh; @@ -719,8 +682,8 @@ sub recenter { my $self = shift; # calculate displacement needed to center the print - my @print_bb = $self->{print}->bounding_box; - @print_bb = (0,0,0,0) if !defined $print_bb[0]; + my @print_bb = (0,0,0,0); + @print_bb = if !defined $print_bb[0]; $self->{shift} = [ ($self->{canvas}->GetSize->GetWidth - ($self->to_pixel($print_bb[X2] + $print_bb[X1]))) / 2, ($self->{canvas}->GetSize->GetHeight - ($self->to_pixel($print_bb[Y2] + $print_bb[Y1]))) / 2, @@ -758,9 +721,6 @@ sub _update_bed_size { my $bed_largest_side = $bed_size->[X] > $bed_size->[Y] ? $bed_size->[X] : $bed_size->[Y]; my $old_scaling_factor = $self->{scaling_factor}; $self->{scaling_factor} = $canvas_side / $bed_largest_side; - if (defined $old_scaling_factor && $self->{scaling_factor} != $old_scaling_factor) { - $self->make_thumbnail($_) for 0..$#{$self->{thumbnails}}; - } } # this is called on the canvas @@ -936,9 +896,10 @@ sub selection_changed { } } -sub selected_object_idx { +sub selected_object { my $self = shift; - return $self->{selected_objects}[0] ? $self->{selected_objects}[0][0] : $self->{list}->GetFirstSelected; + my $obj_idx = $self->{selected_objects}[0] ? $self->{selected_objects}[0][0] : $self->{list}->GetFirstSelected; + return ($obj_idx, $self->{objects}[$obj_idx]), } sub statusbar { @@ -989,4 +950,60 @@ sub OnDropFiles { $self->{window}->load_file($_) for @$filenames; } +package Slic3r::GUI::Plater::Object; +use Moo; + +use Math::ConvexHull qw(convex_hull); + +has 'name' => (is => 'rw', required => 1); +has 'input_file' => (is => 'rw', required => 1); +has 'input_file_object_id' => (is => 'rw', required => 1); +has 'mesh' => (is => 'rw', required => 1, trigger => 1); +has 'size' => (is => 'rw'); +has 'scale' => (is => 'rw', default => sub { 1 }); +has 'rotate' => (is => 'rw', default => sub { 0 }); +has 'instances' => (is => 'rw', default => sub { [] }); +has 'thumbnail' => (is => 'rw'); + +sub _trigger_mesh { + my $self = shift; + $self->size($mesh->size) if $self->mesh; +} + +sub instances_count { + my $self = shift; + return scalar @{$self->instances}; +} + +sub make_thumbnail { + my $self = shift; + + my @points = map [ @$_[X,Y] ], @{$object->mesh->vertices}; + my $convex_hull = Slic3r::Polygon->new(convex_hull(\@points)); + for (@$convex_hull) { + @$_ = map $self->to_pixel($_), @$_; + } + $convex_hull->simplify(0.3); + + $self->thumbnail($convex_hull); # ignored in multi-threaded environments + $self->mesh(undef); + return $convex_hull; +} + +sub set_rotation { + my $self = shift; + my ($angle) = @_; + + $self->thumbnail->rotate($angle - $self->rotate); + $self->rotate($angle); +} + +sub set_scale { + my $self = shift; + my ($scale) = @_; + + $self->thumbnail->scale($scale - $self->scale); + $self->scale($scale); +} + 1; diff --git a/lib/Slic3r/Model.pm b/lib/Slic3r/Model.pm index b648d4b3..3a4aec49 100644 --- a/lib/Slic3r/Model.pm +++ b/lib/Slic3r/Model.pm @@ -6,6 +6,18 @@ use Slic3r::Geometry qw(X Y Z); has 'materials' => (is => 'ro', default => sub { {} }); has 'objects' => (is => 'ro', default => sub { [] }); +sub read_from_file { + my $class = shift; + my ($input_file) = @_; + + my $model = $input_file =~ /\.stl$/i ? Slic3r::Format::STL->read_file($input_file) + : $input_file =~ /\.obj$/i ? Slic3r::Format::OBJ->read_file($input_file) + : $input_file =~ /\.amf(\.xml)?$/i ? Slic3r::Format::AMF->read_file($input_file) + : die "Input file must have .stl, .obj or .amf(.xml) extension\n"; + + return $model; +} + sub add_object { my $self = shift; @@ -21,34 +33,15 @@ sub mesh { my $vertices = []; my $facets = []; foreach my $object (@{$self->objects}) { - my @instances = $object->instances ? @{$object->instances} : (undef); - foreach my $instance (@instances) { - my @vertices = @{$object->vertices}; - if ($instance) { - # save Z coordinates, as rotation and translation discard them - my @z = map $_->[Z], @vertices; - - if ($instance->rotation) { - # transform vertex coordinates - my $rad = Slic3r::Geometry::deg2rad($instance->rotation); - @vertices = Slic3r::Geometry::rotate_points($rad, undef, @vertices); - } - @vertices = Slic3r::Geometry::move_points($instance->offset, @vertices); - - # reapply Z coordinates - $vertices[$_][Z] = $z[$_] for 0 .. $#z; - } - - my $v_offset = @$vertices; - push @$vertices, @vertices; - foreach my $volume (@{$object->volumes}) { - push @$facets, map { - my $f = [@$_]; - $f->[$_] += $v_offset for -3..-1; - $f; - } @{$volume->facets}; - } - } + my $mesh = $object->mesh; + + my $v_offset = @$vertices; + push @$vertices, @{$mesh->vertices}; + push @$facets, map { + my $f = [@$_]; + $f->[$_] += $v_offset for -3..-1; + $f; + } @{$mesh->facets}; } return Slic3r::TriangleMesh->new( @@ -87,6 +80,47 @@ sub add_instance { return $self->instances->[-1]; } +sub mesh { + my $self = shift; + + my $vertices = []; + my $facets = []; + + my @instances = $self->instances ? @{$self->instances} : (undef); + foreach my $instance (@instances) { + my @vertices = @{$self->vertices}; + if ($instance) { + # save Z coordinates, as rotation and translation discard them + my @z = map $_->[Z], @vertices; + + if ($instance->rotation) { + # transform vertex coordinates + my $rad = Slic3r::Geometry::deg2rad($instance->rotation); + @vertices = Slic3r::Geometry::rotate_points($rad, undef, @vertices); + } + @vertices = Slic3r::Geometry::move_points($instance->offset, @vertices); + + # reapply Z coordinates + $vertices[$_][Z] = $z[$_] for 0 .. $#z; + } + + my $v_offset = @$vertices; + push @$vertices, @vertices; + foreach my $volume (@{$self->volumes}) { + push @$facets, map { + my $f = [@$_]; + $f->[$_] += $v_offset for -3..-1; + $f; + } @{$volume->facets}; + } + } + + return Slic3r::TriangleMesh->new( + vertices => $vertices, + facets => $facets, + ); +} + package Slic3r::Model::Volume; use Moo; diff --git a/lib/Slic3r/Polyline.pm b/lib/Slic3r/Polyline.pm index da9cc047..ce13e778 100644 --- a/lib/Slic3r/Polyline.pm +++ b/lib/Slic3r/Polyline.pm @@ -4,7 +4,7 @@ use warnings; use Math::Clipper qw(); use Scalar::Util qw(reftype); -use Slic3r::Geometry qw(A B polyline_remove_parallel_continuous_edges polyline_remove_acute_vertices +use Slic3r::Geometry qw(A B X Y MIN MAX polyline_remove_parallel_continuous_edges polyline_remove_acute_vertices polyline_lines move_points same_point); # the constructor accepts an array(ref) of points @@ -140,6 +140,13 @@ sub bounding_box { return Slic3r::Geometry::bounding_box($self); } +sub size { + my $self = shift; + + my @extents = $self->bounding_box; + return map $extents[$_][MAX] - $extents[$_][MIN], (X,Y); +} + sub rotate { my $self = shift; my ($angle, $center) = @_; diff --git a/lib/Slic3r/Print.pm b/lib/Slic3r/Print.pm index bfa7c2c3..735a3168 100644 --- a/lib/Slic3r/Print.pm +++ b/lib/Slic3r/Print.pm @@ -3,6 +3,7 @@ use Moo; use File::Basename qw(basename fileparse); use File::Spec; +use List::Util qw(max); use Math::ConvexHull 1.0.4 qw(convex_hull); use Slic3r::ExtrusionPath ':roles'; use Slic3r::Geometry qw(X Y Z X1 Y1 X2 Y2 PI scale unscale move_points); @@ -83,10 +84,7 @@ sub add_objects_from_file { my $self = shift; my ($input_file) = @_; - my $model = $input_file =~ /\.stl$/i ? Slic3r::Format::STL->read_file($input_file) - : $input_file =~ /\.obj$/i ? Slic3r::Format::OBJ->read_file($input_file) - : $input_file =~ /\.amf(\.xml)?$/i ? Slic3r::Format::AMF->read_file($input_file) - : die "Input file must have .stl, .obj or .amf(.xml) extension\n"; + my $model = Slic3r::Model->read_from_file($input_file); my @print_objects = $self->add_model($model); $_->input_file($input_file) for @print_objects; @@ -231,19 +229,11 @@ sub arrange_objects { my $self = shift; my $total_parts = scalar map @$_, @{$self->copies}; - my $partx = my $party = 0; - foreach my $object (@{$self->objects}) { - $partx = $object->size->[X] if $object->size->[X] > $partx; - $party = $object->size->[Y] if $object->size->[Y] > $party; - } - - # object distance is max(duplicate_distance, clearance_radius) - my $distance = $Slic3r::Config->complete_objects && $Slic3r::Config->extruder_clearance_radius > $Slic3r::Config->duplicate_distance - ? $Slic3r::Config->extruder_clearance_radius - : $Slic3r::Config->duplicate_distance; + my $partx = max(map $_->size->[X], @{$self->objects}); + my $party = max(map $_->size->[Y], @{$self->objects}); my @positions = Slic3r::Geometry::arrange - ($total_parts, $partx, $party, (map scale $_, @{$Slic3r::Config->bed_size}), scale $distance); + ($total_parts, $partx, $party, (map scale $_, @{$Slic3r::Config->bed_size}), scale $Slic3r::Config->min_object_distance); for my $obj_idx (0..$#{$self->objects}) { @{$self->copies->[$obj_idx]} = splice @positions, 0, scalar @{$self->copies->[$obj_idx]};