diff --git a/Build.PL b/Build.PL index 89af11ed83..b4bed05ce0 100644 --- a/Build.PL +++ b/Build.PL @@ -7,7 +7,7 @@ my $build = Module::Build->new( dist_version => '0.1', license => 'perl', requires => { - 'Boost::Geometry::Utils' => '0', + 'Boost::Geometry::Utils' => '0.06', 'Encode::Locale' => '0', 'File::Basename' => '0', 'File::Spec' => '0', diff --git a/MANIFEST b/MANIFEST index 4f6b6fba36..e48376de87 100644 --- a/MANIFEST +++ b/MANIFEST @@ -24,6 +24,7 @@ lib/Slic3r/Format/AMF/Parser.pm lib/Slic3r/Format/OBJ.pm lib/Slic3r/Format/STL.pm lib/Slic3r/GCode.pm +lib/Slic3r/GCode/MotionPlanner.pm lib/Slic3r/Geometry.pm lib/Slic3r/Geometry/Clipper.pm lib/Slic3r/GUI.pm diff --git a/README.markdown b/README.markdown index c8a5e57e7d..bc012abcfa 100644 --- a/README.markdown +++ b/README.markdown @@ -190,6 +190,7 @@ The author of the Silk icon set is Mark James. --toolchange-gcode Load tool-change G-code from the supplied file (default: nothing). --extra-perimeters Add more perimeters when needed (default: yes) --randomize-start Randomize starting point across layers (default: yes) + --avoid-crossing-perimeters Optimize travel moves so that no perimeters are crossed (default: no) --only-retract-when-crossing-perimeters Disable retraction when travelling between infill paths inside the same island. (default: no) diff --git a/lib/Slic3r.pm b/lib/Slic3r.pm index 3045fb1cfb..f4e26b98a0 100644 --- a/lib/Slic3r.pm +++ b/lib/Slic3r.pm @@ -29,6 +29,7 @@ our $var = "$FindBin::Bin/var"; use Encode; use Encode::Locale; +use Boost::Geometry::Utils 0.06; use Moo 0.091009; use Slic3r::Config; @@ -44,6 +45,7 @@ use Slic3r::Format::AMF; use Slic3r::Format::OBJ; use Slic3r::Format::STL; use Slic3r::GCode; +use Slic3r::GCode::MotionPlanner; use Slic3r::Geometry qw(PI); use Slic3r::Layer; use Slic3r::Layer::Region; diff --git a/lib/Slic3r/Config.pm b/lib/Slic3r/Config.pm index ac474d92f8..f57eff5977 100644 --- a/lib/Slic3r/Config.pm +++ b/lib/Slic3r/Config.pm @@ -524,6 +524,13 @@ our $Options = { type => 'bool', default => 1, }, + 'avoid_crossing_perimeters' => { + label => 'Avoid crossing perimeters', + tooltip => 'Optimize travel moves in order to minimize the crossing of perimeters. This is mostly useful with Bowden extruders which suffer from oozing. This feature slows down both the print and the G-code generation.', + cli => 'avoid-crossing-perimeters!', + type => 'bool', + default => 0, + }, 'only_retract_when_crossing_perimeters' => { label => 'Only retract when crossing perimeters', tooltip => 'Disables retraction when travelling between infill paths inside the same island.', diff --git a/lib/Slic3r/ExPolygon.pm b/lib/Slic3r/ExPolygon.pm index 23ee8cc5c9..770db9a8b9 100644 --- a/lib/Slic3r/ExPolygon.pm +++ b/lib/Slic3r/ExPolygon.pm @@ -6,7 +6,7 @@ use warnings; use Boost::Geometry::Utils; use Math::Geometry::Voronoi; -use Slic3r::Geometry qw(X Y A B point_in_polygon same_line line_length); +use Slic3r::Geometry qw(X Y A B point_in_polygon same_line line_length epsilon); use Slic3r::Geometry::Clipper qw(union_ex JT_MITER); # the constructor accepts an array of polygons @@ -159,15 +159,13 @@ sub clip_line { my $self = shift; my ($line) = @_; # line must be a Slic3r::Line object - return Boost::Geometry::Utils::polygon_linestring_intersection( - $self->boost_polygon, - $line->boost_linestring, - ); + return Boost::Geometry::Utils::polygon_multi_linestring_intersection($self, [$line]); } sub simplify { my $self = shift; $_->simplify(@_) for @$self; + $self; } sub scale { @@ -178,11 +176,13 @@ sub scale { sub translate { my $self = shift; $_->translate(@_) for @$self; + $self; } sub rotate { my $self = shift; $_->rotate(@_) for @$self; + $self; } sub area { diff --git a/lib/Slic3r/Fill/Rectilinear.pm b/lib/Slic3r/Fill/Rectilinear.pm index ea9f1ddb06..eb633a74a1 100644 --- a/lib/Slic3r/Fill/Rectilinear.pm +++ b/lib/Slic3r/Fill/Rectilinear.pm @@ -48,9 +48,9 @@ sub fill_surface { # clip paths against a slightly offsetted expolygon, so that the first and last paths # are kept even if the expolygon has vertical sides - my @paths = @{ Boost::Geometry::Utils::polygon_linestring_intersection( - +($expolygon->offset_ex(scaled_epsilon))[0]->boost_polygon, # TODO: we should use all the resulting expolygons and clip the linestrings to a multipolygon object - Boost::Geometry::Utils::linestring(@vertical_lines), + my @paths = @{ Boost::Geometry::Utils::polygon_multi_linestring_intersection( + +($expolygon->offset_ex(scaled_epsilon))[0], # TODO: we should use all the resulting expolygons and clip the linestrings to a multipolygon object + [ @vertical_lines ], ) }; for (@paths) { $_->[0][Y] += $overlap_distance; diff --git a/lib/Slic3r/GCode.pm b/lib/Slic3r/GCode.pm index 30e54f734a..3633c3c5bb 100644 --- a/lib/Slic3r/GCode.pm +++ b/lib/Slic3r/GCode.pm @@ -4,6 +4,7 @@ use Moo; use List::Util qw(max first); use Slic3r::ExtrusionPath ':roles'; use Slic3r::Geometry qw(scale unscale scaled_epsilon points_coincide PI X Y B); +use Slic3r::Geometry::Clipper qw(union_ex); has 'multiple_extruders' => (is => 'ro', default => sub {0} ); has 'layer_count' => (is => 'ro', required => 1 ); @@ -14,6 +15,10 @@ has 'shift_y' => (is => 'rw', default => sub {0} ); has 'z' => (is => 'rw'); has 'speed' => (is => 'rw'); +has 'external_mp' => (is => 'rw'); +has 'layer_mp' => (is => 'rw'); +has 'new_object' => (is => 'rw', default => sub {0}); +has 'straight_once' => (is => 'rw', default => sub {1}); has 'extruder' => (is => 'rw'); has 'extrusion_distance' => (is => 'rw', default => sub {0} ); has 'elapsed_time' => (is => 'rw', default => sub {0} ); # seconds @@ -60,7 +65,7 @@ sub set_shift { # if shift increases (goes towards right), last_pos decreases because it goes towards left $self->last_pos->translate( scale ($self->shift_x - $shift[X]), - scale ($self->shift_x - $shift[Y]), + scale ($self->shift_y - $shift[Y]), ); $self->shift_x($shift[X]); @@ -72,6 +77,11 @@ sub change_layer { my ($layer) = @_; $self->layer($layer); + if ($Slic3r::Config->avoid_crossing_perimeters) { + $self->layer_mp(Slic3r::GCode::MotionPlanner->new( + islands => union_ex([ map @$_, @{$layer->slices} ], undef, 1), + )); + } my $gcode = ""; if ($Slic3r::Config->gcode_flavor =~ /^(?:makerbot|sailfish)$/) { @@ -154,8 +164,7 @@ sub extrude_loop { $point->rotate($angle, $extrusion_path->polyline->[0]); # generate the travel move - $self->speed('travel'); - $gcode .= $self->G0($point, undef, 0, "move inwards before travel"); + $gcode .= $self->travel_to($point, "move inwards before travel"); } return $gcode; @@ -196,9 +205,7 @@ sub extrude_path { } # go to first point of extrusion path - $self->speed('travel'); - $gcode .= $self->G0($path->points->[0], undef, 0, "move to first $description point") - if !points_coincide($self->last_pos, $path->points->[0]); + $gcode .= $self->travel_to($path->points->[0], "move to first $description point"); # compensate retraction $gcode .= $self->unretract; @@ -251,6 +258,44 @@ sub extrude_path { return $gcode; } +sub travel_to { + my $self = shift; + my ($point, $comment) = @_; + + return "" if points_coincide($self->last_pos, $point); + $self->speed('travel'); + my $gcode = ""; + if ($Slic3r::Config->avoid_crossing_perimeters && $self->last_pos->distance_to($point) > scale 5 && !$self->straight_once) { + my $plan = sub { + my $mp = shift; + return join '', + map $self->G0($_->[B], undef, 0, $comment || ""), + $mp->shortest_path($self->last_pos, $point)->lines; + }; + + if ($self->new_object) { + $self->new_object(0); + + # represent $point in G-code coordinates + $point = $point->clone; + my @shift = ($self->shift_x, $self->shift_y); + $point->translate(map scale $_, @shift); + + # calculate path (external_mp uses G-code coordinates so we temporary need a null shift) + $self->set_shift(0,0); + $gcode .= $plan->($self->external_mp); + $self->set_shift(@shift); + } else { + $gcode .= $plan->($self->layer_mp); + } + } else { + $self->straight_once(0); + $gcode .= $self->G0($point, undef, 0, $comment || ""); + } + + return $gcode; +} + sub retract { my $self = shift; my %params = @_; diff --git a/lib/Slic3r/GCode/MotionPlanner.pm b/lib/Slic3r/GCode/MotionPlanner.pm new file mode 100644 index 0000000000..d55faeb2a6 --- /dev/null +++ b/lib/Slic3r/GCode/MotionPlanner.pm @@ -0,0 +1,282 @@ +package Slic3r::GCode::MotionPlanner; +use Moo; + +has 'islands' => (is => 'ro', required => 1); +has 'no_internal' => (is => 'ro'); +has 'last_crossings'=> (is => 'rw'); +has '_inner' => (is => 'rw', default => sub { [] }); # arrayref of arrayrefs of expolygons +has '_outer' => (is => 'rw', default => sub { [] }); # arrayref of arrayrefs of polygons +has '_contours_ex' => (is => 'rw', default => sub { [] }); # arrayref of arrayrefs of expolygons +has '_pointmap' => (is => 'rw', default => sub { {} }); # { id => $point } +has '_edges' => (is => 'rw', default => sub { {} }); # node_idx => { node_idx => distance, ... } +has '_crossing_edges' => (is => 'rw', default => sub { {} }); # edge_idx => bool + +use List::Util qw(first); +use Slic3r::Geometry qw(A B scale epsilon nearest_point); +use Slic3r::Geometry::Clipper qw(diff_ex JT_MITER); + +# clearance (in mm) from the perimeters +has '_inner_margin' => (is => 'ro', default => sub { scale 0.5 }); +has '_outer_margin' => (is => 'ro', default => sub { scale 2 }); + +# this factor weigths the crossing of a perimeter +# vs. the alternative path. a value of 5 means that +# a perimeter will be crossed if the alternative path +# is >= 5x the length of the straight line we could +# follow if we decided to cross the perimeter. +# a nearly-infinite value for this will only permit +# perimeter crossing when there's no alternative path. +use constant CROSSING_FACTOR => 20; + +use constant INFINITY => 'inf'; + +# setup our configuration space +sub BUILD { + my $self = shift; + + my $edges = $self->_edges; + my $crossing_edges = $self->_crossing_edges; + my $tolerance = scale epsilon; + + # given an expolygon, this subroutine connects all its visible points + my $add_expolygon = sub { + my ($expolygon, $crosses_perimeter) = @_; + my @points = map @$_, @$expolygon; + for my $i (0 .. $#points) { + for my $j (($i+1) .. $#points) { + my $line = Slic3r::Line->new($points[$i], $points[$j]); + if ($expolygon->encloses_line($line, $tolerance)) { + my $dist = $line->length * ($crosses_perimeter ? CROSSING_FACTOR : 1); + $edges->{$points[$i]}{$points[$j]} = $dist; + $edges->{$points[$j]}{$points[$i]} = $dist; + $crossing_edges->{$points[$i]}{$points[$j]} = 1; + $crossing_edges->{$points[$j]}{$points[$i]} = 1; + } + } + } + }; + + # process individual islands + for my $i (0 .. $#{$self->islands}) { + # simplify the island's contours + $self->islands->[$i]->simplify($self->_inner_margin); + + # offset the island inwards to make the boundaries for internal movements + # so that no motion along external perimeters happens + $self->_inner->[$i] = [ $self->islands->[$i]->offset_ex(-$self->_inner_margin) ] + if !$self->no_internal; + + # offset the island outwards to make the boundaries for external movements + $self->_outer->[$i] = [ $self->islands->[$i]->contour->offset($self->_outer_margin) ]; + + # further simplification (isn't this a duplication of the one above?) + $_->simplify($self->_inner_margin) for @{$self->_inner->[$i]}, @{$self->_outer->[$i]}; + + # if internal motion is enabled, build a set of utility expolygons representing + # the outer boundaries (as contours) and the inner boundaries (as holes). whenever + # we jump from a hole to a contour or viceversa, we know we're crossing a perimeter + if (!$self->no_internal) { + $self->_contours_ex->[$i] = diff_ex( + $self->_outer->[$i], + [ map $_->contour, @{$self->_inner->[$i]} ], + ); + + # lines enclosed in inner expolygons are visible + $add_expolygon->($_) for @{ $self->_inner->[$i] }; + + # lines enclosed in expolygons covering perimeters are visible + # (but discouraged) + $add_expolygon->($_, 1) for @{ $self->_contours_ex->[$i] }; + } + } + + my $intersects = sub { + my ($polygon, $line) = @_; + @{Boost::Geometry::Utils::polygon_multi_linestring_intersection([$polygon], [$line])} > 0; + }; + + { + my @outer = (map @$_, @{$self->_outer}); + + # lines of outer polygons connect visible points + for my $i (0 .. $#outer) { + foreach my $line ($outer[$i]->lines) { + my $dist = $line->length; + $edges->{$line->[A]}{$line->[B]} = $dist; + $edges->{$line->[B]}{$line->[A]} = $dist; + } + } + + # lines connecting outer polygons are visible + for my $i (0 .. $#outer) { + for my $j (($i+1) .. $#outer) { + for my $m (0 .. $#{$outer[$i]}) { + for my $n (0 .. $#{$outer[$j]}) { + my $line = Slic3r::Line->new($outer[$i][$m], $outer[$j][$n]); + if (!first { $intersects->($_, $line) } @outer) { + # this line does not cross any polygon + my $dist = $line->length; + $edges->{$outer[$i][$m]}{$outer[$j][$n]} = $dist; + $edges->{$outer[$j][$n]}{$outer[$i][$m]} = $dist; + } + } + } + } + } + } + + # lines connecting inner polygons contours are visible but discouraged + if (!$self->no_internal) { + my @inner = (map $_->contour, map @$_, @{$self->_inner}); + for my $i (0 .. $#inner) { + for my $j (($i+1) .. $#inner) { + for my $m (0 .. $#{$inner[$i]}) { + for my $n (0 .. $#{$inner[$j]}) { + my $line = Slic3r::Line->new($inner[$i][$m], $inner[$j][$n]); + if (!first { $intersects->($_, $line) } @inner) { + # this line does not cross any polygon + my $dist = $line->length * CROSSING_FACTOR; + $edges->{$inner[$i][$m]}{$inner[$j][$n]} = $dist; + $edges->{$inner[$j][$n]}{$inner[$i][$m]} = $dist; + $crossing_edges->{$inner[$i][$m]}{$inner[$j][$n]} = 1; + $crossing_edges->{$inner[$j][$n]}{$inner[$i][$m]} = 1; + } + } + } + } + } + } + + $self->_pointmap({ + map +("$_" => $_), + (map @$_, map @$_, map @$_, @{$self->_inner}), + (map @$_, map @$_, @{$self->_outer}), + (map @$_, map @$_, map @$_, @{$self->_contours_ex}), + }); + + if (0) { + my @lines = (); + my %lines = (); + for my $i (keys %{$self->_edges}) { + for my $j (keys %{$self->_edges->{$i}}) { + next if $lines{join '_', sort $i, $j}; + push @lines, [ map $self->_pointmap->{$_}, $i, $j ]; + $lines{join '_', sort $i, $j} = 1; + } + } + + require "Slic3r/SVG.pm"; + Slic3r::SVG::output("space.svg", + lines => \@lines, + points => [ values %{$self->_pointmap} ], + no_arrows => 1, + polygons => [ map @$_, @{$self->islands} ], + #red_polygons => [ map $_->holes, map @$_, @{$self->_inner} ], + #white_polygons => [ map @$_, @{$self->_outer} ], + ); + printf "%d islands\n", scalar @{$self->islands}; + } +} + +sub find_node { + my $self = shift; + my ($point, $near_to) = @_; + + # for optimal pathing, we should check visibility from $point to all $candidates, and then + # choose the one that is nearest to $near_to among the visible ones; however this is probably too slow + + # if we're inside a hole, move to a point on hole; + { + my $polygon = first { $_->encloses_point($point) } (map $_->holes, map @$_, @{$self->_inner}); + return nearest_point($point, $polygon) if $polygon; + } + + # if we're inside an expolygon move to a point on contour or holes + { + my $expolygon = first { $_->encloses_point_quick($point) } (map @$_, @{$self->_inner}); + return nearest_point($point, [ map @$_, @$expolygon ]) if $expolygon; + } + + { + my $outer_polygon_idx; + if (!$self->no_internal) { + # look for an outer expolygon whose contour contains our point + $outer_polygon_idx = first { first { $_->contour->encloses_point($point) } @{$self->_contours_ex->[$_]} } + 0 .. $#{ $self->_contours_ex }; + } else { + # # look for an outer expolygon containing our point + $outer_polygon_idx = first { first { $_->encloses_point($point) } @{$self->_outer->[$_]} } + 0 .. $#{ $self->_outer }; + } + my $candidates = defined $outer_polygon_idx + ? [ map @{$_->contour}, @{$self->_inner->[$outer_polygon_idx]} ] + : [ map @$_, map @$_, @{$self->_outer} ]; + $candidates = [ map @$_, @{$self->_outer->[$outer_polygon_idx]} ] + if @$candidates == 0; + return nearest_point($point, $candidates); + } +} + +sub shortest_path { + my $self = shift; + my ($from, $to) = @_; + + return Slic3r::Polyline->new($from, $to) if !@{$self->islands}; + + # find nearest nodes + my $new_from = $self->find_node($from, $to); + my $new_to = $self->find_node($to, $from); + + my $root = "$new_from"; + my $target = "$new_to"; + my $edges = $self->_edges; + my %dist = map { $_ => INFINITY } keys %$edges; + $dist{$root} = 0; + my %prev = map { $_ => undef } keys %$edges; + my @unsolved = keys %$edges; + my %crossings = (); # node_idx => bool + + while (@unsolved) { + # sort unsolved by distance from root + # using a sorting option that accounts for infinity + @unsolved = sort { + $dist{$a} eq INFINITY ? +1 : + $dist{$b} eq INFINITY ? -1 : + $dist{$a} <=> $dist{$b}; + } @unsolved; + + # we'll solve the closest node + last if $dist{$unsolved[0]} eq INFINITY; + my $n = shift @unsolved; + + # stop search + last if $n eq $target; + + # now, look at all the nodes connected to n + foreach my $n2 (keys %{$edges->{$n}}) { + # .. and find out if any of their estimated distances + # can be improved if we go through n + if ( ($dist{$n2} eq INFINITY) || ($dist{$n2} > ($dist{$n} + $edges->{$n}{$n2})) ) { + $dist{$n2} = $dist{$n} + $edges->{$n}{$n2}; + $prev{$n2} = $n; + $crossings{$n} = 1 if $self->_crossing_edges->{$n}{$n2}; + } + } + } + + my @points = (); + my $crossings = 0; + { + my $pointmap = $self->_pointmap; + my $u = $target; + while (defined $prev{$u}) { + unshift @points, $pointmap->{$u}; + $crossings++ if $crossings{$u}; + $u = $prev{$u}; + } + } + $self->last_crossings($crossings); + return Slic3r::Polyline->new($from, $new_from, @points, $to); # @points already includes $new_to +} + +1; diff --git a/lib/Slic3r/GUI/Tab.pm b/lib/Slic3r/GUI/Tab.pm index 4bb5303509..a0ebc1a9af 100644 --- a/lib/Slic3r/GUI/Tab.pm +++ b/lib/Slic3r/GUI/Tab.pm @@ -404,6 +404,10 @@ sub build { }, ], }, + { + title => 'Advanced', + options => [qw(avoid_crossing_perimeters)], + }, ]); $self->add_options_page('Infill', 'shading.png', optgroups => [ diff --git a/lib/Slic3r/Geometry.pm b/lib/Slic3r/Geometry.pm index db3ecb6c81..7151895191 100644 --- a/lib/Slic3r/Geometry.pm +++ b/lib/Slic3r/Geometry.pm @@ -7,6 +7,7 @@ our @ISA = qw(Exporter); our @EXPORT_OK = qw( PI X Y Z A B X1 Y1 X2 Y2 MIN MAX epsilon slope line_atan lines_parallel line_point_belongs_to_segment points_coincide distance_between_points + comparable_distance_between_points line_length midpoint point_in_polygon point_in_segment segment_in_segment point_is_on_left_of_segment polyline_lines polygon_lines nearest_point point_along_segment polygon_segment_having_point polygon_has_subsegment @@ -114,6 +115,11 @@ sub distance_between_points { return sqrt((($p1->[X] - $p2->[X])**2) + ($p1->[Y] - $p2->[Y])**2); } +sub comparable_distance_between_points { + my ($p1, $p2) = @_; + return (($p1->[X] - $p2->[X])**2) + (($p1->[Y] - $p2->[Y])**2); +} + sub point_line_distance { my ($point, $line) = @_; return distance_between_points($point, $line->[A]) @@ -251,7 +257,7 @@ sub nearest_point_index { my ($nearest_point_index, $distance) = (); for my $i (0..$#$points) { - my $d = distance_between_points($point, $points->[$i]); + my $d = comparable_distance_between_points($point, $points->[$i]); if (!defined $distance || $d < $distance) { $nearest_point_index = $i; $distance = $d; diff --git a/lib/Slic3r/Point.pm b/lib/Slic3r/Point.pm index 9c704c8532..26e79045af 100644 --- a/lib/Slic3r/Point.pm +++ b/lib/Slic3r/Point.pm @@ -39,12 +39,14 @@ sub rotate { my $self = shift; my ($angle, $center) = @_; @$self = @{ +(Slic3r::Geometry::rotate_points($angle, $center, $self))[0] }; + $self; } sub translate { my $self = shift; my ($x, $y) = @_; @$self = @{ +(Slic3r::Geometry::move_points([$x, $y], $self))[0] }; + $self; } sub x { $_[0]->[0] } diff --git a/lib/Slic3r/Polygon.pm b/lib/Slic3r/Polygon.pm index eaaca1d0f7..68f1002eb6 100644 --- a/lib/Slic3r/Polygon.pm +++ b/lib/Slic3r/Polygon.pm @@ -14,6 +14,11 @@ sub lines { return polygon_lines($self); } +sub boost_polygon { + my $self = shift; + return Boost::Geometry::Utils::polygon($self); +} + sub boost_linestring { my $self = shift; return Boost::Geometry::Utils::linestring([@$self, $self->[0]]); diff --git a/lib/Slic3r/Polyline.pm b/lib/Slic3r/Polyline.pm index 6e3497544a..9d9926c42a 100644 --- a/lib/Slic3r/Polyline.pm +++ b/lib/Slic3r/Polyline.pm @@ -68,7 +68,7 @@ sub simplify { my $self = shift; my $tolerance = shift || 10; - @$self = @{ Slic3r::Geometry::douglas_peucker($self, $tolerance) }; + @$self = @{ Boost::Geometry::Utils::linestring_simplify($self, $tolerance) }; bless $_, 'Slic3r::Point' for @$self; } @@ -115,10 +115,7 @@ sub clip_with_expolygon { my $self = shift; my ($expolygon) = @_; - my $result = Boost::Geometry::Utils::polygon_linestring_intersection( - $expolygon->boost_polygon, - $self->boost_linestring, - ); + my $result = Boost::Geometry::Utils::polygon_multi_linestring_intersection($expolygon, [$self]); bless $_, 'Slic3r::Polyline' for @$result; bless $_, 'Slic3r::Point' for map @$_, @$result; return @$result; diff --git a/lib/Slic3r/Print.pm b/lib/Slic3r/Print.pm index 8c2d94e462..5f06cf1dbe 100644 --- a/lib/Slic3r/Print.pm +++ b/lib/Slic3r/Print.pm @@ -713,9 +713,29 @@ sub write_gcode { $Slic3r::Config->print_center->[Y] - (unscale ($print_bb[Y2] - $print_bb[Y1]) / 2) - unscale $print_bb[Y1], ); + # initialize a motion planner for object-to-object travel moves + if ($Slic3r::Config->avoid_crossing_perimeters) { + my $distance_from_objects = 1; + # compute the offsetted convex hull for each object and repeat it for each copy. + my @islands = (); + foreach my $obj_idx (0 .. $#{$self->objects}) { + my @island = Slic3r::ExPolygon->new(convex_hull([ + map @{$_->contour}, map @{$_->slices}, @{$self->objects->[$obj_idx]->layers}, + ]))->translate(scale $shift[X], scale $shift[Y])->offset_ex(scale $distance_from_objects, 1, JT_SQUARE); + foreach my $copy (@{ $self->objects->[$obj_idx]->copies }) { + push @islands, map $_->clone->translate(@$copy), @island; + } + } + $gcodegen->external_mp(Slic3r::GCode::MotionPlanner->new( + islands => union_ex([ map @$_, @islands ]), + no_internal => 1, + )); + } + # prepare the logic to print one layer my $skirt_done = 0; # count of skirt layers done my $brim_done = 0; + my $last_obj_copy = ""; my $extrude_layer = sub { my ($layer_id, $object_copies) = @_; my $gcode = ""; @@ -758,6 +778,7 @@ sub write_gcode { } } $skirt_done++; + $gcodegen->straight_once(1); } # extrude brim @@ -767,10 +788,13 @@ sub write_gcode { $gcodegen->set_shift(@shift); $gcode .= $gcodegen->extrude_loop($_, 'brim') for @{$self->brim}; $brim_done = 1; + $gcodegen->straight_once(1); } for my $obj_copy (@$object_copies) { my ($obj_idx, $copy) = @$obj_copy; + $gcodegen->new_object(1) if $last_obj_copy && $last_obj_copy ne "${obj_idx}_${copy}"; + $last_obj_copy = "${obj_idx}_${copy}"; my $layer = $self->objects->[$obj_idx]->layers->[$layer_id]; $gcodegen->set_shift(map $shift[$_] + unscale $copy->[$_], X,Y); diff --git a/slic3r.pl b/slic3r.pl index aa6d26b325..c66277c58a 100755 --- a/slic3r.pl +++ b/slic3r.pl @@ -238,6 +238,7 @@ $j --toolchange-gcode Load tool-change G-code from the supplied file (default: nothing). --extra-perimeters Add more perimeters when needed (default: yes) --randomize-start Randomize starting point across layers (default: yes) + --avoid-crossing-perimeters Optimize travel moves so that no perimeters are crossed (default: no) --only-retract-when-crossing-perimeters Disable retraction when travelling between infill paths inside the same island. (default: no) diff --git a/t/clean_polylines.t b/t/clean_polylines.t index bc7d864e6f..064b7bd3ac 100644 --- a/t/clean_polylines.t +++ b/t/clean_polylines.t @@ -32,8 +32,9 @@ use Slic3r; my $polyline = Slic3r::Polyline->new([ [0,0],[0.5,0.5],[1,0],[1.25,-0.25],[1.5,.5], ]); - $polyline->simplify(0.25); - is_deeply $polyline, [ [0, 0], [0.5, 0.5], [1.25, -0.25], [1.5, 0.5] ], 'Douglas-Peucker'; + $polyline->scale(100); + $polyline->simplify(25); + is_deeply $polyline, [ [0, 0], [50, 50], [125, -25], [150, 50] ], 'Douglas-Peucker'; } { diff --git a/t/polyclip.t b/t/polyclip.t index 285ef45beb..49e3be175a 100644 --- a/t/polyclip.t +++ b/t/polyclip.t @@ -92,7 +92,7 @@ is_deeply $intersection, [ [12, 12], [18, 16] ], 'internal lines are preserved'; { my $intersections = $expolygon->clip_line(Slic3r::Line->new(reverse @$line)); is_deeply $intersections, [ - [ [20, 15], [16, 15] ], + [ [20, 15], [15, 15] ], [ [14, 15], [10, 15] ], ], 'reverse line is clipped to square with hole'; } @@ -144,8 +144,8 @@ is_deeply $intersection, [ [12, 12], [18, 16] ], 'internal lines are preserved'; my $intersections = $expolygon->clip_line($line); is_deeply $intersections, [ - [ [152.742, 288.087], [152.742, 215.179], ], - [ [152.742, 108.088], [152.742, 35.1665] ], + [ [152, 287], [152, 214], ], + [ [152, 107], [152, 35] ], ], 'line is clipped to square with hole'; }