ArcFitting.pm 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242
  1. package Slic3r::GCode::ArcFitting;
  2. use Moo;
  3. use Slic3r::Geometry qw(X Y PI scale unscale epsilon scaled_epsilon deg2rad angle3points);
  4. extends 'Slic3r::GCode::Reader';
  5. has 'config' => (is => 'ro', required => 0);
  6. has 'min_segments' => (is => 'rw', default => sub { 2 });
  7. has 'min_total_angle' => (is => 'rw', default => sub { deg2rad(30) });
  8. has 'max_relative_angle' => (is => 'rw', default => sub { deg2rad(15) });
  9. has 'len_epsilon' => (is => 'rw', default => sub { scale 0.2 });
  10. has 'angle_epsilon' => (is => 'rw', default => sub { abs(deg2rad(10)) });
  11. has '_extrusion_axis' => (is => 'lazy');
  12. has '_path' => (is => 'rw');
  13. has '_cur_F' => (is => 'rw');
  14. has '_cur_E' => (is => 'rw');
  15. has '_cur_E0' => (is => 'rw');
  16. has '_comment' => (is => 'rw');
  17. sub _build__extrusion_axis {
  18. my ($self) = @_;
  19. return $self->config ? $self->config->get_extrusion_axis : 'E';
  20. }
  21. sub process {
  22. my $self = shift;
  23. my ($gcode) = @_;
  24. die "Arc fitting is not available (incomplete feature)\n";
  25. die "Arc fitting doesn't support extrusion axis not being E\n" if $self->_extrusion_axis ne 'E';
  26. my $new_gcode = "";
  27. $self->parse($gcode, sub {
  28. my ($reader, $cmd, $args, $info) = @_;
  29. if ($info->{extruding} && $info->{dist_XY} > 0) {
  30. # this is an extrusion segment
  31. # get segment
  32. my $line = Slic3r::Line->new(
  33. Slic3r::Point->new_scale($self->X, $self->Y),
  34. Slic3r::Point->new_scale($args->{X}, $args->{Y}),
  35. );
  36. # get segment speed
  37. my $F = $args->{F} // $reader->F;
  38. # get extrusion per unscaled distance unit
  39. my $e = $info->{dist_E} / unscale($line->length);
  40. if ($self->_path && $F == $self->_cur_F && abs($e - $self->_cur_E) < epsilon) {
  41. # if speed and extrusion per unit are the same as the previous segments,
  42. # append this segment to path
  43. $self->_path->append($line->b);
  44. } elsif ($self->_path) {
  45. # segment can't be appended to previous path, so we flush the previous one
  46. # and start over
  47. $new_gcode .= $self->path_to_gcode;
  48. $self->_path(undef);
  49. }
  50. if (!$self->_path) {
  51. # if this is the first segment of a path, start it from scratch
  52. $self->_path(Slic3r::Polyline->new(@$line));
  53. $self->_cur_F($F);
  54. $self->_cur_E($e);
  55. $self->_cur_E0($self->E);
  56. $self->_comment($info->{comment});
  57. }
  58. } else {
  59. # if we have a path, we flush it and go on
  60. $new_gcode .= $self->path_to_gcode if $self->_path;
  61. $new_gcode .= $info->{raw} . "\n";
  62. $self->_path(undef);
  63. }
  64. });
  65. $new_gcode .= $self->path_to_gcode if $self->_path;
  66. return $new_gcode;
  67. }
  68. sub path_to_gcode {
  69. my ($self) = @_;
  70. my @chunks = $self->detect_arcs($self->_path);
  71. my $gcode = "";
  72. my $E = $self->_cur_E0;
  73. foreach my $chunk (@chunks) {
  74. if ($chunk->isa('Slic3r::Polyline')) {
  75. my @lines = @{$chunk->lines};
  76. $gcode .= sprintf "G1 F%s\n", $self->_cur_F;
  77. foreach my $line (@lines) {
  78. $E += $self->_cur_E * unscale($line->length);
  79. $gcode .= sprintf "G1 X%.3f Y%.3f %s%.5f",
  80. (map unscale($_), @{$line->b}),
  81. $self->_extrusion_axis, $E;
  82. $gcode .= sprintf " ; %s", $self->_comment if $self->_comment;
  83. $gcode .= "\n";
  84. }
  85. } elsif ($chunk->isa('Slic3r::GCode::ArcFitting::Arc')) {
  86. $gcode .= !$chunk->is_ccw ? "G2" : "G3";
  87. $gcode .= sprintf " X%.3f Y%.3f", map unscale($_), @{$chunk->end}; # destination point
  88. # XY distance of the center from the start position
  89. $gcode .= sprintf " I%.3f", unscale($chunk->center->[X] - $chunk->start->[X]);
  90. $gcode .= sprintf " J%.3f", unscale($chunk->center->[Y] - $chunk->start->[Y]);
  91. $E += $self->_cur_E * unscale($chunk->length);
  92. $gcode .= sprintf " %s%.5f", $self->_extrusion_axis, $E;
  93. $gcode .= sprintf " F%s\n", $self->_cur_F;
  94. }
  95. }
  96. return $gcode;
  97. }
  98. sub detect_arcs {
  99. my ($self, $path) = @_;
  100. my @chunks = ();
  101. my @arc_points = ();
  102. my $polyline = undef;
  103. my $arc_start = undef;
  104. my @points = @$path;
  105. for (my $i = 1; $i <= $#points; ++$i) {
  106. my $end = undef;
  107. # we need at least three points to check whether they form an arc
  108. if ($i < $#points) {
  109. my $len = $points[$i-1]->distance_to($points[$i]);
  110. my $rel_angle = PI - angle3points(@points[$i, $i-1, $i+1]);
  111. if (abs($rel_angle) <= $self->max_relative_angle) {
  112. for (my $j = $i+1; $j <= $#points; ++$j) {
  113. # check whether @points[($i-1)..$j] form an arc
  114. last if abs($points[$j-1]->distance_to($points[$j]) - $len) > $self->len_epsilon;
  115. last if abs(PI - angle3points(@points[$j-1, $j-2, $j]) - $rel_angle) > $self->angle_epsilon;
  116. $end = $j;
  117. }
  118. }
  119. }
  120. if (defined $end && ($end - $i + 1) >= $self->min_segments) {
  121. my $arc = polyline_to_arc(Slic3r::Polyline->new(@points[($i-1)..$end]));
  122. if (1||$arc->angle >= $self->min_total_angle) {
  123. push @chunks, $arc;
  124. # continue scanning after arc points
  125. $i = $end;
  126. next;
  127. }
  128. }
  129. # if last chunk was a polyline, append to it
  130. if (@chunks && $chunks[-1]->isa('Slic3r::Polyline')) {
  131. $chunks[-1]->append($points[$i]);
  132. } else {
  133. push @chunks, Slic3r::Polyline->new(@points[($i-1)..$i]);
  134. }
  135. }
  136. return @chunks;
  137. }
  138. sub polyline_to_arc {
  139. my ($polyline) = @_;
  140. my @points = @$polyline;
  141. my $is_ccw = $points[2]->ccw(@points[0,1]) > 0;
  142. # to find the center, we intersect the perpendicular lines
  143. # passing by first and last vertex;
  144. # a better method would be to draw all the perpendicular lines
  145. # and find the centroid of the enclosed polygon, or to
  146. # intersect multiple lines and find the centroid of the convex hull
  147. # around the intersections
  148. my $arc_center;
  149. {
  150. my $first_ray = Slic3r::Line->new(@points[0,1]);
  151. $first_ray->rotate(PI/2 * ($is_ccw ? 1 : -1), $points[0]);
  152. my $last_ray = Slic3r::Line->new(@points[-2,-1]);
  153. $last_ray->rotate(PI/2 * ($is_ccw ? -1 : 1), $points[-1]);
  154. # require non-parallel rays in order to compute an accurate center
  155. return if abs($first_ray->atan2_ - $last_ray->atan2_) < deg2rad(30);
  156. $arc_center = $first_ray->intersection($last_ray, 0) or return;
  157. }
  158. # angle measured in ccw orientation
  159. my $abs_angle = Slic3r::Geometry::angle3points($arc_center, @points[0,-1]);
  160. my $rel_angle = $is_ccw
  161. ? $abs_angle
  162. : (2*PI - $abs_angle);
  163. my $arc = Slic3r::GCode::ArcFitting::Arc->new(
  164. start => $points[0]->clone,
  165. end => $points[-1]->clone,
  166. center => $arc_center,
  167. is_ccw => $is_ccw || 0,
  168. angle => $rel_angle,
  169. );
  170. if (0) {
  171. printf "points = %d, path length = %f, arc angle = %f, arc length = %f\n",
  172. scalar(@points),
  173. unscale(Slic3r::Polyline->new(@points)->length),
  174. Slic3r::Geometry::rad2deg($rel_angle),
  175. unscale($arc->length);
  176. }
  177. return $arc;
  178. }
  179. package Slic3r::GCode::ArcFitting::Arc;
  180. use Moo;
  181. has 'start' => (is => 'ro', required => 1);
  182. has 'end' => (is => 'ro', required => 1);
  183. has 'center' => (is => 'ro', required => 1);
  184. has 'is_ccw' => (is => 'ro', required => 1);
  185. has 'angle' => (is => 'ro', required => 1);
  186. sub radius {
  187. my ($self) = @_;
  188. return $self->start->distance_to($self->center);
  189. }
  190. sub length {
  191. my ($self) = @_;
  192. return $self->radius * $self->angle;
  193. }
  194. 1;