From 415d1a51700ff2982d08515e48bfce09d79ae75a Mon Sep 17 00:00:00 2001 From: Alessandro Ranellucci Date: Mon, 26 Sep 2011 10:52:58 +0200 Subject: [PATCH] Optimization of travel paths for fills --- README.markdown | 1 - lib/Slic3r.pm | 4 +- lib/Slic3r/ExtrusionPath.pm | 12 ++ lib/Slic3r/ExtrusionPath/Collection.pm | 55 ++++++ lib/Slic3r/Fill/Rectilinear.pm | 257 +++++++++++++------------ lib/Slic3r/Geometry.pm | 1 + lib/Slic3r/Perimeter.pm | 19 +- lib/Slic3r/Print.pm | 7 +- lib/Slic3r/Surface/Collection.pm | 10 + 9 files changed, 230 insertions(+), 136 deletions(-) create mode 100644 lib/Slic3r/ExtrusionPath/Collection.pm create mode 100644 lib/Slic3r/Surface/Collection.pm diff --git a/README.markdown b/README.markdown index ad65a7561..3058fb8bf 100644 --- a/README.markdown +++ b/README.markdown @@ -42,7 +42,6 @@ Roadmap includes the following goals: * output some statistics; * allow the user to customize initial and final GCODE commands; * support material for internal perimeters; -* travel path optimization; * ability to infill in the direction of bridges; * input object transform (scale, rotate, multiply); * cool; diff --git a/lib/Slic3r.pm b/lib/Slic3r.pm index d9db1381b..f6fff16d1 100644 --- a/lib/Slic3r.pm +++ b/lib/Slic3r.pm @@ -10,6 +10,7 @@ sub debugf { use Slic3r::ExtrusionLoop; use Slic3r::ExtrusionPath; +use Slic3r::ExtrusionPath::Collection; use Slic3r::Fill; use Slic3r::Geometry; use Slic3r::Layer; @@ -21,6 +22,7 @@ use Slic3r::Polyline::Closed; use Slic3r::Print; use Slic3r::STL; use Slic3r::Surface; +use Slic3r::Surface::Collection; # printer options our $nozzle_diameter = 0.45; @@ -33,7 +35,7 @@ our $filament_packing_density = 0.85; # speed options our $print_feed_rate = 60; # mm/sec -our $travel_feed_rate = 80; # mm/sec +our $travel_feed_rate = 130; # mm/sec our $bottom_layer_speed_ratio = 0.6; # accuracy options diff --git a/lib/Slic3r/ExtrusionPath.pm b/lib/Slic3r/ExtrusionPath.pm index 8df519789..580ed3bd5 100644 --- a/lib/Slic3r/ExtrusionPath.pm +++ b/lib/Slic3r/ExtrusionPath.pm @@ -22,4 +22,16 @@ sub clip_end { } } +sub endpoints { + my $self = shift; + my ($as_arrayref) = @_; + my @points = ($self->points->[0], $self->points->[-1]); + return $as_arrayref ? map($_->p, @points) : @points; +} + +sub reverse { + my $self = shift; + @{$self->points} = reverse @{$self->points}; +} + 1; diff --git a/lib/Slic3r/ExtrusionPath/Collection.pm b/lib/Slic3r/ExtrusionPath/Collection.pm new file mode 100644 index 000000000..dda6e1d37 --- /dev/null +++ b/lib/Slic3r/ExtrusionPath/Collection.pm @@ -0,0 +1,55 @@ +package Slic3r::ExtrusionPath::Collection; +use Moo; + +use XXX; + +has 'paths' => ( + is => 'rw', + #isa => 'ArrayRef[Slic3r::ExtrusionPath]', + default => sub { [] }, +); + +sub add { + my $self = shift; + my ($path) = @_; + + push @{$self->paths}, $path; +} + +sub endpoints { + my $self = shift; + my ($as_arrayref) = @_; + return map $_->endpoints($as_arrayref), @{$self->paths}; +} + +sub shortest_path { + my $self = shift; + my ($start_near) = @_; + + # get point as arrayref + $start_near = $start_near->p if $start_near && ref $start_near ne 'ARRAY'; + + my @paths = (); + my $start_at; + CYCLE: while (@{$self->paths}) { + # find nearest point + $start_at = Slic3r::Point->cast(Slic3r::Geometry::nearest_point($start_near, [ $self->endpoints(1) ])); + + # loop through paths to find the one that starts or ends at the point found + PATH: for (my $i = 0; $i <= $#{$self->paths}; $i++) { + if ($start_at->id eq $self->paths->[$i]->points->[0]->id) { + push @paths, splice @{$self->paths}, $i, 1; + } elsif ($start_at->id eq $self->paths->[$i]->points->[-1]->id) { + $self->paths->[$i]->reverse; + push @paths, splice @{$self->paths}, $i, 1; + } else { + next PATH; + } + $start_near = $paths[-1]->points->[-1]->p; + next CYCLE; + } + } + return @paths; +} + +1; diff --git a/lib/Slic3r/Fill/Rectilinear.pm b/lib/Slic3r/Fill/Rectilinear.pm index 65013ec1e..ef419a0f8 100644 --- a/lib/Slic3r/Fill/Rectilinear.pm +++ b/lib/Slic3r/Fill/Rectilinear.pm @@ -21,145 +21,154 @@ sub make_fill { printf "Filling layer %d:\n", $layer->id; my $max_print_dimension = $print->max_length; + my $n = 1; - SURFACE: foreach my $surface (@{ $layer->fill_surfaces }) { - Slic3r::debugf " Processing surface %s:\n", $surface->id; - my $polygon = $surface->mgp_polygon; + foreach my $surface_collection (@{ $layer->fill_surfaces }) { + my @path_collection = (); - # alternate fill direction - my (@rotate, @shift); - if ($layer->id % 2) { - @rotate = ( PI/2, [ $print->x_length / 2, $print->y_length / 2 ] ); - $shift[X] = $print->y_length / 2 - $print->x_length / 2; - $shift[Y] = -$shift[X]; - } - - # TODO: here we should implement an "infill in direction of bridges" option - - # rotate surface as needed - $polygon = $polygon->rotate(@rotate)->move(@shift) if @rotate; - - # force 100% density for external surfaces - my $density = $surface->surface_type eq 'internal' ? $Slic3r::fill_density : 1; - next SURFACE unless $density > 0; - - my $distance_between_lines = $Slic3r::flow_width / $Slic3r::resolution / $density; - my $number_of_lines = ceil($max_print_dimension / $distance_between_lines); - - printf "distance = %f\n", $distance_between_lines; - printf "number_of_lines = %d\n", $number_of_lines; - - # this arrayref will hold intersection points of the fill grid with surface segments - my $points = [ map [], 0..$number_of_lines-1 ]; - foreach my $line (map $self->_lines_from_mgp_points($_), @{ $polygon->polygons }) { + SURFACE: foreach my $surface (@{ $surface_collection->surfaces }) { + Slic3r::debugf " Processing surface %s:\n", $surface->id; + my $polygon = $surface->mgp_polygon; - # find out the coordinates - my @coordinates = map @$_, @$line; + # alternate fill direction + my (@rotate, @shift); + if ($layer->id % 2) { + @rotate = ( PI/2, [ $print->x_length / 2, $print->y_length / 2 ] ); + $shift[X] = $print->y_length / 2 - $print->x_length / 2; + $shift[Y] = -$shift[X]; + } - # get the extents of the segment along the primary axis - my @line_c = sort { $a <=> $b } @coordinates[X1, X2]; - Slic3r::debugf "Segment %d,%d - %d,%d (extents: %f, %f)\n", @coordinates, @line_c; + # TODO: here we should implement an "infill in direction of bridges" option - for (my $c = int($line_c[0] / $distance_between_lines) * $distance_between_lines; - $c <= $line_c[1]; $c += $distance_between_lines) { - next if $c < $line_c[0] || $c > $line_c[1]; - my $i = sprintf('%.0f', $c / $distance_between_lines) - 1; - printf "CURRENT \$i = %d, \$c = %f\n", $i, $c; + # rotate surface as needed + $polygon = $polygon->rotate(@rotate)->move(@shift) if @rotate; + + # force 100% density for external surfaces + my $density = $surface->surface_type eq 'internal' ? $Slic3r::fill_density : 1; + next SURFACE unless $density > 0; + + my $distance_between_lines = $Slic3r::flow_width / $Slic3r::resolution / $density; + my $number_of_lines = ceil($max_print_dimension / $distance_between_lines); + + printf "distance = %f\n", $distance_between_lines; + printf "number_of_lines = %d\n", $number_of_lines; + + # this arrayref will hold intersection points of the fill grid with surface segments + my $points = [ map [], 0..$number_of_lines-1 ]; + foreach my $line (map $self->_lines_from_mgp_points($_), @{ $polygon->polygons }) { - # if the segment is parallel to our ray, there will be two intersection points - if ($line_c[0] == $line_c[1]) { - Slic3r::debugf " Segment is parallel!\n"; - push @{ $points->[$i] }, $coordinates[Y1], $coordinates[Y2]; - Slic3r::debugf " intersections at %f (%d) = %f, %f\n", $c, $i, $points->[$i][-2], $points->[$i][-1]; - } else { - Slic3r::debugf " Segment NOT parallel!\n"; - # one point of intersection - push @{ $points->[$i] }, $coordinates[Y1] + ($coordinates[Y2] - $coordinates[Y1]) - * ($c - $coordinates[X1]) / ($coordinates[X2] - $coordinates[X1]); - Slic3r::debugf " intersection at %f (%d) = %f\n", $c, $i, $points->[$i][-1]; + # find out the coordinates + my @coordinates = map @$_, @$line; + + # get the extents of the segment along the primary axis + my @line_c = sort { $a <=> $b } @coordinates[X1, X2]; + Slic3r::debugf "Segment %d,%d - %d,%d (extents: %f, %f)\n", @coordinates, @line_c; + + for (my $c = int($line_c[0] / $distance_between_lines) * $distance_between_lines; + $c <= $line_c[1]; $c += $distance_between_lines) { + next if $c < $line_c[0] || $c > $line_c[1]; + my $i = sprintf('%.0f', $c / $distance_between_lines) - 1; + printf "CURRENT \$i = %d, \$c = %f\n", $i, $c; + + # if the segment is parallel to our ray, there will be two intersection points + if ($line_c[0] == $line_c[1]) { + Slic3r::debugf " Segment is parallel!\n"; + push @{ $points->[$i] }, $coordinates[Y1], $coordinates[Y2]; + Slic3r::debugf " intersections at %f (%d) = %f, %f\n", $c, $i, $points->[$i][-2], $points->[$i][-1]; + } else { + Slic3r::debugf " Segment NOT parallel!\n"; + # one point of intersection + push @{ $points->[$i] }, $coordinates[Y1] + ($coordinates[Y2] - $coordinates[Y1]) + * ($c - $coordinates[X1]) / ($coordinates[X2] - $coordinates[X1]); + Slic3r::debugf " intersection at %f (%d) = %f\n", $c, $i, $points->[$i][-1]; + } } } - } - - # sort and remove duplicates - for (my $i = 0; $i <= $#$points; $i++) { - my %h = map { sprintf("%.9f", $_) => 1 } @{ $points->[$i] }; - $points->[$i] = [ sort { $a <=> $b } keys %h ]; - } - - # generate extrusion paths - my (@paths, @path_points) = (); - my $direction = 0; - - my $stop_path = sub { - # defensive programming - if (@path_points == 1) { - #warn "There shouldn't be only one point in the current path"; + + # sort and remove duplicates + for (my $i = 0; $i <= $#$points; $i++) { + my %h = map { sprintf("%.9f", $_) => 1 } @{ $points->[$i] }; + $points->[$i] = [ sort { $a <=> $b } keys %h ]; } - - # if we were constructing a path, stop it - push @paths, [ @path_points ] if @path_points > 1; - @path_points = (); - }; - - # loop until we have spare points - CYCLE: while (scalar map(@$_, @$points) > 1) { - # loop through rows - ROW: for (my $i = 0; $i <= $#$points; $i++) { - my $row = $points->[$i] or next ROW; - Slic3r::debugf "\nProcessing row %d (direction: %d)...\n", $i, $direction; - if (!@$row) { - Slic3r::debugf " no points\n"; - $stop_path->(); - next ROW; + + # generate extrusion paths + my (@paths, @path_points) = (); + my $direction = 0; + + my $stop_path = sub { + # defensive programming + if (@path_points == 1) { + #warn "There shouldn't be only one point in the current path"; } - Slic3r::debugf " points = %s\n", join ', ', @$row if $Slic3r::debug; - - # coordinate of current row - my $c = ($i + 1) * $distance_between_lines; - - # need to start a path? - if (!@path_points) { - Slic3r::debugf " path starts at %d\n", $row->[0]; - push @path_points, [ $c, shift @$row ]; + + # if we were constructing a path, stop it + push @paths, [ @path_points ] if @path_points > 1; + @path_points = (); + }; + + # loop until we have spare points + CYCLE: while (scalar map(@$_, @$points) > 1) { + # loop through rows + ROW: for (my $i = 0; $i <= $#$points; $i++) { + my $row = $points->[$i] or next ROW; + Slic3r::debugf "\nProcessing row %d (direction: %d)...\n", $i, $direction; + if (!@$row) { + Slic3r::debugf " no points\n"; + $stop_path->(); + next ROW; + } + Slic3r::debugf " points = %s\n", join ', ', @$row if $Slic3r::debug; + + # coordinate of current row + my $c = ($i + 1) * $distance_between_lines; + + # need to start a path? + if (!@path_points) { + Slic3r::debugf " path starts at %d\n", $row->[0]; + push @path_points, [ $c, shift @$row ]; + } + + my @search_points = @$row; + @search_points = reverse @search_points if $direction == 1; + my @connectable_points = $self->find_connectable_points($polygon, $path_points[-1], $c, [@search_points]); + Slic3r::debugf " ==> found %d connectable points = %s\n", scalar(@connectable_points), + join ', ', @connectable_points if $Slic3r::debug; + + if (!@connectable_points && @path_points && $path_points[-1][0] != $c) { + # no connectable in this row + $stop_path->(); + } + + if (@connectable_points == 1 && $path_points[0][0] != $c + && (($connectable_points[0] == $row->[-1] && $direction == 0) + || ($connectable_points[0] == $row->[0] && $direction == 1))) { + $i--; # keep searching on current row in the opposite direction + } + + foreach my $p (@connectable_points) { + push @path_points, [ $c, $p ]; + @$row = grep $_ != $p, @$row; # remove point from row + } + + # invert direction + $direction = $direction ? 0 : 1; } - - my @search_points = @$row; - @search_points = reverse @search_points if $direction == 1; - my @connectable_points = $self->find_connectable_points($polygon, $path_points[-1], $c, [@search_points]); - Slic3r::debugf " ==> found %d connectable points = %s\n", scalar(@connectable_points), - join ', ', @connectable_points if $Slic3r::debug; - - if (!@connectable_points && @path_points && $path_points[-1][0] != $c) { - # no connectable in this row - $stop_path->(); - } - - if (@connectable_points == 1 && $path_points[0][0] != $c - && (($connectable_points[0] == $row->[-1] && $direction == 0) - || ($connectable_points[0] == $row->[0] && $direction == 1))) { - $i--; # keep searching on current row in the opposite direction - } - - foreach my $p (@connectable_points) { - push @path_points, [ $c, $p ]; - @$row = grep $_ != $p, @$row; # remove point from row - } - - # invert direction - $direction = $direction ? 0 : 1; + $stop_path->() if @path_points; } - $stop_path->() if @path_points; - } - - # paths must be rotated back - if (@rotate) { - # TODO: this skips 2-points paths! we shouldn't create a mgp polygon - @paths = map $self->_mgp_from_points_ref($_)->move(map -$_, @shift)->rotate(-$rotate[0], $rotate[1])->points, @paths; + + # paths must be rotated back + if (@rotate) { + # TODO: this skips 2-points paths! we shouldn't create a mgp polygon + @paths = map $self->_mgp_from_points_ref($_)->move(map -$_, @shift)->rotate(-$rotate[0], $rotate[1])->points, @paths; + } + + push @path_collection, @paths; } # save into layer - FINISH: push @{ $layer->fills }, map Slic3r::ExtrusionPath->cast([ @$_ ]), @paths; + FINISH: push @{ $layer->fills }, Slic3r::ExtrusionPath::Collection->new( + paths => [ map Slic3r::ExtrusionPath->cast([ @$_ ]), @path_collection ], + ); } } diff --git a/lib/Slic3r/Geometry.pm b/lib/Slic3r/Geometry.pm index f81e67122..b8a9d4091 100644 --- a/lib/Slic3r/Geometry.pm +++ b/lib/Slic3r/Geometry.pm @@ -124,6 +124,7 @@ sub nearest_point { if (!defined $distance || $d < $distance) { $nearest_point = $p; $distance = $d; + return $p if $distance < epsilon; } } return $nearest_point; diff --git a/lib/Slic3r/Perimeter.pm b/lib/Slic3r/Perimeter.pm index 69c49e1fd..cbfc1be5d 100644 --- a/lib/Slic3r/Perimeter.pm +++ b/lib/Slic3r/Perimeter.pm @@ -48,14 +48,17 @@ sub make_perimeter { } # create one more offset to be used as boundary for fill - push @{ $layer->fill_surfaces }, - map Slic3r::Surface->new( - surface_type => $surface->surface_type, - contour => Slic3r::Polyline::Closed->cast($_->{outer}), - holes => [ - map Slic3r::Polyline::Closed->cast($_), @{$_->{holes}} - ], - ), $self->offset_polygon($perimeters[-1]), + push @{ $layer->fill_surfaces }, Slic3r::Surface::Collection->new( + surfaces => [ + map Slic3r::Surface->new( + surface_type => $surface->surface_type, + contour => Slic3r::Polyline::Closed->cast($_->{outer}), + holes => [ + map Slic3r::Polyline::Closed->cast($_), @{$_->{holes}} + ], + ), $self->offset_polygon($perimeters[-1]), + ], + ); } # generate paths for holes: diff --git a/lib/Slic3r/Print.pm b/lib/Slic3r/Print.pm index cd2fedcce..bc3aa3f8a 100644 --- a/lib/Slic3r/Print.pm +++ b/lib/Slic3r/Print.pm @@ -143,7 +143,7 @@ sub extrude_fills { $fill_extruder->make_fill($self, $layer); Slic3r::debugf " generated %d paths: %s\n", scalar @{ $layer->fills }, - join ' ', map $_->id, @{ $layer->fills } if $Slic3r::debug; + join ' ', map $_->id, map @{$_->paths}, @{ $layer->fills } if $Slic3r::debug; } } @@ -286,7 +286,10 @@ sub export_gcode { } # extrude fills - $Extrude->($_, 'fill') for @{ $layer->fills }; + for my $fill (@{ $layer->fills }) { + my @paths = $fill->shortest_path($last_pos); + $Extrude->($_, 'fill') for @paths; + } } # write end commands to file diff --git a/lib/Slic3r/Surface/Collection.pm b/lib/Slic3r/Surface/Collection.pm new file mode 100644 index 000000000..94c3df2cf --- /dev/null +++ b/lib/Slic3r/Surface/Collection.pm @@ -0,0 +1,10 @@ +package Slic3r::Surface::Collection; +use Moo; + +has 'surfaces' => ( + is => 'rw', + #isa => 'ArrayRef[Slic3r::Surface]', + default => sub { [] }, +); + +1;