ExPolygon.pm 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  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 same_line epsilon);
  9. use Slic3r::Geometry::Clipper qw(union_ex JT_MITER);
  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 && same_line($clip->[0]->pp, $line->pp);
  53. } else {
  54. return @$clip == 1 && abs(Boost::Geometry::Utils::linestring_length($clip->[0]->pp) - $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. @{Boost::Geometry::Utils::polygon_multi_linestring_intersection($self->pp, [$line->pp])}
  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 = shift;
  87. my ($width) = @_;
  88. my @self_lines = map $_->lines, @$self;
  89. my $expolygon = $self->clone;
  90. my @points = ();
  91. foreach my $polygon (@$expolygon) {
  92. {
  93. my $p = $polygon->pp;
  94. Slic3r::Geometry::polyline_remove_short_segments($p, $width / 2);
  95. $polygon = Slic3r::Polygon->new(@$p);
  96. }
  97. # subdivide polygon segments so that we don't have anyone of them
  98. # being longer than $width / 2
  99. $polygon->subdivide($width/2);
  100. push @points, @$polygon;
  101. }
  102. my $voronoi = Math::Geometry::Voronoi->new(points => [ map $_->pp, @points ]);
  103. $voronoi->compute;
  104. my @skeleton_lines = ();
  105. my $vertices = $voronoi->vertices;
  106. my $edges = $voronoi->edges;
  107. foreach my $edge (@$edges) {
  108. # ignore lines going to infinite
  109. next if $edge->[1] == -1 || $edge->[2] == -1;
  110. my ($a, $b);
  111. $a = Slic3r::Point->new(@{$vertices->[$edge->[1]]});
  112. $b = Slic3r::Point->new(@{$vertices->[$edge->[2]]});
  113. next if !$self->encloses_point_quick($a) || !$self->encloses_point_quick($b);
  114. push @skeleton_lines, [$edge->[1], $edge->[2]];
  115. }
  116. # remove leafs (lines not connected to other lines at one of their endpoints)
  117. {
  118. my %pointmap = ();
  119. $pointmap{$_}++ for map @$_, @skeleton_lines;
  120. @skeleton_lines = grep {
  121. $pointmap{$_->[A]} >= 2 && $pointmap{$_->[B]} >= 2
  122. } @skeleton_lines;
  123. }
  124. return () if !@skeleton_lines;
  125. # now walk along the medial axis and build continuos polylines or polygons
  126. my @polylines = ();
  127. {
  128. # build a map of line endpoints
  129. my %pointmap = (); # point_idx => [line_idx, line_idx ...]
  130. for my $line_idx (0 .. $#skeleton_lines) {
  131. for my $point_idx (@{$skeleton_lines[$line_idx]}) {
  132. $pointmap{$point_idx} ||= [];
  133. push @{$pointmap{$point_idx}}, $line_idx;
  134. }
  135. }
  136. # build the list of available lines
  137. my %spare_lines = map {$_ => 1} (0 .. $#skeleton_lines);
  138. CYCLE: while (%spare_lines) {
  139. push @polylines, [];
  140. my $polyline = $polylines[-1];
  141. # start from a random line
  142. my $first_line_idx = +(keys %spare_lines)[0];
  143. delete $spare_lines{$first_line_idx};
  144. push @$polyline, @{ $skeleton_lines[$first_line_idx] };
  145. while (1) {
  146. my $last_point_id = $polyline->[-1];
  147. my $lines_starting_here = $pointmap{$last_point_id};
  148. # remove all the visited lines from the array
  149. shift @$lines_starting_here
  150. while @$lines_starting_here && !$spare_lines{$lines_starting_here->[0]};
  151. # do we have a line starting here?
  152. my $next_line_idx = shift @$lines_starting_here;
  153. if (!defined $next_line_idx) {
  154. delete $pointmap{$last_point_id};
  155. next CYCLE;
  156. }
  157. # line is not available anymore
  158. delete $spare_lines{$next_line_idx};
  159. # add the other point to our polyline and continue walking
  160. push @$polyline, grep $_ ne $last_point_id, @{$skeleton_lines[$next_line_idx]};
  161. }
  162. }
  163. }
  164. my @result = ();
  165. foreach my $polyline (@polylines) {
  166. next unless @$polyline >= 2;
  167. # now replace point indexes with coordinates
  168. @$polyline = map $vertices->[$_], @$polyline;
  169. # cleanup
  170. $polyline = Slic3r::Geometry::douglas_peucker($polyline, $width / 7);
  171. if (Slic3r::Geometry::same_point($polyline->[0], $polyline->[-1])) {
  172. next if @$polyline == 2;
  173. push @result, Slic3r::Polygon->new(@$polyline[0..$#$polyline-1]);
  174. } else {
  175. push @result, Slic3r::Polyline->new(@$polyline);
  176. }
  177. }
  178. return @result;
  179. }
  180. package Slic3r::ExPolygon::Collection;
  181. use Slic3r::Geometry qw(X1 Y1);
  182. sub align_to_origin {
  183. my $self = shift;
  184. my @bb = Slic3r::Geometry::bounding_box([ map @$_, map @$_, @$self ]);
  185. $self->translate(-$bb[X1], -$bb[Y1]);
  186. $self;
  187. }
  188. sub size {
  189. my $self = shift;
  190. return [ Slic3r::Geometry::size_2D([ map @$_, map @$_, @$self ]) ];
  191. }
  192. 1;