ExPolygon.pm 9.4 KB

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