diff --git a/Build.PL b/Build.PL index 01314e924..951180135 100644 --- a/Build.PL +++ b/Build.PL @@ -11,6 +11,7 @@ my $build = Module::Build->new( 'Getopt::Long' => '0', 'Math::Clipper' => '1.02', 'Math::ConvexHull' => '1.0.4', + 'Math::Geometry::Voronoi' => '1.3', 'Math::PlanePath' => '53', 'Moo' => '0', 'Time::HiRes' => '0', diff --git a/README.markdown b/README.markdown index ba9b41347..094ece767 100644 --- a/README.markdown +++ b/README.markdown @@ -55,7 +55,6 @@ Roadmap includes the following goals: Sure, it's very usable. Remember that: -* it doesn't currently support single-walled parts (such as thin calibration objects); * it doesn't generate support material; * it only works well with manifold models (check them with Meshlab or Netfabb or http://cloud.netfabb.com/). diff --git a/lib/Slic3r.pm b/lib/Slic3r.pm index f0c6813af..246dcb0f7 100644 --- a/lib/Slic3r.pm +++ b/lib/Slic3r.pm @@ -3,7 +3,7 @@ package Slic3r; use strict; use warnings; -our $VERSION = "0.5.8-beta"; +our $VERSION = "0.6.0-beta"; our $debug = 0; sub debugf { diff --git a/lib/Slic3r/ExPolygon.pm b/lib/Slic3r/ExPolygon.pm index eddeac8e3..6dd78cf88 100644 --- a/lib/Slic3r/ExPolygon.pm +++ b/lib/Slic3r/ExPolygon.pm @@ -4,6 +4,7 @@ use warnings; # an ExPolygon is a polygon with holes +use Math::Geometry::Voronoi; use Slic3r::Geometry qw(point_in_polygon X Y A B); use Slic3r::Geometry::Clipper qw(union_ex JT_MITER); @@ -24,6 +25,11 @@ sub new { $self; } +sub clone { + my $self = shift; + return (ref $self)->new(map $_->clone, @$self); +} + sub contour { my $self = shift; return $self->[0]; @@ -147,4 +153,102 @@ sub area { return $area; } +# this method only works for expolygons having only a contour or +# a contour and a hole, and not being thicker than the supplied +# width. it returns a polyline or a polygon +sub medial_axis { + my $self = shift; + my ($width) = @_; + + my @self_lines = map $_->lines, @$self; + my $expolygon = $self->clone; + my @points = (); + foreach my $polygon (@$expolygon) { + Slic3r::Geometry::polyline_remove_short_segments($polygon, $width / 2); + + # subdivide polygon segments so that we don't have anyone of them + # being longer than $width / 2 + $polygon->subdivide($width/2); + + push @points, @$polygon; + } + + my $voronoi = Math::Geometry::Voronoi->new(points => \@points); + $voronoi->compute; + + my @skeleton_lines = (); + + my $vertices = $voronoi->vertices; + my $edges = $voronoi->edges; + foreach my $edge (@$edges) { + # ignore lines going to infinite + next if $edge->[1] == -1 || $edge->[2] == -1; + + my ($a, $b); + $a = $vertices->[$edge->[1]]; + $b = $vertices->[$edge->[2]]; + + next if !$self->encloses_point($a) || !$self->encloses_point($b); + + push @skeleton_lines, [$edge->[1], $edge->[2]]; + } + + # remove leafs (lines not connected to other lines at one of their endpoints) + { + my %pointmap = (); + $pointmap{$_}++ for map @$_, @skeleton_lines; + @skeleton_lines = grep { + $pointmap{$_->[A]} >= 2 && $pointmap{$_->[B]} >= 2 + } @skeleton_lines; + } + + # now build a single polyline + my $polyline = []; + { + my %pointmap = (); + foreach my $line (@skeleton_lines) { + foreach my $point_id (@$line) { + $pointmap{$point_id} ||= []; + push @{$pointmap{$point_id}}, $line; + } + } + + # start from a point having only one line + foreach my $point_id (keys %pointmap) { + if (@{$pointmap{$point_id}} == 1) { + push @$polyline, grep $_ ne $point_id, map @$_, shift @{$pointmap{$point_id}}; + last; + } + } + + # if no such point is found, pick a random one + push @$polyline, shift @{ +(values %pointmap)[0][0] } if !@$polyline; + + my %visited_lines = (); + while (1) { + my $last_point_id = $polyline->[-1]; + + shift @{ $pointmap{$last_point_id} } + while @{ $pointmap{$last_point_id} } && $visited_lines{$pointmap{$last_point_id}[0]}; + my $next_line = shift @{ $pointmap{$last_point_id} } or last; + $visited_lines{$next_line} = 1; + push @$polyline, grep $_ ne $last_point_id, @$next_line; + } + } + + # now replace point indexes with coordinates + @$polyline = map $vertices->[$_], @$polyline; + + # cleanup + Slic3r::Geometry::polyline_remove_short_segments($polyline, $width / 2); + @$polyline = Slic3r::Geometry::Douglas_Peucker($polyline, $width / 100); + Slic3r::Geometry::polyline_remove_parallel_continuous_edges($polyline); + + if (Slic3r::Geometry::same_point($polyline->[0], $polyline->[-1])) { + return Slic3r::Polygon->new(@$polyline[0..$#$polyline-1]); + } else { + return Slic3r::Polyline->cast($polyline); + } +} + 1; diff --git a/lib/Slic3r/Extruder.pm b/lib/Slic3r/Extruder.pm index 8bd438312..35f6a3d47 100644 --- a/lib/Slic3r/Extruder.pm +++ b/lib/Slic3r/Extruder.pm @@ -66,6 +66,14 @@ sub change_layer { return $gcode; } +sub extrude { + my $self = shift; + + return $_[0]->isa('Slic3r::ExtrusionLoop') + ? $self->extrude_loop(@_) + : $self->extrude_path(@_); +} + sub extrude_loop { my $self = shift; my ($loop, $description) = @_; @@ -80,10 +88,10 @@ sub extrude_loop { $extrusion_path->clip_end(scale $Slic3r::nozzle_diameter / 2); # extrude along the path - return $self->extrude($extrusion_path, $description); + return $self->extrude_path($extrusion_path, $description); } -sub extrude { +sub extrude_path { my $self = shift; my ($path, $description, $recursive) = @_; @@ -92,7 +100,7 @@ sub extrude { # detect arcs if ($Slic3r::gcode_arcs && !$recursive) { my $gcode = ""; - $gcode .= $self->extrude($_, $description, 1) for $path->detect_arcs; + $gcode .= $self->extrude_path($_, $description, 1) for $path->detect_arcs; return $gcode; } diff --git a/lib/Slic3r/ExtrusionPath.pm b/lib/Slic3r/ExtrusionPath.pm index fc6919238..592a61638 100644 --- a/lib/Slic3r/ExtrusionPath.pm +++ b/lib/Slic3r/ExtrusionPath.pm @@ -45,6 +45,8 @@ sub reverse { @{$self->points} = reverse @{$self->points}; } +sub is_printable { 1 } + sub split_at_acute_angles { my $self = shift; diff --git a/lib/Slic3r/Geometry.pm b/lib/Slic3r/Geometry.pm index bdb9efa03..6652a49d3 100644 --- a/lib/Slic3r/Geometry.pm +++ b/lib/Slic3r/Geometry.pm @@ -18,7 +18,8 @@ our @EXPORT_OK = qw( polyline_remove_parallel_continuous_edges polyline_remove_acute_vertices polygon_remove_acute_vertices polygon_remove_parallel_continuous_edges shortest_path collinear scale unscale merge_collinear_lines - rad2deg_dir bounding_box_center + rad2deg_dir bounding_box_center line_intersects_any + polyline_remove_short_segments ); use Slic3r::Geometry::DouglasPeucker qw(Douglas_Peucker); @@ -66,7 +67,7 @@ sub line_direction { sub lines_parallel { my ($line1, $line2) = @_; - return abs(line_atan($line1) - line_atan($line2)) < $parallel_degrees_limit; + return abs(line_direction($line1) - line_direction($line2)) < $parallel_degrees_limit; } sub three_points_aligned { @@ -438,6 +439,14 @@ sub polygon_points_visibility { return 1; } +sub line_intersects_any { + my ($line, $lines) = @_; + for (@$lines) { + return 1 if line_intersection($line, $_, 1); + } + return 0; +} + sub line_intersection { my ($line1, $line2, $require_crossing) = @_; $require_crossing ||= 0; @@ -676,6 +685,17 @@ sub polygon_remove_acute_vertices { return polyline_remove_acute_vertices($points, 1); } +sub polyline_remove_short_segments { + my ($points, $min_length, $isPolygon) = @_; + for (my $i = $isPolygon ? 0 : 1; $i < $#$points; $i++) { + if (distance_between_points($points->[$i-1], $points->[$i]) < $min_length) { + # we can remove $points->[$i] + splice @$points, $i, 1; + $i--; + } + } +} + # accepts an arrayref; each item should be an arrayref whose first # item is the point to be used for the shortest path, and the second # one is the value to be returned in output (if the second item diff --git a/lib/Slic3r/Layer.pm b/lib/Slic3r/Layer.pm index eab34e029..78ef1c9b8 100644 --- a/lib/Slic3r/Layer.pm +++ b/lib/Slic3r/Layer.pm @@ -30,6 +30,10 @@ has 'slices' => ( default => sub { [] }, ); +# collection of polygons or polylines representing thin walls contained +# in the original geometry +has 'thin_walls' => (is => 'rw', default => sub { [] }); + # collection of surfaces generated by offsetting the innermost perimeter(s) # they represent boundaries of areas to fill has 'fill_boundaries' => ( @@ -160,6 +164,19 @@ sub make_surfaces { ($_, surface_type => 'internal'), $surface->expolygon->offset_ex(-$distance); } + + # now detect thin walls by re-outgrowing offsetted surfaces and subtracting + # them from the original slices + my $outgrown = Math::Clipper::offset([ map $_->p, @{$self->slices} ], $distance); + my $diff = diff_ex( + [ map $_->p, @surfaces ], + $outgrown, + ); + + push @{$self->thin_walls}, + grep $_, + map $_->medial_axis(scale $Slic3r::flow_width), + @$diff; } if (0) { diff --git a/lib/Slic3r/Perimeter.pm b/lib/Slic3r/Perimeter.pm index ab58e81d8..ec24c0285 100644 --- a/lib/Slic3r/Perimeter.pm +++ b/lib/Slic3r/Perimeter.pm @@ -79,6 +79,15 @@ sub make_perimeter { for (@{ $layer->perimeters }) { $_->role('small-perimeter') if $_->polygon->area < $Slic3r::small_perimeter_area; } + + # add thin walls as perimeters + for (@{ $layer->thin_walls }) { + if ($_->isa('Slic3r::Polygon')) { + push @{ $layer->perimeters }, Slic3r::ExtrusionLoop->cast($_, role => 'perimeter'); + } else { + push @{ $layer->perimeters }, Slic3r::ExtrusionPath->cast($_->points, role => 'perimeter'); + } + } } 1; diff --git a/lib/Slic3r/Point.pm b/lib/Slic3r/Point.pm index a9bc3ea99..f8d3af071 100644 --- a/lib/Slic3r/Point.pm +++ b/lib/Slic3r/Point.pm @@ -20,6 +20,11 @@ sub new { return $self; } +sub clone { + my $self = shift; + return (ref $self)->new(@$self); +} + sub cast { my $class = shift; if (ref $_[0] eq 'Slic3r::Point') { diff --git a/lib/Slic3r/Polygon.pm b/lib/Slic3r/Polygon.pm index 375037ffc..441b11056 100644 --- a/lib/Slic3r/Polygon.pm +++ b/lib/Slic3r/Polygon.pm @@ -26,6 +26,11 @@ sub new { $self; } +sub clone { + my $self = shift; + return (ref $self)->new(map $_->clone, @$self); +} + # legacy method, to be removed when we ditch Slic3r::Polyline::Closed sub closed_polyline { my $self = shift; @@ -87,4 +92,26 @@ sub offset { return @$offsets; } +# this method subdivides the polygon segments to that no one of them +# is longer than the length provided +sub subdivide { + my $self = shift; + my ($max_length) = @_; + + for (my $i = 0; $i <= $#$self; $i++) { + my $len = Slic3r::Geometry::line_length([ $self->[$i-1], $self->[$i] ]); + my $num_points = int($len / $max_length) - 1; + $num_points++ if $len % $max_length; + next unless $num_points; + + # $num_points is the number of points to add between $i-1 and $i + my $spacing = $len / ($num_points + 1); + my @new_points = map Slic3r::Geometry::point_along_segment($self->[$i-1], $self->[$i], $spacing * $_), + 1..$num_points; + + splice @$self, $i, 0, @new_points; + $i += @new_points; + } +} + 1; \ No newline at end of file diff --git a/lib/Slic3r/Print.pm b/lib/Slic3r/Print.pm index ea21fd570..54dd602fc 100644 --- a/lib/Slic3r/Print.pm +++ b/lib/Slic3r/Print.pm @@ -131,7 +131,7 @@ sub new_from_mesh { } # remove empty layers from bottom - while (@{$print->layers} && !@{$print->layers->[0]->slices}) { + while (@{$print->layers} && !@{$print->layers->[0]->slices} && !@{$print->layers->[0]->thin_walls}) { shift @{$print->layers}; for (my $i = 0; $i <= $#{$print->layers}; $i++) { $print->layers->[$i]->id($i); @@ -327,11 +327,10 @@ sub extrude_skirt { return unless $Slic3r::skirts > 0; # collect points from all layers contained in skirt height - my @points = (); my $skirt_height = $Slic3r::skirt_height; $skirt_height = $self->layer_count if $skirt_height > $self->layer_count; my @layers = map $self->layer($_), 0..($skirt_height-1); - push @points, map @$_, map $_->p, map @{ $_->slices }, @layers; + my @points = map @$_, map $_->p, map @{ $_->slices }, @layers; return if !@points; # find out convex hull @@ -484,11 +483,11 @@ sub export_gcode { printf $fh $extruder->extrude_loop($_, 'skirt') for @{ $layer->skirts }; # extrude perimeters - printf $fh $extruder->extrude_loop($_, 'perimeter') for @{ $layer->perimeters }; + printf $fh $extruder->extrude($_, 'perimeter') for @{ $layer->perimeters }; # extrude fills for my $fill (@{ $layer->fills }) { - printf $fh $extruder->extrude($_, 'fill') + printf $fh $extruder->extrude_path($_, 'fill') for $fill->shortest_path($extruder->last_pos); } }