ExPolygon.pm 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
  1. package Slic3r::ExPolygon;
  2. use strict;
  3. use warnings;
  4. # an ExPolygon is a polygon with holes
  5. use Boost::Geometry::Utils;
  6. use List::Util qw(first);
  7. use Math::Geometry::Voronoi;
  8. use Slic3r::Geometry qw(X Y A B point_in_polygon epsilon scaled_epsilon);
  9. use Slic3r::Geometry::Clipper qw(union_ex diff_pl);
  10. sub wkt {
  11. my $self = shift;
  12. return sprintf "POLYGON(%s)",
  13. join ',', map "($_)", map { join ',', map "$_->[0] $_->[1]", @$_ } @$self;
  14. }
  15. sub dump_perl {
  16. my $self = shift;
  17. return sprintf "[%s]",
  18. join ',', map "[$_]", map { join ',', map "[$_->[0],$_->[1]]", @$_ } @$self;
  19. }
  20. sub offset {
  21. my $self = shift;
  22. return Slic3r::Geometry::Clipper::offset(\@$self, @_);
  23. }
  24. sub offset_ex {
  25. my $self = shift;
  26. return Slic3r::Geometry::Clipper::offset_ex(\@$self, @_);
  27. }
  28. sub noncollapsing_offset_ex {
  29. my $self = shift;
  30. my ($distance, @params) = @_;
  31. return $self->offset_ex($distance + 1, @params);
  32. }
  33. sub encloses_point {
  34. my $self = shift;
  35. my ($point) = @_;
  36. return Boost::Geometry::Utils::point_covered_by_polygon($point->pp, $self->pp);
  37. }
  38. # A version of encloses_point for use when hole borders do not matter.
  39. # Useful because point_on_segment is probably slower (this was true
  40. # before the switch to Boost.Geometry, not sure about now)
  41. sub encloses_point_quick {
  42. my $self = shift;
  43. my ($point) = @_;
  44. return Boost::Geometry::Utils::point_within_polygon($point->pp, $self->pp);
  45. }
  46. sub encloses_line {
  47. my $self = shift;
  48. my ($line, $tolerance) = @_;
  49. my $clip = $self->clip_line($line);
  50. if (!defined $tolerance) {
  51. # optimization
  52. return @$clip == 1 && $clip->[0]->coincides_with($line);
  53. } else {
  54. return @$clip == 1 && abs($clip->[0]->length - $line->length) < $tolerance;
  55. }
  56. }
  57. sub bounding_box {
  58. my $self = shift;
  59. return $self->contour->bounding_box;
  60. }
  61. sub clip_line {
  62. my $self = shift;
  63. my ($line) = @_; # line must be a Slic3r::Line object
  64. return [
  65. map Slic3r::Line->new(@$_),
  66. @{Slic3r::Geometry::Clipper::intersection_pl([ Slic3r::Polyline->new(@$line) ], \@$self)}
  67. ];
  68. }
  69. sub simplify_as_polygons {
  70. my $self = shift;
  71. my ($tolerance) = @_;
  72. # it would be nice to have a multilinestring_simplify method in B::G::U
  73. return @{Slic3r::Geometry::Clipper::simplify_polygons(
  74. [ map Boost::Geometry::Utils::linestring_simplify($_, $tolerance), @{$self->pp} ],
  75. )};
  76. }
  77. sub simplify {
  78. my $self = shift;
  79. my ($tolerance) = @_;
  80. return @{ Slic3r::Geometry::Clipper::union_ex([ $self->simplify_as_polygons($tolerance) ]) };
  81. }
  82. # this method only works for expolygons having only a contour or
  83. # a contour and a hole, and not being thicker than the supplied
  84. # width. it returns a polyline or a polygon
  85. sub medial_axis {
  86. my ($self, $width) = @_;
  87. return $self->_medial_axis_voronoi($width);
  88. }
  89. sub _medial_axis_clip {
  90. my ($self, $width) = @_;
  91. my $grow = sub {
  92. my ($line, $distance) = @_;
  93. my $line_clone = $line->clone;
  94. $line_clone->clip_start(scaled_epsilon);
  95. return () if !$line_clone->is_valid;
  96. $line_clone->clip_end(scaled_epsilon);
  97. return () if !$line_clone->is_valid;
  98. my ($a, $b) = @$line_clone;
  99. my $dx = $a->x - $b->x;
  100. my $dy = $a->y - $b->y; #-
  101. my $dist = sqrt($dx*$dx + $dy*$dy);
  102. $dx /= $dist;
  103. $dy /= $dist;
  104. return Slic3r::Polygon->new(
  105. Slic3r::Point->new($a->x + $distance*$dy, $a->y - $distance*$dx), #--
  106. Slic3r::Point->new($b->x + $distance*$dy, $b->y - $distance*$dx), #--
  107. Slic3r::Point->new($b->x - $distance*$dy, $b->y + $distance*$dx), #++
  108. Slic3r::Point->new($a->x - $distance*$dy, $a->y + $distance*$dx), #++
  109. );
  110. };
  111. my @result = ();
  112. my $covered = [];
  113. foreach my $polygon (@$self) {
  114. my @polylines = ();
  115. foreach my $line (@{$polygon->lines}) {
  116. # remove the areas that are already covered from this line
  117. my $clipped = diff_pl([$line->as_polyline], $covered);
  118. # skip very short segments/dots
  119. @$clipped = grep $_->length > $width/10, @$clipped;
  120. # grow the remaining lines and add them to the covered areas
  121. push @$covered, map $grow->($_, $width*1.1), @$clipped;
  122. # if the first remaining segment is connected to the last polyline, append it
  123. # to that -- NOTE: this assumes that multi_linestring_multi_polygon_difference()
  124. # preserved the orientation of the input linestring
  125. if (@polylines && @$clipped && $clipped->[0]->first_point->distance_to($polylines[-1]->last_point) <= $width/10) {
  126. $polylines[-1]->append_polyline(shift @$clipped);
  127. }
  128. push @polylines, @$clipped;
  129. }
  130. foreach my $polyline (@polylines) {
  131. # if this polyline looks like a closed loop, return it as a polygon
  132. if ($polyline->first_point->coincides_with($polyline->last_point)) {
  133. next if @$polyline == 2;
  134. $polyline->pop_back;
  135. push @result, Slic3r::Polygon->new(@$polyline);
  136. } else {
  137. push @result, $polyline;
  138. }
  139. }
  140. }
  141. return @result;
  142. }
  143. my $voronoi_lock :shared;
  144. sub _medial_axis_voronoi {
  145. my ($self, $width) = @_;
  146. lock($voronoi_lock);
  147. my $voronoi;
  148. {
  149. my @points = ();
  150. foreach my $polygon (@$self) {
  151. {
  152. my $p = $polygon->pp;
  153. Slic3r::Geometry::polyline_remove_short_segments($p, $width / 2);
  154. $polygon = Slic3r::Polygon->new(@$p);
  155. }
  156. # subdivide polygon segments so that we don't have anyone of them
  157. # being longer than $width / 2
  158. $polygon = $polygon->subdivide($width/2);
  159. push @points, @{$polygon->pp};
  160. }
  161. $voronoi = Math::Geometry::Voronoi->new(points => \@points);
  162. }
  163. $voronoi->compute;
  164. my $vertices = $voronoi->vertices;
  165. my @skeleton_lines = ();
  166. foreach my $edge (@{ $voronoi->edges }) {
  167. # ignore lines going to infinite
  168. next if $edge->[1] == -1 || $edge->[2] == -1;
  169. next if !$self->encloses_point_quick(Slic3r::Point->new(@{$vertices->[$edge->[1]]}))
  170. || !$self->encloses_point_quick(Slic3r::Point->new(@{$vertices->[$edge->[2]]}));
  171. push @skeleton_lines, [$edge->[1], $edge->[2]];
  172. }
  173. return () if !@skeleton_lines;
  174. # now walk along the medial axis and build continuos polylines or polygons
  175. my @polylines = ();
  176. {
  177. my @lines = @skeleton_lines;
  178. push @polylines, [ map @$_, shift @lines ];
  179. CYCLE: while (@lines) {
  180. for my $i (0..$#lines) {
  181. if ($lines[$i][0] == $polylines[-1][-1]) {
  182. push @{$polylines[-1]}, $lines[$i][1];
  183. } elsif ($lines[$i][1] == $polylines[-1][-1]) {
  184. push @{$polylines[-1]}, $lines[$i][0];
  185. } elsif ($lines[$i][1] == $polylines[-1][0]) {
  186. unshift @{$polylines[-1]}, $lines[$i][0];
  187. } elsif ($lines[$i][0] == $polylines[-1][0]) {
  188. unshift @{$polylines[-1]}, $lines[$i][1];
  189. } else {
  190. next;
  191. }
  192. splice @lines, $i, 1;
  193. next CYCLE;
  194. }
  195. push @polylines, [ map @$_, shift @lines ];
  196. }
  197. }
  198. my @result = ();
  199. foreach my $polyline (@polylines) {
  200. next unless @$polyline >= 2;
  201. # now replace point indexes with coordinates
  202. my @points = map Slic3r::Point->new(@{$vertices->[$_]}), @$polyline;
  203. if ($points[0]->coincides_with($points[-1])) {
  204. next if @points == 2;
  205. push @result, Slic3r::Polygon->new(@points[0..$#points-1]);
  206. } else {
  207. push @result, Slic3r::Polyline->new(@points);
  208. }
  209. $result[-1]->simplify($width / 7);
  210. }
  211. return @result;
  212. }
  213. package Slic3r::ExPolygon::Collection;
  214. use Slic3r::Geometry qw(X1 Y1);
  215. sub align_to_origin {
  216. my $self = shift;
  217. my @bb = Slic3r::Geometry::bounding_box([ map @$_, map @$_, @$self ]);
  218. $self->translate(-$bb[X1], -$bb[Y1]);
  219. $self;
  220. }
  221. sub size {
  222. my $self = shift;
  223. return [ Slic3r::Geometry::size_2D([ map @$_, map @$_, @$self ]) ];
  224. }
  225. 1;