ExPolygon.pm 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
  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 Math::Geometry::Voronoi;
  7. use Slic3r::Geometry qw(X Y A B point_in_polygon same_line line_length);
  8. use Slic3r::Geometry::Clipper qw(union_ex JT_MITER);
  9. # the constructor accepts an array of polygons
  10. # or a Math::Clipper ExPolygon (hashref)
  11. sub new {
  12. my $class = shift;
  13. my $self;
  14. if (@_ == 1 && ref $_[0] eq 'HASH') {
  15. $self = [
  16. Slic3r::Polygon->new($_[0]{outer}),
  17. map Slic3r::Polygon->new($_), @{$_[0]{holes}},
  18. ];
  19. } else {
  20. $self = [ map Slic3r::Polygon->new($_), @_ ];
  21. }
  22. bless $self, $class;
  23. $self;
  24. }
  25. sub clone {
  26. my $self = shift;
  27. return (ref $self)->new(map $_->clone, @$self);
  28. }
  29. sub contour {
  30. my $self = shift;
  31. return $self->[0];
  32. }
  33. sub holes {
  34. my $self = shift;
  35. return @$self[1..$#$self];
  36. }
  37. sub lines {
  38. my $self = shift;
  39. return map $_->lines, @$self;
  40. }
  41. sub clipper_expolygon {
  42. my $self = shift;
  43. return {
  44. outer => $self->contour,
  45. holes => [ $self->holes ],
  46. };
  47. }
  48. sub boost_polygon {
  49. my $self = shift;
  50. return Boost::Geometry::Utils::polygon(@$self);
  51. }
  52. sub offset {
  53. my $self = shift;
  54. my ($distance, $scale, $joinType, $miterLimit) = @_;
  55. $scale ||= &Slic3r::SCALING_FACTOR * 1000000;
  56. $joinType = JT_MITER if !defined $joinType;
  57. $miterLimit ||= 2;
  58. my $offsets = Math::Clipper::offset($self, $distance, $scale, $joinType, $miterLimit);
  59. return @$offsets;
  60. }
  61. sub safety_offset {
  62. my $self = shift;
  63. # we're offsetting contour and holes separately
  64. # because Clipper doesn't return polygons in the same order as
  65. # we feed them to it
  66. return (ref $self)->new(
  67. $self->contour->safety_offset,
  68. @{ Slic3r::Geometry::Clipper::safety_offset([$self->holes]) },
  69. );
  70. }
  71. sub offset_ex {
  72. my $self = shift;
  73. my @offsets = $self->offset(@_);
  74. # apply holes to the right contours
  75. return @{ union_ex(\@offsets) };
  76. }
  77. sub noncollapsing_offset_ex {
  78. my $self = shift;
  79. my ($distance, @params) = @_;
  80. return $self->offset_ex($distance + 1, @params);
  81. }
  82. sub encloses_point {
  83. my $self = shift;
  84. my ($point) = @_;
  85. return $self->contour->encloses_point($point)
  86. && (!grep($_->encloses_point($point), $self->holes)
  87. || grep($_->point_on_segment($point), $self->holes));
  88. }
  89. # A version of encloses_point for use when hole borders do not matter.
  90. # Useful because point_on_segment is slow
  91. sub encloses_point_quick {
  92. my $self = shift;
  93. my ($point) = @_;
  94. return $self->contour->encloses_point($point)
  95. && !grep($_->encloses_point($point), $self->holes);
  96. }
  97. sub encloses_line {
  98. my $self = shift;
  99. my ($line, $tolerance) = @_;
  100. my $clip = $self->clip_line($line);
  101. if (!defined $tolerance) {
  102. # optimization
  103. return @$clip == 1 && same_line($clip->[0], $line);
  104. } else {
  105. return @$clip == 1 && abs(line_length($clip->[0]) - $line->length) < $tolerance;
  106. }
  107. }
  108. sub point_on_segment {
  109. my $self = shift;
  110. my ($point) = @_;
  111. for (@$self) {
  112. my $line = $_->point_on_segment($point);
  113. return $line if $line;
  114. }
  115. return undef;
  116. }
  117. sub bounding_box {
  118. my $self = shift;
  119. return Slic3r::Geometry::bounding_box($self->contour);
  120. }
  121. sub bounding_box_polygon {
  122. my $self = shift;
  123. my @bb = $self->bounding_box;
  124. return Slic3r::Polygon->new([
  125. [ $bb[0], $bb[1] ],
  126. [ $bb[2], $bb[1] ],
  127. [ $bb[2], $bb[3] ],
  128. [ $bb[0], $bb[3] ],
  129. ]);
  130. }
  131. sub clip_line {
  132. my $self = shift;
  133. my ($line) = @_; # line must be a Slic3r::Line object
  134. return Boost::Geometry::Utils::polygon_linestring_intersection(
  135. $self->boost_polygon,
  136. $line->boost_linestring,
  137. );
  138. }
  139. sub simplify {
  140. my $self = shift;
  141. $_->simplify(@_) for @$self;
  142. }
  143. sub translate {
  144. my $self = shift;
  145. $_->translate(@_) for @$self;
  146. }
  147. sub rotate {
  148. my $self = shift;
  149. $_->rotate(@_) for @$self;
  150. }
  151. sub area {
  152. my $self = shift;
  153. my $area = $self->contour->area;
  154. $area -= $_->area for $self->holes;
  155. return $area;
  156. }
  157. # this method only works for expolygons having only a contour or
  158. # a contour and a hole, and not being thicker than the supplied
  159. # width. it returns a polyline or a polygon
  160. sub medial_axis {
  161. my $self = shift;
  162. my ($width) = @_;
  163. my @self_lines = map $_->lines, @$self;
  164. my $expolygon = $self->clone;
  165. my @points = ();
  166. foreach my $polygon (@$expolygon) {
  167. Slic3r::Geometry::polyline_remove_short_segments($polygon, $width / 2);
  168. # subdivide polygon segments so that we don't have anyone of them
  169. # being longer than $width / 2
  170. $polygon->subdivide($width/2);
  171. push @points, @$polygon;
  172. }
  173. my $voronoi = Math::Geometry::Voronoi->new(points => \@points);
  174. $voronoi->compute;
  175. my @skeleton_lines = ();
  176. my $vertices = $voronoi->vertices;
  177. my $edges = $voronoi->edges;
  178. foreach my $edge (@$edges) {
  179. # ignore lines going to infinite
  180. next if $edge->[1] == -1 || $edge->[2] == -1;
  181. my ($a, $b);
  182. $a = $vertices->[$edge->[1]];
  183. $b = $vertices->[$edge->[2]];
  184. next if !$self->encloses_point_quick($a) || !$self->encloses_point_quick($b);
  185. push @skeleton_lines, [$edge->[1], $edge->[2]];
  186. }
  187. # remove leafs (lines not connected to other lines at one of their endpoints)
  188. {
  189. my %pointmap = ();
  190. $pointmap{$_}++ for map @$_, @skeleton_lines;
  191. @skeleton_lines = grep {
  192. $pointmap{$_->[A]} >= 2 && $pointmap{$_->[B]} >= 2
  193. } @skeleton_lines;
  194. }
  195. return () if !@skeleton_lines;
  196. # now walk along the medial axis and build continuos polylines or polygons
  197. my @polylines = ();
  198. {
  199. # build a map of line endpoints
  200. my %pointmap = (); # point_idx => [line_idx, line_idx ...]
  201. for my $line_idx (0 .. $#skeleton_lines) {
  202. for my $point_idx (@{$skeleton_lines[$line_idx]}) {
  203. $pointmap{$point_idx} ||= [];
  204. push @{$pointmap{$point_idx}}, $line_idx;
  205. }
  206. }
  207. # build the list of available lines
  208. my %spare_lines = map {$_ => 1} (0 .. $#skeleton_lines);
  209. CYCLE: while (%spare_lines) {
  210. push @polylines, [];
  211. my $polyline = $polylines[-1];
  212. # start from a random line
  213. my $first_line_idx = +(keys %spare_lines)[0];
  214. delete $spare_lines{$first_line_idx};
  215. push @$polyline, @{ $skeleton_lines[$first_line_idx] };
  216. while (1) {
  217. my $last_point_id = $polyline->[-1];
  218. my $lines_starting_here = $pointmap{$last_point_id};
  219. # remove all the visited lines from the array
  220. shift @$lines_starting_here
  221. while @$lines_starting_here && !$spare_lines{$lines_starting_here->[0]};
  222. # do we have a line starting here?
  223. my $next_line_idx = shift @$lines_starting_here;
  224. if (!defined $next_line_idx) {
  225. delete $pointmap{$last_point_id};
  226. next CYCLE;
  227. }
  228. # line is not available anymore
  229. delete $spare_lines{$next_line_idx};
  230. # add the other point to our polyline and continue walking
  231. push @$polyline, grep $_ ne $last_point_id, @{$skeleton_lines[$next_line_idx]};
  232. }
  233. }
  234. }
  235. my @result = ();
  236. foreach my $polyline (@polylines) {
  237. next unless @$polyline >= 2;
  238. # now replace point indexes with coordinates
  239. @$polyline = map $vertices->[$_], @$polyline;
  240. # cleanup
  241. $polyline = Slic3r::Geometry::douglas_peucker($polyline, $width / 7);
  242. if (Slic3r::Geometry::same_point($polyline->[0], $polyline->[-1])) {
  243. next if @$polyline == 2;
  244. push @result, Slic3r::Polygon->new(@$polyline[0..$#$polyline-1]);
  245. } else {
  246. push @result, Slic3r::Polyline->new($polyline);
  247. }
  248. }
  249. return @result;
  250. }
  251. 1;