2011-10-15 09:36:05 +00:00
|
|
|
package Slic3r::ExPolygon;
|
|
|
|
use strict;
|
|
|
|
use warnings;
|
|
|
|
|
|
|
|
# an ExPolygon is a polygon with holes
|
|
|
|
|
2011-12-30 16:17:37 +00:00
|
|
|
use Math::Geometry::Voronoi;
|
2012-02-25 15:14:59 +00:00
|
|
|
use Slic3r::Geometry qw(X Y A B point_in_polygon);
|
2011-11-19 15:08:00 +00:00
|
|
|
use Slic3r::Geometry::Clipper qw(union_ex JT_MITER);
|
2011-10-15 09:36:05 +00:00
|
|
|
|
|
|
|
# the constructor accepts an array of polygons
|
|
|
|
# or a Math::Clipper ExPolygon (hashref)
|
|
|
|
sub new {
|
|
|
|
my $class = shift;
|
|
|
|
my $self;
|
|
|
|
if (@_ == 1 && ref $_[0] eq 'HASH') {
|
|
|
|
$self = [
|
|
|
|
Slic3r::Polygon->new($_[0]{outer}),
|
|
|
|
map Slic3r::Polygon->new($_), @{$_[0]{holes}},
|
|
|
|
];
|
|
|
|
} else {
|
2011-11-11 09:21:48 +00:00
|
|
|
$self = [ map Slic3r::Polygon->new($_), @_ ];
|
2011-10-15 09:36:05 +00:00
|
|
|
}
|
|
|
|
bless $self, $class;
|
|
|
|
$self;
|
|
|
|
}
|
|
|
|
|
2011-12-30 16:17:37 +00:00
|
|
|
sub clone {
|
|
|
|
my $self = shift;
|
|
|
|
return (ref $self)->new(map $_->clone, @$self);
|
|
|
|
}
|
|
|
|
|
2011-10-15 09:36:05 +00:00
|
|
|
sub contour {
|
|
|
|
my $self = shift;
|
|
|
|
return $self->[0];
|
|
|
|
}
|
|
|
|
|
|
|
|
sub holes {
|
|
|
|
my $self = shift;
|
|
|
|
return @$self[1..$#$self];
|
|
|
|
}
|
|
|
|
|
2011-12-30 18:59:51 +00:00
|
|
|
sub lines {
|
|
|
|
my $self = shift;
|
|
|
|
return map $_->lines, @$self;
|
|
|
|
}
|
|
|
|
|
2011-10-15 09:36:05 +00:00
|
|
|
sub clipper_expolygon {
|
|
|
|
my $self = shift;
|
|
|
|
return {
|
|
|
|
outer => $self->contour,
|
|
|
|
holes => [ $self->holes ],
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
sub offset {
|
|
|
|
my $self = shift;
|
|
|
|
my ($distance, $scale, $joinType, $miterLimit) = @_;
|
|
|
|
$scale ||= $Slic3r::resolution * 1000000;
|
|
|
|
$joinType = JT_MITER if !defined $joinType;
|
|
|
|
$miterLimit ||= 2;
|
|
|
|
|
|
|
|
my $offsets = Math::Clipper::offset($self, $distance, $scale, $joinType, $miterLimit);
|
2011-11-16 15:35:20 +00:00
|
|
|
return @$offsets;
|
|
|
|
}
|
|
|
|
|
2011-11-20 21:09:59 +00:00
|
|
|
sub safety_offset {
|
|
|
|
my $self = shift;
|
2011-12-01 18:40:45 +00:00
|
|
|
|
|
|
|
# we're offsetting contour and holes separately
|
|
|
|
# because Clipper doesn't return polygons in the same order as
|
|
|
|
# we feed them to it
|
|
|
|
|
2011-11-20 21:09:59 +00:00
|
|
|
return (ref $self)->new(
|
2011-12-01 18:40:45 +00:00
|
|
|
$self->contour->safety_offset,
|
|
|
|
@{ Slic3r::Geometry::Clipper::safety_offset([$self->holes]) },
|
2011-11-20 21:09:59 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2011-11-16 15:35:20 +00:00
|
|
|
sub offset_ex {
|
|
|
|
my $self = shift;
|
|
|
|
my @offsets = $self->offset(@_);
|
2011-10-15 09:36:05 +00:00
|
|
|
|
|
|
|
# apply holes to the right contours
|
2011-11-16 15:35:20 +00:00
|
|
|
return @{ union_ex(\@offsets) };
|
2011-11-13 17:14:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
sub encloses_point {
|
|
|
|
my $self = shift;
|
|
|
|
my ($point) = @_;
|
|
|
|
return $self->contour->encloses_point($point)
|
|
|
|
&& (!grep($_->encloses_point($point), $self->holes)
|
|
|
|
|| grep($_->point_on_segment($point), $self->holes));
|
|
|
|
}
|
|
|
|
|
|
|
|
sub point_on_segment {
|
|
|
|
my $self = shift;
|
|
|
|
my ($point) = @_;
|
|
|
|
for (@$self) {
|
|
|
|
my $line = $_->point_on_segment($point);
|
|
|
|
return $line if $line;
|
|
|
|
}
|
|
|
|
return undef;
|
|
|
|
}
|
|
|
|
|
|
|
|
sub bounding_box {
|
|
|
|
my $self = shift;
|
|
|
|
return Slic3r::Geometry::bounding_box($self->contour);
|
|
|
|
}
|
|
|
|
|
2012-02-25 13:46:21 +00:00
|
|
|
sub bounding_box_polygon {
|
|
|
|
my $self = shift;
|
|
|
|
my @bb = $self->bounding_box;
|
|
|
|
return Slic3r::Polygon->new([
|
|
|
|
[ $bb[0], $bb[1] ],
|
|
|
|
[ $bb[2], $bb[1] ],
|
|
|
|
[ $bb[2], $bb[3] ],
|
|
|
|
[ $bb[0], $bb[3] ],
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
2011-11-13 17:14:02 +00:00
|
|
|
sub clip_line {
|
|
|
|
my $self = shift;
|
2012-02-25 21:15:34 +00:00
|
|
|
my ($line) = @_; # line must be a Slic3r::Line object
|
2011-11-13 17:14:02 +00:00
|
|
|
|
|
|
|
my @intersections = grep $_, map $_->intersection($line, 1), map $_->lines, @$self;
|
|
|
|
my @dir = (
|
|
|
|
$line->[B][X] <=> $line->[A][X],
|
|
|
|
$line->[B][Y] <=> $line->[A][Y],
|
|
|
|
);
|
|
|
|
|
|
|
|
@intersections = sort {
|
|
|
|
(($a->[X] <=> $b->[X]) == $dir[X]) && (($a->[Y] <=> $b->[Y]) == $dir[Y]) ? 1 : -1
|
|
|
|
} @intersections, @$line;
|
|
|
|
|
|
|
|
shift @intersections if $intersections[0]->coincides_with($intersections[1]);
|
|
|
|
pop @intersections if $intersections[-1]->coincides_with($intersections[-2]);
|
|
|
|
|
|
|
|
shift @intersections
|
|
|
|
if !$self->encloses_point($intersections[0])
|
|
|
|
&& !$self->point_on_segment($intersections[0]);
|
|
|
|
|
|
|
|
my @lines = ();
|
|
|
|
while (@intersections) {
|
|
|
|
# skip tangent points
|
|
|
|
my @points = splice @intersections, 0, 2;
|
|
|
|
next if !$points[1];
|
|
|
|
next if $points[0]->coincides_with($points[1]);
|
|
|
|
push @lines, [ @points ];
|
|
|
|
}
|
|
|
|
return [@lines];
|
|
|
|
}
|
|
|
|
|
2012-02-25 13:46:21 +00:00
|
|
|
sub simplify {
|
|
|
|
my $self = shift;
|
|
|
|
$_->simplify(@_) for @$self;
|
|
|
|
}
|
|
|
|
|
2011-11-13 17:14:02 +00:00
|
|
|
sub translate {
|
|
|
|
my $self = shift;
|
|
|
|
$_->translate(@_) for @$self;
|
|
|
|
}
|
|
|
|
|
|
|
|
sub rotate {
|
|
|
|
my $self = shift;
|
|
|
|
$_->rotate(@_) for @$self;
|
2011-10-15 09:36:05 +00:00
|
|
|
}
|
|
|
|
|
2011-12-24 10:01:28 +00:00
|
|
|
sub area {
|
|
|
|
my $self = shift;
|
|
|
|
my $area = $self->contour->area;
|
|
|
|
$area -= $_->area for $self->holes;
|
|
|
|
return $area;
|
|
|
|
}
|
|
|
|
|
2011-12-30 16:17:37 +00:00
|
|
|
# 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;
|
|
|
|
}
|
2011-12-30 17:58:27 +00:00
|
|
|
return undef if !@skeleton_lines;
|
2011-12-30 16:17:37 +00:00
|
|
|
|
2011-12-30 18:59:51 +00:00
|
|
|
return undef if !@skeleton_lines;
|
|
|
|
|
2011-12-30 16:17:37 +00:00
|
|
|
# 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);
|
2012-02-25 15:27:33 +00:00
|
|
|
$polyline = Slic3r::Geometry::douglas_peucker($polyline, $width / 7);
|
2011-12-30 16:17:37 +00:00
|
|
|
Slic3r::Geometry::polyline_remove_parallel_continuous_edges($polyline);
|
|
|
|
|
|
|
|
if (Slic3r::Geometry::same_point($polyline->[0], $polyline->[-1])) {
|
2012-02-19 16:11:18 +00:00
|
|
|
return undef if @$polyline == 2;
|
2011-12-30 16:17:37 +00:00
|
|
|
return Slic3r::Polygon->new(@$polyline[0..$#$polyline-1]);
|
|
|
|
} else {
|
2011-12-30 18:59:51 +00:00
|
|
|
return Slic3r::Polyline->new($polyline);
|
2011-12-30 16:17:37 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2011-10-15 09:36:05 +00:00
|
|
|
1;
|