Manual rebase of the avoid_crossing_perimeters feature

This commit is contained in:
Alessandro Ranellucci 2012-08-23 15:42:58 +02:00
parent d278998f11
commit 0eadc5adba
12 changed files with 362 additions and 11 deletions

View file

@ -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

View file

@ -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

View file

@ -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;

View file

@ -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.',

View file

@ -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 {

View file

@ -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 = @_;

View file

@ -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;

View file

@ -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 => [

View file

@ -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] }

View file

@ -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]]);

View file

@ -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;

View file

@ -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