diff --git a/Build.PL b/Build.PL index 89af11ed..b4bed05c 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 4f6b6fba..e48376de 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 c8a5e57e..bc012abc 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 3045fb1c..f4e26b98 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 ac474d92..f57eff59 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 23ee8cc5..770db9a8 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 ea9f1ddb..eb633a74 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 30e54f73..3633c3c5 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 00000000..d55faeb2 --- /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 4bb53035..a0ebc1a9 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 db3ecb6c..71518951 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 9c704c85..26e79045 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 eaaca1d0..68f1002e 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 6e349754..9d9926c4 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 8c2d94e4..5f06cf1d 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 aa6d26b3..c66277c5 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 bc7d864e..064b7bd3 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 285ef45b..49e3be17 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'; }