package Slic3r::Fill::3DHoneycomb; use Moo; extends 'Slic3r::Fill::Base'; use POSIX qw(ceil fmod); use Slic3r::Geometry qw(scale scaled_epsilon); use Slic3r::Geometry::Clipper qw(intersection_pl); sub fill_surface { my ($self, $surface, %params) = @_; # use bridge flow since most of this pattern hangs in air my $flow = Slic3r::Flow->new( width => $params{flow}->width, height => $params{flow}->height, nozzle_diameter => $params{flow}->nozzle_diameter, bridge => 1, ); my $expolygon = $surface->expolygon; my $bb = $expolygon->bounding_box; my $size = $bb->size; my $distance = $flow->scaled_spacing / $params{density}; # align bounding box to a multiple of our honeycomb grid { my $min = $bb->min_point; $min->translate( -($bb->x_min % $distance), -($bb->y_min % $distance), ); $bb->merge_point($min); } # generate pattern my @polylines = map Slic3r::Polyline->new(@$_), makeGrid( scale($self->z), $distance, ceil($size->x / $distance), ceil($size->y / $distance), #// (($self->layer_id / $surface->thickness_layers) % 2) + 1, ); # move pattern in place $_->translate($bb->x_min, $bb->y_min) for @polylines; # clip pattern to boundaries @polylines = @{intersection_pl(\@polylines, \@$expolygon)}; # connect lines unless ($params{dont_connect} || !@polylines) { # prevent calling leftmost_point() on empty collections my ($expolygon_off) = @{$expolygon->offset_ex(scaled_epsilon)}; my $collection = Slic3r::Polyline::Collection->new(@polylines); @polylines = (); foreach my $polyline (@{$collection->chained_path_from($collection->leftmost_point, 0)}) { # try to append this polyline to previous one if any if (@polylines) { my $line = Slic3r::Line->new($polylines[-1]->last_point, $polyline->first_point); if ($line->length <= 1.5*$distance && $expolygon_off->contains_line($line)) { $polylines[-1]->append_polyline($polyline); next; } } # make a clone before $collection goes out of scope push @polylines, $polyline->clone; } } # TODO: return ExtrusionLoop objects to get better chained paths return { flow => $flow}, @polylines; } =head1 DESCRIPTION Creates a contiguous sequence of points at a specified height that make up a horizontal slice of the edges of a space filling truncated octahedron tesselation. The octahedrons are oriented so that the square faces are in the horizontal plane with edges parallel to the X and Y axes. Credits: David Eccles (gringer). =head2 makeGrid(z, gridSize, gridWidth, gridHeight, curveType) Generate a set of curves (array of array of 2d points) that describe a horizontal slice of a truncated regular octahedron with a specified grid square size. =cut sub makeGrid { my ($z, $gridSize, $gridWidth, $gridHeight, $curveType) = @_; my $scaleFactor = $gridSize; my $normalisedZ = $z / $scaleFactor; my @points = makeNormalisedGrid($normalisedZ, $gridWidth, $gridHeight, $curveType); foreach my $lineRef (@points) { foreach my $pointRef (@$lineRef) { $pointRef->[0] *= $scaleFactor; $pointRef->[1] *= $scaleFactor; } } return @points; } =head1 FUNCTIONS =cut =head2 colinearPoints(offset, gridLength) Generate an array of points that are in the same direction as the basic printing line (i.e. Y points for columns, X points for rows) Note: a negative offset only causes a change in the perpendicular direction =cut sub colinearPoints { my ($offset, $baseLocation, $gridLength) = @_; my @points = (); push @points, $baseLocation - abs($offset/2); for (my $i = 0; $i < $gridLength; $i++) { push @points, $baseLocation + $i + abs($offset/2); push @points, $baseLocation + ($i+1) - abs($offset/2); } push @points, $baseLocation + $gridLength + abs($offset/2); return @points; } =head2 colinearPoints(offset, baseLocation, gridLength) Generate an array of points for the dimension that is perpendicular to the basic printing line (i.e. X points for columns, Y points for rows) =cut sub perpendPoints { my ($offset, $baseLocation, $gridLength) = @_; my @points = (); my $side = 2*(($baseLocation) % 2) - 1; push @points, $baseLocation - $offset/2 * $side; for (my $i = 0; $i < $gridLength; $i++) { $side = 2*(($i+$baseLocation) % 2) - 1; push @points, $baseLocation + $offset/2 * $side; push @points, $baseLocation + $offset/2 * $side; } push @points, $baseLocation - $offset/2 * $side; return @points; } =head2 trim(pointArrayRef, minX, minY, maxX, maxY) Trims an array of points to specified rectangular limits. Point components that are outside these limits are set to the limits. =cut sub trim { my ($pointArrayRef, $minX, $minY, $maxX, $maxY) = @_; foreach (@$pointArrayRef) { $_->[0] = ($_->[0] < $minX) ? $minX : (($_->[0] > $maxX) ? $maxX : $_->[0]); $_->[1] = ($_->[1] < $minY) ? $minY : (($_->[1] > $maxY) ? $maxY : $_->[1]); } } =head2 makeNormalisedGrid(z, gridWidth, gridHeight, curveType) Generate a set of curves (array of array of 2d points) that describe a horizontal slice of a truncated regular octahedron with edge length 1. curveType specifies which lines to print, 1 for vertical lines (columns), 2 for horizontal lines (rows), and 3 for both. =cut sub makeNormalisedGrid { my ($z, $gridWidth, $gridHeight, $curveType) = @_; ## offset required to create a regular octagram my $octagramGap = 3/4; # sawtooth wave function for range f($z) = [-$octagramGap .. $octagramGap] my $wave = abs(fmod($z, 2) - 1)*2 - 1; my $offset = $wave * $octagramGap; my @points = (); if (($curveType & 1) != 0) { for (my $x = 0; $x <= $gridWidth; $x++) { my @xPoints = perpendPoints($offset, $x, $gridHeight); my @yPoints = colinearPoints($offset, 0, $gridHeight); # This is essentially @newPoints = zip(@xPoints, @yPoints) my @newPoints = map [ $xPoints[$_], $yPoints[$_] ], 0..$#xPoints; # trim points to grid edges #trim(\@newPoints, 0, 0, $gridWidth, $gridHeight); if ($x % 2 == 0){ push @points, [ @newPoints ]; } else { push @points, [ reverse @newPoints ]; } } } if (($curveType & 2) != 0) { for (my $y = 0; $y <= $gridHeight; $y++) { my @xPoints = colinearPoints($offset, 0, $gridWidth); my @yPoints = perpendPoints($offset, $y, $gridWidth); my @newPoints = map [ $xPoints[$_], $yPoints[$_] ], 0..$#xPoints; # trim points to grid edges #trim(\@newPoints, 0, 0, $gridWidth, $gridHeight); if ($y % 2 == 0) { push @points, [ @newPoints ]; } else { push @points, [ reverse @newPoints ]; } } } return @points; } 1;