From c5d5e4d244f049ef9068f3b13370577774916cc2 Mon Sep 17 00:00:00 2001 From: Alessandro Ranellucci Date: Fri, 11 Nov 2011 22:01:27 +0100 Subject: [PATCH] Cleanup lines resulting from plane intersection before detecting polygons. This allows for more tolerance with dirty models. Performance impact depends on how many layers are detected as dirty. #16 #28 --- MANIFEST | 1 + lib/Slic3r/Extruder.pm | 6 +- lib/Slic3r/ExtrusionPath.pm | 5 +- lib/Slic3r/Geometry.pm | 23 +++- lib/Slic3r/Layer.pm | 241 ++++++++++++++++++++---------------- lib/Slic3r/Line.pm | 10 +- lib/Slic3r/Perimeter.pm | 5 +- lib/Slic3r/Print.pm | 4 +- lib/Slic3r/STL.pm | 5 +- t/collinear.t | 91 ++++++++++++++ 10 files changed, 259 insertions(+), 132 deletions(-) create mode 100644 t/collinear.t diff --git a/MANIFEST b/MANIFEST index 61f1923c6..bf403ca67 100644 --- a/MANIFEST +++ b/MANIFEST @@ -39,6 +39,7 @@ slic3r.pl t/arcs.t t/clean_polylines.t t/clipper.t +t/collinear.t t/geometry.t t/polyclip.t t/stl.t diff --git a/lib/Slic3r/Extruder.pm b/lib/Slic3r/Extruder.pm index 704a3d5a2..5bcfaabdc 100644 --- a/lib/Slic3r/Extruder.pm +++ b/lib/Slic3r/Extruder.pm @@ -31,13 +31,9 @@ has 'retract_speed' => ( default => sub { $Slic3r::retract_speed * 60 }, # mm/min ); -use Slic3r::Geometry qw(points_coincide); +use Slic3r::Geometry qw(points_coincide PI X Y); use XXX; -use constant PI => 4 * atan2(1, 1); -use constant X => 0; -use constant Y => 1; - sub move_z { my $self = shift; my ($z) = @_; diff --git a/lib/Slic3r/ExtrusionPath.pm b/lib/Slic3r/ExtrusionPath.pm index 3c33f6501..2702b20ac 100644 --- a/lib/Slic3r/ExtrusionPath.pm +++ b/lib/Slic3r/ExtrusionPath.pm @@ -7,10 +7,7 @@ extends 'Slic3r::Polyline'; # expressed in layers has 'depth_layers' => (is => 'ro', default => sub {1}); -use constant X => 0; -use constant Y => 1; - -use Slic3r::Geometry qw(PI epsilon deg2rad rotate_points); +use Slic3r::Geometry qw(PI X Y epsilon deg2rad rotate_points); use XXX; sub clip_end { diff --git a/lib/Slic3r/Geometry.pm b/lib/Slic3r/Geometry.pm index ade98aecc..39fdc0937 100644 --- a/lib/Slic3r/Geometry.pm +++ b/lib/Slic3r/Geometry.pm @@ -5,7 +5,7 @@ use warnings; require Exporter; our @ISA = qw(Exporter); our @EXPORT_OK = qw( - PI epsilon slope line_atan lines_parallel three_points_aligned + PI X Y Z A B epsilon slope line_atan lines_parallel three_points_aligned line_point_belongs_to_segment points_coincide 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 @@ -17,7 +17,7 @@ our @EXPORT_OK = qw( clip_segment_complex_polygon longest_segment angle3points polyline_remove_parallel_continuous_edges polyline_remove_acute_vertices polygon_remove_acute_vertices polygon_remove_parallel_continuous_edges - shortest_path + shortest_path collinear ); use Slic3r::Geometry::DouglasPeucker qw(Douglas_Peucker); @@ -28,6 +28,7 @@ use constant A => 0; use constant B => 1; use constant X => 0; use constant Y => 1; +use constant Z => 2; our $parallel_degrees_limit = abs(deg2rad(3)); our $epsilon = 1E-4; @@ -416,6 +417,22 @@ sub line_intersection { : undef; } +sub collinear { + my ($line1, $line2, $require_overlapping) = @_; + my $intersection = _line_intersection(map @$_, @$line1, @$line2); + return 0 unless !ref($intersection) + && ($intersection eq 'parallel collinear' + || ($intersection eq 'parallel vertical' && abs($line1->[A][X] - $line2->[A][X]) < epsilon)); + + if ($require_overlapping) { + my @box_a = bounding_box([ $line1->[0], $line1->[1] ]); + my @box_b = bounding_box([ $line2->[0], $line2->[1] ]); + return 0 unless bounding_box_intersect( 2, @box_a, @box_b ); + } + + return 1; +} + sub _line_intersection { my ( $x0, $y0, $x1, $y1, $x2, $y2, $x3, $y3 ); @@ -495,7 +512,7 @@ sub _line_intersection { my $ya = $y0 - $dyx10 * $x0; my $yb = $y2 - $dyx32 * $x2; - + return "parallel collinear" if abs( $ya - $yb ) < epsilon; return "parallel"; } diff --git a/lib/Slic3r/Layer.pm b/lib/Slic3r/Layer.pm index 75e4327d8..9a5242924 100644 --- a/lib/Slic3r/Layer.pm +++ b/lib/Slic3r/Layer.pm @@ -3,14 +3,10 @@ use Moo; use Math::Clipper ':all'; use Slic3r::Geometry qw(polygon_lines points_coincide angle3points polyline_lines nearest_point - line_length); -use Slic3r::Geometry::Clipper qw(safety_offset union_ex PFT_EVENODD); + line_length collinear X Y A B PI); +use Slic3r::Geometry::Clipper qw(safety_offset union_ex); use XXX; -use constant PI => 4 * atan2(1, 1); -use constant A => 0; -use constant B => 1; - # a sequential number of layer, starting at 0 has 'id' => ( is => 'ro', @@ -116,122 +112,149 @@ sub add_line { return $line; } -sub remove_line { +# merge overlapping lines +sub cleanup_lines { my $self = shift; - my ($line) = @_; - @{ $self->lines } = grep $_ ne $line, @{ $self->lines }; -} - -sub remove_surface { - my $self = shift; - my ($surface) = @_; - @{ $self->surfaces } = grep $_ ne $surface, @{ $self->surfaces }; + + my $lines = $self->lines; + my $line_count = @$lines; + + for (my $i = 0; $i <= $#$lines-1; $i++) { + for (my $j = $i+1; $j <= $#$lines; $j++) { + # lines are collinear and overlapping? + next unless collinear($lines->[$i], $lines->[$j], 1); + + # lines have same orientation? + next unless ($lines->[$i][A][X] <=> $lines->[$i][B][X]) == ($lines->[$j][A][X] <=> $lines->[$j][B][X]) + && ($lines->[$i][A][Y] <=> $lines->[$i][B][Y]) == ($lines->[$j][A][Y] <=> $lines->[$j][B][Y]); + + # resulting line + my @x = sort { $a <=> $b } ($lines->[$i][A][X], $lines->[$i][B][X], $lines->[$j][A][X], $lines->[$j][B][X]); + my @y = sort { $a <=> $b } ($lines->[$i][A][Y], $lines->[$i][B][Y], $lines->[$j][A][Y], $lines->[$j][B][Y]); + my $new_line = Slic3r::Line->new([$x[0], $y[0]], [$x[-1], $y[-1]]); + for (X, Y) { + ($new_line->[A][$_], $new_line->[B][$_]) = ($new_line->[B][$_], $new_line->[A][$_]) + if $lines->[$i][A][$_] > $lines->[$i][B][$_]; + } + + # save new line and remove found one + $lines->[$i] = $new_line; + splice @$lines, $j, 1; + $j--; + } + } + + Slic3r::debugf " merging %d lines resulted in %d lines\n", $line_count, scalar(@$lines); } # build polylines from lines sub make_surfaces { my $self = shift; - my @lines = (); - push @lines, @{$self->lines}; - #@lines = grep line_length($_) > xx, @lines; - - #use Slic3r::SVG; - #Slic3r::SVG::output(undef, "lines.svg", - # lines => [ map $_->p, grep !$_->isa('Slic3r::Line::FacetEdge'), @{$self->lines} ], - # red_lines => [ map $_->p, grep $_->isa('Slic3r::Line::FacetEdge'), @{$self->lines} ], - #); - - my $get_point_id = sub { sprintf "%.0f,%.0f", @{$_[0]} }; - - my (%pointmap, @pointmap_keys) = (); - foreach my $line (@lines) { - my $point_id = $get_point_id->($line->[A]); - if (!exists $pointmap{$point_id}) { - $pointmap{$point_id} = []; - push @pointmap_keys, $line->[A]; - } - push @{ $pointmap{$point_id} }, $line; + if (0) { + require "Slic3r/SVG.pm"; + Slic3r::SVG::output(undef, "lines.svg", + lines => [ grep !$_->isa('Slic3r::Line::FacetEdge'), @{$self->lines} ], + red_lines => [ grep $_->isa('Slic3r::Line::FacetEdge'), @{$self->lines} ], + ); } - my $n = 0; my (@polygons, %visited_lines, @discarded_lines, @discarded_polylines) = (); - while (my $first_line = shift @lines) { - next if $visited_lines{ $first_line->id }; - my @points = @$first_line; + + my $detect = sub { + my @lines = @{$self->lines}; + (@polygons, %visited_lines, @discarded_lines, @discarded_polylines) = (); + my $get_point_id = sub { sprintf "%.0f,%.0f", @{$_[0]} }; - my @seen_lines = ($first_line); - my %seen_points = map { $get_point_id->($points[$_]) => $_ } 0..1; + my (%pointmap, @pointmap_keys) = (); + foreach my $line (@lines) { + my $point_id = $get_point_id->($line->[A]); + if (!exists $pointmap{$point_id}) { + $pointmap{$point_id} = []; + push @pointmap_keys, $line->[A]; + } + push @{ $pointmap{$point_id} }, $line; + } - CYCLE: while (1) { - my $next_lines = $pointmap{ $get_point_id->($points[-1]) }; + my $n = 0; + while (my $first_line = shift @lines) { + next if $visited_lines{ $first_line->id }; + my @points = @$first_line; - # shouldn't we find the point, let's try with a slower algorithm - # as approximation may make the coordinates differ - if (!$next_lines) { - my $nearest_point = nearest_point($points[-1], \@pointmap_keys); - #printf " we have a nearest point: %f,%f (%s)\n", @$nearest_point, $get_point_id->($nearest_point); + my @seen_lines = ($first_line); + my %seen_points = map { $get_point_id->($points[$_]) => $_ } 0..1; + + CYCLE: while (1) { + my $next_lines = $pointmap{ $get_point_id->($points[-1]) }; - if ($nearest_point) { - local $Slic3r::Geometry::epsilon = 1000000; - $next_lines = $pointmap{$get_point_id->($nearest_point)} - if points_coincide($points[-1], $nearest_point); + # shouldn't we find the point, let's try with a slower algorithm + # as approximation may make the coordinates differ + if (!$next_lines) { + my $nearest_point = nearest_point($points[-1], \@pointmap_keys); + #printf " we have a nearest point: %f,%f (%s)\n", @$nearest_point, $get_point_id->($nearest_point); + + if ($nearest_point) { + local $Slic3r::Geometry::epsilon = 1000000; + $next_lines = $pointmap{$get_point_id->($nearest_point)} + if points_coincide($points[-1], $nearest_point); + } } + + #Slic3r::SVG::output(undef, "lines.svg", + # lines => [ map $_->p, grep !$_->isa('Slic3r::Line::FacetEdge'), @{$self->lines} ], + # red_lines => [ map $_->p, grep $_->isa('Slic3r::Line::FacetEdge'), @{$self->lines} ], + # points => [ $points[-1] ], + # no_arrows => 1, + #) if !$next_lines; + + $next_lines + or die sprintf("No lines start at point %s. This shouldn't happen. Please check the model for manifoldness.", $get_point_id->($points[-1])); + last CYCLE if !@$next_lines; + + my @ordered_next_lines = sort + { angle3points($points[-1], $points[-2], $next_lines->[$a][B]) <=> angle3points($points[-1], $points[-2], $next_lines->[$b][B]) } + 0..$#$next_lines; + + #if (@$next_lines > 1) { + # Slic3r::SVG::output(undef, "next_line.svg", + # lines => $next_lines, + # red_lines => [ polyline_lines([@points]) ], + # green_lines => [ $next_lines->[ $ordered_next_lines[0] ] ], + # ); + #} + + my ($next_line) = splice @$next_lines, $ordered_next_lines[0], 1; + push @seen_lines, $next_line; + + push @points, $next_line->[B]; + + my $point_id = $get_point_id->($points[-1]); + if ($seen_points{$point_id}) { + splice @points, 0, $seen_points{$point_id}; + last CYCLE; + } + + $seen_points{$point_id} = $#points; } - #Slic3r::SVG::output(undef, "lines.svg", - # lines => [ map $_->p, grep !$_->isa('Slic3r::Line::FacetEdge'), @{$self->lines} ], - # red_lines => [ map $_->p, grep $_->isa('Slic3r::Line::FacetEdge'), @{$self->lines} ], - # points => [ $points[-1] ], - # no_arrows => 1, - #) if !$next_lines; - - $next_lines - or die sprintf("No lines start at point %s. This shouldn't happen. Please check the model for manifoldness.", $get_point_id->($points[-1])); - last CYCLE if !@$next_lines; - - my @ordered_next_lines = sort - { angle3points($points[-1], $points[-2], $next_lines->[$a][B]) <=> angle3points($points[-1], $points[-2], $next_lines->[$b][B]) } - 0..$#$next_lines; - - #if (@$next_lines > 1) { - # Slic3r::SVG::output(undef, "next_line.svg", - # lines => $next_lines, - # red_lines => [ polyline_lines([@points]) ], - # green_lines => [ $next_lines->[ $ordered_next_lines[0] ] ], - # ); - #} - - my ($next_line) = splice @$next_lines, $ordered_next_lines[0], 1; - push @seen_lines, $next_line; - - push @points, $next_line->[B]; - - my $point_id = $get_point_id->($points[-1]); - if ($seen_points{$point_id}) { - splice @points, 0, $seen_points{$point_id}; - last CYCLE; + if (@points < 4 || !points_coincide($points[0], $points[-1])) { + # discarding polyline + push @discarded_lines, @seen_lines; + if (@points > 2) { + push @discarded_polylines, [@points]; + } + next; } - $seen_points{$point_id} = $#points; + $visited_lines{ $_->id } = 1 for @seen_lines; + pop @points; + Slic3r::debugf "Discovered polygon of %d points\n", scalar(@points); + push @polygons, Slic3r::Polygon->new(@points); + $polygons[-1]->cleanup; } - - if (@points < 4 || !points_coincide($points[0], $points[-1])) { - # discarding polyline - if (@points == 2) { - push @discarded_lines, [@points]; - } else { - push @discarded_polylines, [@points]; - } - next; - } - - $visited_lines{ $_->id } = 1 for @seen_lines; - pop @points; - Slic3r::debugf "Discovered polygon of %d points\n", scalar(@points); - push @polygons, Slic3r::Polygon->new(@points); - $polygons[-1]->cleanup; - } + }; + + $detect->(); # Now, if we got a clean and manifold model then @polygons would contain everything # we need to draw our layer. In real life, sadly, things are different and it is likely @@ -243,10 +266,11 @@ sub make_surfaces { # other line. # So, let's first check what lines were not detected as part of polygons. - if (@discarded_lines || @discarded_polylines) { - print " Warning: errors while parsing this layer (dirty or non-manifold model)\n"; + if (@discarded_lines) { Slic3r::debugf " %d lines out of %d were discarded and %d polylines were not closed\n", scalar(@discarded_lines), scalar(@{$self->lines}), scalar(@discarded_polylines); + print " Warning: errors while parsing this layer (dirty or non-manifold model).\n"; + print " Retrying with slower algorithm.\n"; if (0) { require "Slic3r/SVG.pm"; @@ -261,10 +285,17 @@ sub make_surfaces { ); exit; } + + $self->cleanup_lines; + $detect->(); + + if (@discarded_lines) { + print " Warning: even slow detection algorithm throwed errors. Review the output before printing.\n"; + } } { - my $expolygons = union_ex([ @polygons ], PFT_EVENODD); + my $expolygons = union_ex([ @polygons ]); Slic3r::debugf " %d surface(s) having %d holes detected from %d polylines\n", scalar(@$expolygons), scalar(map $_->holes, @$expolygons), scalar(@polygons); diff --git a/lib/Slic3r/Line.pm b/lib/Slic3r/Line.pm index eda212ec8..0e192a400 100644 --- a/lib/Slic3r/Line.pm +++ b/lib/Slic3r/Line.pm @@ -2,10 +2,7 @@ package Slic3r::Line; use strict; use warnings; -use constant A => 0; -use constant B => 1; -use constant X => 0; -use constant Y => 1; +use Slic3r::Geometry qw(A B X Y); sub new { my $class = shift; @@ -100,4 +97,9 @@ sub midpoint { ); } +sub reverse { + my $self = shift; + @$self = reverse @$self; +} + 1; diff --git a/lib/Slic3r/Perimeter.pm b/lib/Slic3r/Perimeter.pm index 1d8d3a8b3..0c31f18bd 100644 --- a/lib/Slic3r/Perimeter.pm +++ b/lib/Slic3r/Perimeter.pm @@ -3,12 +3,9 @@ use Moo; use Math::Clipper ':all'; use Math::ConvexHull 1.0.4 qw(convex_hull); -use Slic3r::Geometry qw(shortest_path); +use Slic3r::Geometry qw(X Y shortest_path); use XXX; -use constant X => 0; -use constant Y => 1; - sub make_perimeter { my $self = shift; my ($layer) = @_; diff --git a/lib/Slic3r/Print.pm b/lib/Slic3r/Print.pm index 4873faa1d..82dfe585c 100644 --- a/lib/Slic3r/Print.pm +++ b/lib/Slic3r/Print.pm @@ -2,12 +2,10 @@ package Slic3r::Print; use Moo; use Math::Clipper ':all'; +use Slic3r::Geometry qw(X Y); use Slic3r::Geometry::Clipper qw(explode_expolygons safety_offset diff_ex intersection_ex); use XXX; -use constant X => 0; -use constant Y => 1; - has 'x_length' => ( is => 'ro', required => 1, diff --git a/lib/Slic3r/STL.pm b/lib/Slic3r/STL.pm index d0fd9e0e4..107ce2425 100644 --- a/lib/Slic3r/STL.pm +++ b/lib/Slic3r/STL.pm @@ -2,12 +2,9 @@ package Slic3r::STL; use Moo; use Math::Clipper qw(integerize_coordinate_sets is_counter_clockwise); -use Slic3r::Geometry qw(three_points_aligned longest_segment); +use Slic3r::Geometry qw(X Y Z three_points_aligned longest_segment); use XXX; -use constant X => 0; -use constant Y => 1; -use constant Z => 2; use constant MIN => 0; use constant MAX => 1; diff --git a/t/collinear.t b/t/collinear.t new file mode 100644 index 000000000..e09d02224 --- /dev/null +++ b/t/collinear.t @@ -0,0 +1,91 @@ +use Test::More; +use strict; +use warnings; + +plan tests => 11; + +BEGIN { + use FindBin; + use lib "$FindBin::Bin/../lib"; +} + +use Slic3r; +use Slic3r::Geometry qw(collinear); + +#========================================================== + +{ + my @lines = ( + [ [0,4], [4,2] ], + [ [2,3], [8,0] ], + [ [6,1], [8,0] ], + ); + is collinear($lines[0], $lines[1]), 1, 'collinear'; + is collinear($lines[1], $lines[2]), 1, 'collinear'; + is collinear($lines[0], $lines[2]), 1, 'collinear'; +} + +#========================================================== + +{ + # horizontal + my @lines = ( + [ [0,1], [5,1] ], + [ [2,1], [8,1] ], + ); + is collinear($lines[0], $lines[1]), 1, 'collinear'; +} + +#========================================================== + +{ + # vertical + my @lines = ( + [ [1,0], [1,5] ], + [ [1,2], [1,8] ], + ); + is collinear($lines[0], $lines[1]), 1, 'collinear'; +} + +#========================================================== + +{ + # non overlapping + my @lines = ( + [ [0,1], [5,1] ], + [ [7,1], [10,1] ], + ); + is collinear($lines[0], $lines[1], 1), 0, 'non overlapping'; + is collinear($lines[0], $lines[1], 0), 1, 'overlapping'; +} + +#========================================================== + +{ + # with one common point + my @lines = ( + [ [0,4], [4,2] ], + [ [4,2], [8,0] ], + ); + is collinear($lines[0], $lines[1], 1), 1, 'one common point'; + is collinear($lines[0], $lines[1], 0), 1, 'one common point'; +} + +#========================================================== + +{ + # not collinear + my @lines = ( + [ [290000000,690525600], [285163380,684761540] ], + [ [285163380,684761540], [193267599,575244400] ], + ); + is collinear($lines[0], $lines[1], 0), 0, 'not collinear'; + is collinear($lines[0], $lines[1], 1), 0, 'not collinear'; + + use Slic3r::SVG; + Slic3r::SVG::output(undef, "collinear.svg", + lines => \@lines, + ); +} + +#==========================================================