BridgeDetector.pm 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  1. package Slic3r::Layer::BridgeDetector;
  2. use Moo;
  3. use List::Util qw(first sum max min);
  4. use Slic3r::Geometry qw(PI unscale scaled_epsilon rad2deg epsilon);
  5. use Slic3r::Geometry::Clipper qw(intersection_pl intersection_ex union offset diff_pl union_ex);
  6. has 'expolygon' => (is => 'ro', required => 1);
  7. has 'lower_slices' => (is => 'rw', required => 1); # ExPolygons or ExPolygonCollection
  8. has 'extrusion_width' => (is => 'rw', required => 1); # scaled
  9. has 'resolution' => (is => 'rw', default => sub { PI/36 });
  10. has '_edges' => (is => 'rw'); # Polylines representing the supporting edges
  11. has '_anchors' => (is => 'rw'); # ExPolygons
  12. has 'angle' => (is => 'rw');
  13. sub BUILD {
  14. my ($self) = @_;
  15. # outset our bridge by an arbitrary amout; we'll use this outer margin
  16. # for detecting anchors
  17. my $grown = $self->expolygon->offset(+$self->extrusion_width);
  18. # detect what edges lie on lower slices
  19. $self->_edges(my $edges = []);
  20. foreach my $lower (@{$self->lower_slices}) {
  21. # turn bridge contour and holes into polylines and then clip them
  22. # with each lower slice's contour
  23. push @$edges, map @{$_->clip_as_polyline([$lower->contour])}, @$grown;
  24. }
  25. Slic3r::debugf " bridge has %d support(s)\n", scalar(@$edges);
  26. # detect anchors as intersection between our bridge expolygon and the lower slices
  27. $self->_anchors(intersection_ex(
  28. $grown,
  29. [ map @$_, @{$self->lower_slices} ],
  30. 1, # safety offset required to avoid Clipper from detecting empty intersection while Boost actually found some @edges
  31. ));
  32. if (0) {
  33. require "Slic3r/SVG.pm";
  34. Slic3r::SVG::output("bridge.svg",
  35. expolygons => [ $self->expolygon ],
  36. red_expolygons => $self->lower_slices,
  37. polylines => $self->_edges,
  38. );
  39. }
  40. }
  41. sub detect_angle {
  42. my ($self) = @_;
  43. return undef if !@{$self->_edges};
  44. my @edges = @{$self->_edges};
  45. my $anchors = $self->_anchors;
  46. if (!@$anchors) {
  47. $self->angle(undef);
  48. return undef;
  49. }
  50. # Outset the bridge expolygon by half the amount we used for detecting anchors;
  51. # we'll use this one to clip our test lines and be sure that their endpoints
  52. # are inside the anchors and not on their contours leading to false negatives.
  53. my $clip_area = $self->expolygon->offset_ex(+$self->extrusion_width/2);
  54. # we'll now try several directions using a rudimentary visibility check:
  55. # bridge in several directions and then sum the length of lines having both
  56. # endpoints within anchors
  57. # we test angles according to configured resolution
  58. my @angles = map { $_*$self->resolution } 0..(PI/$self->resolution);
  59. # we also test angles of each bridge contour
  60. push @angles, map $_->direction, map @{$_->lines}, @{$self->expolygon};
  61. # we also test angles of each open supporting edge
  62. # (this finds the optimal angle for C-shaped supports)
  63. push @angles,
  64. map Slic3r::Line->new($_->first_point, $_->last_point)->direction,
  65. grep { !$_->first_point->coincides_with($_->last_point) }
  66. @edges;
  67. # remove duplicates
  68. my $min_resolution = PI/180; # 1 degree
  69. @angles = map { ($_ >= &PI-&epsilon) ? ($_-&PI) : $_ } @angles;
  70. @angles = sort @angles;
  71. for (my $i = 1; $i <= $#angles; ++$i) {
  72. if (abs($angles[$i] - $angles[$i-1]) < $min_resolution) {
  73. splice @angles, $i, 1;
  74. --$i;
  75. }
  76. }
  77. my %directions_coverage = (); # angle => score
  78. my %directions_avg_length = (); # angle => score
  79. my $line_increment = $self->extrusion_width;
  80. my %unique_angles = map { $_ => 1 } @angles;
  81. for my $angle (@angles) {
  82. my $my_clip_area = [ map $_->clone, @$clip_area ];
  83. my $my_anchors = [ map $_->clone, @$anchors ];
  84. # rotate everything - the center point doesn't matter
  85. $_->rotate(-$angle, [0,0]) for @$my_clip_area, @$my_anchors;
  86. # generate lines in this direction
  87. my $bounding_box = Slic3r::Geometry::BoundingBox->new_from_points([ map @$_, map @$_, @$my_anchors ]);
  88. my @lines = ();
  89. for (my $y = $bounding_box->y_min; $y <= $bounding_box->y_max; $y+= $line_increment) {
  90. push @lines, Slic3r::Polyline->new(
  91. [$bounding_box->x_min, $y],
  92. [$bounding_box->x_max, $y],
  93. );
  94. }
  95. my @clipped_lines = map Slic3r::Line->new(@$_), @{ intersection_pl(\@lines, [ map @$_, @$my_clip_area ]) };
  96. # remove any line not having both endpoints within anchors
  97. @clipped_lines = grep {
  98. my $line = $_;
  99. (first { $_->contains_point($line->a) } @$my_anchors)
  100. && (first { $_->contains_point($line->b) } @$my_anchors);
  101. } @clipped_lines;
  102. my @lengths = map $_->length, @clipped_lines;
  103. # sum length of bridged lines
  104. $directions_coverage{$angle} = sum(@lengths) // 0;
  105. # max length of bridged lines
  106. $directions_avg_length{$angle} = @lengths ? (max(@lengths)) : -1;
  107. }
  108. # if no direction produced coverage, then there's no bridge direction
  109. return undef if !defined first { $_ > 0 } values %directions_coverage;
  110. # the best direction is the one causing most lines to be bridged (thus most coverage)
  111. # and shortest max line length
  112. my @sorted_directions = sort {
  113. my $cmp;
  114. my $coverage_diff = $directions_coverage{$a} - $directions_coverage{$b};
  115. if (abs($coverage_diff) < $self->extrusion_width) {
  116. $cmp = $directions_avg_length{$b} <=> $directions_avg_length{$a};
  117. } else {
  118. $cmp = ($coverage_diff > 0) ? 1 : -1;
  119. }
  120. $cmp;
  121. } keys %directions_coverage;
  122. $self->angle($sorted_directions[-1]);
  123. if ($self->angle >= PI) {
  124. $self->angle($self->angle - PI);
  125. }
  126. Slic3r::debugf " Optimal infill angle is %d degrees\n", rad2deg($self->angle);
  127. return $self->angle;
  128. }
  129. sub coverage {
  130. my ($self, $angle) = @_;
  131. if (!defined $angle) {
  132. return [] if !defined($angle = $self->angle);
  133. }
  134. # Clone our expolygon and rotate it so that we work with vertical lines.
  135. my $expolygon = $self->expolygon->clone;
  136. $expolygon->rotate(PI/2 - $angle, [0,0]);
  137. # Outset the bridge expolygon by half the amount we used for detecting anchors;
  138. # we'll use this one to generate our trapezoids and be sure that their vertices
  139. # are inside the anchors and not on their contours leading to false negatives.
  140. my $grown = $expolygon->offset_ex(+$self->extrusion_width/2);
  141. # Compute trapezoids according to a vertical orientation
  142. my $trapezoids = [ map @{$_->get_trapezoids2(PI/2)}, @$grown ];
  143. # get anchors and rotate them too
  144. my $anchors = [ map $_->clone, @{$self->_anchors} ];
  145. $_->rotate(PI/2 - $angle, [0,0]) for @$anchors;
  146. my @covered = (); # polygons
  147. foreach my $trapezoid (@$trapezoids) {
  148. my @polylines = map $_->as_polyline, @{$trapezoid->lines};
  149. my @supported = @{intersection_pl(\@polylines, [map @$_, @$anchors])};
  150. # not nice, we need a more robust non-numeric check
  151. @supported = grep $_->length >= $self->extrusion_width, @supported;
  152. if (@supported >= 2) {
  153. push @covered, $trapezoid;
  154. }
  155. }
  156. # merge trapezoids and rotate them back
  157. my $coverage = union(\@covered);
  158. $_->rotate(-(PI/2 - $angle), [0,0]) for @$coverage;
  159. # intersect trapezoids with actual bridge area to remove extra margins
  160. $coverage = intersection_ex($coverage, [ @{$self->expolygon} ]);
  161. if (0) {
  162. my @lines = map @{$_->lines}, @$trapezoids;
  163. $_->rotate(-(PI/2 - $angle), [0,0]) for @lines;
  164. require "Slic3r/SVG.pm";
  165. Slic3r::SVG::output(
  166. "coverage_" . rad2deg($angle) . ".svg",
  167. expolygons => [$self->expolygon],
  168. green_expolygons => $self->_anchors,
  169. red_expolygons => $coverage,
  170. lines => \@lines,
  171. );
  172. }
  173. return $coverage;
  174. }
  175. # this method returns the bridge edges (as polylines) that are not supported
  176. # but would allow the entire bridge area to be bridged with detected angle
  177. # if supported too
  178. sub unsupported_edges {
  179. my ($self, $angle) = @_;
  180. if (!defined $angle) {
  181. return [] if !defined($angle = $self->angle);
  182. }
  183. # get bridge edges (both contour and holes)
  184. my @bridge_edges = map $_->split_at_first_point, @{$self->expolygon};
  185. $_->[0]->translate(1,0) for @bridge_edges; # workaround for Clipper bug, see comments in Slic3r::Polygon::clip_as_polyline()
  186. # get unsupported edges
  187. my $grown_lower = offset([ map @$_, @{$self->lower_slices} ], +$self->extrusion_width);
  188. my $unsupported = diff_pl(
  189. \@bridge_edges,
  190. $grown_lower,
  191. );
  192. # filter out edges parallel to the bridging angle
  193. for (my $i = 0; $i <= $#$unsupported; ++$i) {
  194. if (first { abs($_->direction - $angle) < epsilon } @{$unsupported->[$i]->lines}) {
  195. splice @$unsupported, $i, 1;
  196. --$i;
  197. }
  198. }
  199. if (0) {
  200. require "Slic3r/SVG.pm";
  201. Slic3r::SVG::output(
  202. "unsupported_" . rad2deg($angle) . ".svg",
  203. expolygons => [$self->expolygon],
  204. green_expolygons => $self->_anchors,
  205. red_expolygons => union_ex($grown_lower),
  206. no_arrows => 1,
  207. polylines => \@bridge_edges,
  208. red_polylines => $unsupported,
  209. );
  210. }
  211. return $unsupported;
  212. }
  213. 1;