ExPolygon.pm 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278
  1. package Slic3r::ExPolygon;
  2. use strict;
  3. use warnings;
  4. # an ExPolygon is a polygon with holes
  5. use Math::Geometry::Voronoi;
  6. use Slic3r::Geometry qw(X Y A B point_in_polygon);
  7. use Slic3r::Geometry::Clipper qw(union_ex JT_MITER);
  8. # the constructor accepts an array of polygons
  9. # or a Math::Clipper ExPolygon (hashref)
  10. sub new {
  11. my $class = shift;
  12. my $self;
  13. if (@_ == 1 && ref $_[0] eq 'HASH') {
  14. $self = [
  15. Slic3r::Polygon->new($_[0]{outer}),
  16. map Slic3r::Polygon->new($_), @{$_[0]{holes}},
  17. ];
  18. } else {
  19. $self = [ map Slic3r::Polygon->new($_), @_ ];
  20. }
  21. bless $self, $class;
  22. $self;
  23. }
  24. sub clone {
  25. my $self = shift;
  26. return (ref $self)->new(map $_->clone, @$self);
  27. }
  28. sub contour {
  29. my $self = shift;
  30. return $self->[0];
  31. }
  32. sub holes {
  33. my $self = shift;
  34. return @$self[1..$#$self];
  35. }
  36. sub lines {
  37. my $self = shift;
  38. return map $_->lines, @$self;
  39. }
  40. sub clipper_expolygon {
  41. my $self = shift;
  42. return {
  43. outer => $self->contour,
  44. holes => [ $self->holes ],
  45. };
  46. }
  47. sub offset {
  48. my $self = shift;
  49. my ($distance, $scale, $joinType, $miterLimit) = @_;
  50. $scale ||= $Slic3r::resolution * 1000000;
  51. $joinType = JT_MITER if !defined $joinType;
  52. $miterLimit ||= 2;
  53. my $offsets = Math::Clipper::offset($self, $distance, $scale, $joinType, $miterLimit);
  54. return @$offsets;
  55. }
  56. sub safety_offset {
  57. my $self = shift;
  58. # we're offsetting contour and holes separately
  59. # because Clipper doesn't return polygons in the same order as
  60. # we feed them to it
  61. return (ref $self)->new(
  62. $self->contour->safety_offset,
  63. @{ Slic3r::Geometry::Clipper::safety_offset([$self->holes]) },
  64. );
  65. }
  66. sub offset_ex {
  67. my $self = shift;
  68. my @offsets = $self->offset(@_);
  69. # apply holes to the right contours
  70. return @{ union_ex(\@offsets) };
  71. }
  72. sub encloses_point {
  73. my $self = shift;
  74. my ($point) = @_;
  75. return $self->contour->encloses_point($point)
  76. && (!grep($_->encloses_point($point), $self->holes)
  77. || grep($_->point_on_segment($point), $self->holes));
  78. }
  79. sub point_on_segment {
  80. my $self = shift;
  81. my ($point) = @_;
  82. for (@$self) {
  83. my $line = $_->point_on_segment($point);
  84. return $line if $line;
  85. }
  86. return undef;
  87. }
  88. sub bounding_box {
  89. my $self = shift;
  90. return Slic3r::Geometry::bounding_box($self->contour);
  91. }
  92. sub bounding_box_polygon {
  93. my $self = shift;
  94. my @bb = $self->bounding_box;
  95. return Slic3r::Polygon->new([
  96. [ $bb[0], $bb[1] ],
  97. [ $bb[2], $bb[1] ],
  98. [ $bb[2], $bb[3] ],
  99. [ $bb[0], $bb[3] ],
  100. ]);
  101. }
  102. sub clip_line {
  103. my $self = shift;
  104. my ($line) = @_; # line must be a Slic3r::Line object
  105. my @intersections = grep $_, map $_->intersection($line, 1), map $_->lines, @$self;
  106. my @dir = (
  107. $line->[B][X] <=> $line->[A][X],
  108. $line->[B][Y] <=> $line->[A][Y],
  109. );
  110. @intersections = sort {
  111. (($a->[X] <=> $b->[X]) == $dir[X]) && (($a->[Y] <=> $b->[Y]) == $dir[Y]) ? 1 : -1
  112. } @intersections, @$line;
  113. shift @intersections if $intersections[0]->coincides_with($intersections[1]);
  114. pop @intersections if $intersections[-1]->coincides_with($intersections[-2]);
  115. shift @intersections
  116. if !$self->encloses_point($intersections[0])
  117. && !$self->point_on_segment($intersections[0]);
  118. my @lines = ();
  119. while (@intersections) {
  120. # skip tangent points
  121. my @points = splice @intersections, 0, 2;
  122. next if !$points[1];
  123. next if $points[0]->coincides_with($points[1]);
  124. push @lines, [ @points ];
  125. }
  126. return [@lines];
  127. }
  128. sub simplify {
  129. my $self = shift;
  130. $_->simplify(@_) for @$self;
  131. }
  132. sub translate {
  133. my $self = shift;
  134. $_->translate(@_) for @$self;
  135. }
  136. sub rotate {
  137. my $self = shift;
  138. $_->rotate(@_) for @$self;
  139. }
  140. sub area {
  141. my $self = shift;
  142. my $area = $self->contour->area;
  143. $area -= $_->area for $self->holes;
  144. return $area;
  145. }
  146. # this method only works for expolygons having only a contour or
  147. # a contour and a hole, and not being thicker than the supplied
  148. # width. it returns a polyline or a polygon
  149. sub medial_axis {
  150. my $self = shift;
  151. my ($width) = @_;
  152. my @self_lines = map $_->lines, @$self;
  153. my $expolygon = $self->clone;
  154. my @points = ();
  155. foreach my $polygon (@$expolygon) {
  156. Slic3r::Geometry::polyline_remove_short_segments($polygon, $width / 2);
  157. # subdivide polygon segments so that we don't have anyone of them
  158. # being longer than $width / 2
  159. $polygon->subdivide($width/2);
  160. push @points, @$polygon;
  161. }
  162. my $voronoi = Math::Geometry::Voronoi->new(points => \@points);
  163. $voronoi->compute;
  164. my @skeleton_lines = ();
  165. my $vertices = $voronoi->vertices;
  166. my $edges = $voronoi->edges;
  167. foreach my $edge (@$edges) {
  168. # ignore lines going to infinite
  169. next if $edge->[1] == -1 || $edge->[2] == -1;
  170. my ($a, $b);
  171. $a = $vertices->[$edge->[1]];
  172. $b = $vertices->[$edge->[2]];
  173. next if !$self->encloses_point($a) || !$self->encloses_point($b);
  174. push @skeleton_lines, [$edge->[1], $edge->[2]];
  175. }
  176. # remove leafs (lines not connected to other lines at one of their endpoints)
  177. {
  178. my %pointmap = ();
  179. $pointmap{$_}++ for map @$_, @skeleton_lines;
  180. @skeleton_lines = grep {
  181. $pointmap{$_->[A]} >= 2 && $pointmap{$_->[B]} >= 2
  182. } @skeleton_lines;
  183. }
  184. return undef if !@skeleton_lines;
  185. return undef if !@skeleton_lines;
  186. # now build a single polyline
  187. my $polyline = [];
  188. {
  189. my %pointmap = ();
  190. foreach my $line (@skeleton_lines) {
  191. foreach my $point_id (@$line) {
  192. $pointmap{$point_id} ||= [];
  193. push @{$pointmap{$point_id}}, $line;
  194. }
  195. }
  196. # start from a point having only one line
  197. foreach my $point_id (keys %pointmap) {
  198. if (@{$pointmap{$point_id}} == 1) {
  199. push @$polyline, grep $_ ne $point_id, map @$_, shift @{$pointmap{$point_id}};
  200. last;
  201. }
  202. }
  203. # if no such point is found, pick a random one
  204. push @$polyline, shift @{ +(values %pointmap)[0][0] } if !@$polyline;
  205. my %visited_lines = ();
  206. while (1) {
  207. my $last_point_id = $polyline->[-1];
  208. shift @{ $pointmap{$last_point_id} }
  209. while @{ $pointmap{$last_point_id} } && $visited_lines{$pointmap{$last_point_id}[0]};
  210. my $next_line = shift @{ $pointmap{$last_point_id} } or last;
  211. $visited_lines{$next_line} = 1;
  212. push @$polyline, grep $_ ne $last_point_id, @$next_line;
  213. }
  214. }
  215. # now replace point indexes with coordinates
  216. @$polyline = map $vertices->[$_], @$polyline;
  217. # cleanup
  218. Slic3r::Geometry::polyline_remove_short_segments($polyline, $width / 2);
  219. $polyline = Slic3r::Geometry::douglas_peucker($polyline, $width / 7);
  220. Slic3r::Geometry::polyline_remove_parallel_continuous_edges($polyline);
  221. if (Slic3r::Geometry::same_point($polyline->[0], $polyline->[-1])) {
  222. return undef if @$polyline == 2;
  223. return Slic3r::Polygon->new(@$polyline[0..$#$polyline-1]);
  224. } else {
  225. return Slic3r::Polyline->new($polyline);
  226. }
  227. }
  228. 1;