diff --git a/MANIFEST b/MANIFEST index 19de724981..b021f8d85f 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 fc5ce7f864..0e1d72c728 100644 --- a/README.markdown +++ b/README.markdown @@ -170,6 +170,7 @@ The author of the Silk icon set is Mark James. --layer-gcode Load layer-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) Support material options: --support-material Generate support material for overhangs diff --git a/lib/Slic3r.pm b/lib/Slic3r.pm index dc97761213..72a088702a 100644 --- a/lib/Slic3r.pm +++ b/lib/Slic3r.pm @@ -38,6 +38,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::Line; diff --git a/lib/Slic3r/Config.pm b/lib/Slic3r/Config.pm index 7af58122cd..de64fc2742 100644 --- a/lib/Slic3r/Config.pm +++ b/lib/Slic3r/Config.pm @@ -462,6 +462,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 the processing times.', + cli => 'avoid-crossing-perimeters!', + type => 'bool', + default => 0, + }, 'support_material' => { label => 'Generate support material', tooltip => 'Enable support material generation.', diff --git a/lib/Slic3r/ExPolygon.pm b/lib/Slic3r/ExPolygon.pm index 7037185519..717fc1d53c 100644 --- a/lib/Slic3r/ExPolygon.pm +++ b/lib/Slic3r/ExPolygon.pm @@ -155,16 +155,19 @@ sub clip_line { sub simplify { my $self = shift; $_->simplify(@_) for @$self; + $self; } 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/GCode.pm b/lib/Slic3r/GCode.pm index b5e020ace0..273753d412 100644 --- a/lib/Slic3r/GCode.pm +++ b/lib/Slic3r/GCode.pm @@ -2,7 +2,8 @@ package Slic3r::GCode; use Moo; use Slic3r::ExtrusionPath ':roles'; -use Slic3r::Geometry qw(scale unscale); +use Slic3r::Geometry qw(PI X Y scale unscale points_coincide); +use Slic3r::Geometry::Clipper qw(union_ex); has 'layer' => (is => 'rw'); has 'shift_x' => (is => 'rw', default => sub {0} ); @@ -10,6 +11,8 @@ has 'shift_y' => (is => 'rw', default => sub {0} ); has 'z' => (is => 'rw', default => sub {0} ); has 'speed' => (is => 'rw'); +has 'motionplanner' => (is => 'rw'); +has 'straight_once' => (is => 'rw'); has 'extruder_idx' => (is => 'rw'); has 'extrusion_distance' => (is => 'rw', default => sub {0} ); has 'elapsed_time' => (is => 'rw', default => sub {0} ); # seconds @@ -48,18 +51,35 @@ my %role_speeds = ( &EXTR_ROLE_SUPPORTMATERIAL => 'perimeter', ); -use Slic3r::Geometry qw(points_coincide PI X Y); - sub extruder { my $self = shift; return $Slic3r::extruders->[$self->extruder_idx]; } +sub set_shift { + my $self = shift; + my @shift = @_; + + # adjust last position + $self->last_pos($self->last_pos->clone->translate( + scale($self->shift_x - $shift[X]), + scale($self->shift_y - $shift[Y]), + )); + + $self->shift_x($shift[X]); + $self->shift_y($shift[Y]); +} + sub change_layer { my $self = shift; my ($layer) = @_; $self->layer($layer); + if ($Slic3r::Config->avoid_crossing_perimeters) { + $self->motionplanner(Slic3r::GCode::MotionPlanner->new( + islands => union_ex([ map @{$_->expolygon}, @{$layer->slices} ], undef, 1), + )); + } my $z = $Slic3r::Config->z_offset + $layer->print_z * &Slic3r::SCALING_FACTOR; my $gcode = ""; @@ -145,8 +165,7 @@ sub extrude_path { } # go to first point of extrusion path - $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 if $self->extruder->retracted; @@ -192,6 +211,20 @@ sub extrude_path { return $gcode; } +sub travel_to { + my $self = shift; + my ($point, $comment) = @_; + + return "" if points_coincide($self->last_pos, $point); + if ($Slic3r::Config->avoid_crossing_perimeters && $self->last_pos->distance_to($point) > scale 5 && !$self->straight_once) { + return join '', map $self->G0($_->b, undef, 0, $comment || ""), + $self->motionplanner->shortest_path($self->last_pos, $point)->lines; + } else { + $self->straight_once(0); + return $self->G0($point, undef, 0, $comment || ""); + } +} + 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..cf0d97a2d0 --- /dev/null +++ b/lib/Slic3r/GCode/MotionPlanner.pm @@ -0,0 +1,259 @@ +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(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; + + 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, scale Slic3r::Geometry::epsilon)) { + 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; + } + } + } + }; + + for my $i (0 .. $#{$self->islands}) { + $self->islands->[$i]->simplify($self->_inner_margin); + $self->_inner->[$i] = [ $self->islands->[$i]->offset_ex(-$self->_inner_margin) ] + if !$self->no_internal; + $self->_outer->[$i] = [ $self->islands->[$i]->contour->offset($self->_outer_margin) ]; + $_->simplify($self->_inner_margin) for @{$self->_inner->[$i]}, @{$self->_outer->[$i]}; + + 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_linestring_intersection( + $polygon->boost_polygon, + Boost::Geometry::Utils::linestring($line), + )} > 0; + }; + + # lines connecting outer polygons are visible + { + my @outer = (map @$_, @{$self->_outer}); + 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(undef, "space.svg", + lines => \@lines, + 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) = @_; + + # 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 111800e0aa..1e152b9e31 100644 --- a/lib/Slic3r/GUI/Tab.pm +++ b/lib/Slic3r/GUI/Tab.pm @@ -392,6 +392,10 @@ sub build { title => 'Horizontal shells', options => [qw(solid_layers)], }, + { + title => 'Advanced', + options => [qw(avoid_crossing_perimeters)], + }, ]); $self->add_options_page('Infill', 'shading.png', optgroups => [ diff --git a/lib/Slic3r/Point.pm b/lib/Slic3r/Point.pm index ffa7fa1a87..52720b16af 100644 --- a/lib/Slic3r/Point.pm +++ b/lib/Slic3r/Point.pm @@ -58,12 +58,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 fa7ee712d8..224478905b 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/Print.pm b/lib/Slic3r/Print.pm index c5aec01a70..df1fc4c0a1 100644 --- a/lib/Slic3r/Print.pm +++ b/lib/Slic3r/Print.pm @@ -613,9 +613,30 @@ 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 + my $external_motionplanner; + 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 @{$_->expolygon->contour}, map @{$_->slices}, @{$self->objects->[$obj_idx]->layers}, + ]))->translate(map -$_, @shift)->offset_ex(scale $distance_from_objects); + foreach my $copy (@{$self->copies->[$obj_idx]}) { + push @islands, map $_->clone->translate(@$copy), @island; + } + } + $external_motionplanner = 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 = ""; @@ -635,8 +656,7 @@ sub write_gcode { # extrude skirt if ($skirt_done < $Slic3r::Config->skirt_height) { - $gcodegen->shift_x($shift[X]); - $gcodegen->shift_y($shift[Y]); + $gcodegen->set_shift(@shift); $gcode .= $gcodegen->set_acceleration($Slic3r::Config->perimeter_acceleration); # skip skirt if we have a large brim if ($layer_id < $Slic3r::Config->skirt_height && ($layer_id != 0 || $Slic3r::Config->skirt_distance + (($Slic3r::Config->skirts - 1) * $Slic3r::flow->spacing) > $Slic3r::Config->brim_width)) { @@ -647,8 +667,7 @@ sub write_gcode { # extrude brim if ($layer_id == 0 && !$brim_done) { - $gcodegen->shift_x($shift[X]); - $gcodegen->shift_y($shift[Y]); + $gcodegen->set_shift(@shift); $gcode .= $gcodegen->extrude_loop($_, 'brim') for @{$self->brim}; $brim_done = 1; } @@ -661,8 +680,21 @@ sub write_gcode { # won't always trigger the automatic retraction $gcode .= $gcodegen->retract; - $gcodegen->shift_x($shift[X] + unscale $copy->[X]); - $gcodegen->shift_y($shift[Y] + unscale $copy->[Y]); + # travel to the first perimeter point using the external motion planner + if ($external_motionplanner && @{ $layer->perimeters } && !$gcodegen->straight_once && $last_obj_copy ne "${obj_idx}_${copy}") { + $gcodegen->set_shift(@shift); + my $layer_mp = $gcodegen->motionplanner; + $gcodegen->motionplanner($external_motionplanner); + my $first_perimeter = $layer->perimeters->[0]->unpack; + my $target = $first_perimeter->polygon->[0]->clone->translate(@$copy); + $gcode .= $gcodegen->travel_to($target, "move to first perimeter point"); + $gcodegen->motionplanner($layer_mp); + } + + $gcodegen->set_shift( + $shift[X] + unscale $copy->[X], + $shift[Y] + unscale $copy->[Y], + ); # extrude perimeters $gcode .= $gcodegen->set_tool($Slic3r::Config->perimeter_extruder-1); @@ -686,6 +718,8 @@ sub write_gcode { $gcode .= $gcodegen->extrude_path($_, 'support material') for $layer->support_fills->shortest_path($gcodegen->last_pos); } + + $last_obj_copy = "${obj_idx}_${copy}"; } return if !$gcode; diff --git a/slic3r.pl b/slic3r.pl index a4f5ca4d19..602ebbb658 100755 --- a/slic3r.pl +++ b/slic3r.pl @@ -213,6 +213,7 @@ $j --layer-gcode Load layer-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) Support material options: --support-material Generate support material for overhangs