MotionPlanner.pm 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317
  1. package Slic3r::GCode::MotionPlanner;
  2. use Moo;
  3. has 'islands' => (is => 'ro', required => 1); # arrayref of ExPolygons
  4. has 'internal' => (is => 'ro', default => sub { 1 });
  5. has '_space' => (is => 'ro', default => sub { Slic3r::GCode::MotionPlanner::ConfigurationSpace->new });
  6. has '_inner' => (is => 'ro', default => sub { [] }); # arrayref of ExPolygons
  7. use List::Util qw(first max);
  8. use Slic3r::Geometry qw(A B scale epsilon);
  9. use Slic3r::Geometry::Clipper qw(offset offset_ex diff_ex intersection_pl);
  10. # clearance (in mm) from the perimeters
  11. has '_inner_margin' => (is => 'ro', default => sub { scale 1 });
  12. has '_outer_margin' => (is => 'ro', default => sub { scale 2 });
  13. # this factor weigths the crossing of a perimeter
  14. # vs. the alternative path. a value of 5 means that
  15. # a perimeter will be crossed if the alternative path
  16. # is >= 5x the length of the straight line we could
  17. # follow if we decided to cross the perimeter.
  18. # a nearly-infinite value for this will only permit
  19. # perimeter crossing when there's no alternative path.
  20. use constant CROSSING_PENALTY => 20;
  21. use constant POINT_DISTANCE => 10; # unscaled
  22. # setup our configuration space
  23. sub BUILD {
  24. my $self = shift;
  25. my $point_distance = scale POINT_DISTANCE;
  26. my $nodes = $self->_space->nodes;
  27. my $edges = $self->_space->edges;
  28. # process individual islands
  29. for my $i (0 .. $#{$self->islands}) {
  30. my $expolygon = $self->islands->[$i];
  31. # find external margin
  32. my $outer = offset([ @$expolygon ], +$self->_outer_margin);
  33. my @outer_points = map @{$_->equally_spaced_points($point_distance)}, @$outer;
  34. # add outer points to graph
  35. my $o_outer = $self->_space->add_nodes(@outer_points);
  36. # find pairs of visible outer points and add them to the graph
  37. for my $i (0 .. $#outer_points) {
  38. for my $j (($i+1) .. $#outer_points) {
  39. my ($a, $b) = ($outer_points[$i], $outer_points[$j]);
  40. my $line = Slic3r::Polyline->new($a, $b);
  41. # outer points are visible when their line has empty intersection with islands
  42. my $intersection = intersection_pl(
  43. [ $line ],
  44. [ map @$_, @{$self->islands} ],
  45. );
  46. if (!@$intersection) {
  47. $self->_space->add_edge($i+$o_outer, $j+$o_outer, $line->length);
  48. }
  49. }
  50. }
  51. if ($self->internal) {
  52. # find internal margin
  53. my $inner = offset_ex([ @$expolygon ], -$self->_inner_margin);
  54. push @{ $self->_inner }, @$inner;
  55. my @inner_points = map @{$_->equally_spaced_points($point_distance)}, map @$_, @$inner;
  56. # add points to graph and get their offset
  57. my $o_inner = $self->_space->add_nodes(@inner_points);
  58. # find pairs of visible inner points and add them to the graph
  59. for my $i (0 .. $#inner_points) {
  60. for my $j (($i+1) .. $#inner_points) {
  61. my ($a, $b) = ($inner_points[$i], $inner_points[$j]);
  62. my $line = Slic3r::Line->new($a, $b);
  63. # turn $inner into an ExPolygonCollection and use $inner->contains_line()
  64. if (first { $_->contains_line($line) } @$inner) {
  65. $self->_space->add_edge($i+$o_inner, $j+$o_inner, $line->length);
  66. }
  67. }
  68. }
  69. # generate the stripe around slice contours
  70. my $contour = diff_ex(
  71. $outer,
  72. [ map @$_, @$inner ],
  73. );
  74. # find pairs of visible points in this area and add them to the graph
  75. for my $i (0 .. $#inner_points) {
  76. for my $j (0 .. $#outer_points) {
  77. my ($a, $b) = ($inner_points[$i], $outer_points[$j]);
  78. my $line = Slic3r::Line->new($a, $b);
  79. # turn $contour into an ExPolygonCollection and use $contour->contains_line()
  80. if (first { $_->contains_line($line) } @$contour) {
  81. $self->_space->add_edge($i+$o_inner, $j+$o_outer, $line->length * CROSSING_PENALTY);
  82. }
  83. }
  84. }
  85. }
  86. }
  87. # since Perl has no infinity symbol and we don't want to overcomplicate
  88. # the Dijkstra algorithm with string constants or -1 values
  89. $self->_space->_infinity(10 * (max(map values %$_, values %{$self->_space->edges}) // 0));
  90. if (0) {
  91. require "Slic3r/SVG.pm";
  92. Slic3r::SVG::output("space.svg",
  93. no_arrows => 1,
  94. expolygons => $self->islands,
  95. lines => $self->_space->get_lines,
  96. points => $self->_space->nodes,
  97. );
  98. printf "%d islands\n", scalar @{$self->islands};
  99. eval "use Devel::Size";
  100. print "MEMORY USAGE:\n";
  101. printf " %-19s = %.1fMb\n", $_, Devel::Size::total_size($self->$_)/1024/1024
  102. for qw(_space islands);
  103. printf " %-19s = %.1fMb\n", $_, Devel::Size::total_size($self->_space->$_)/1024/1024
  104. for qw(nodes edges);
  105. printf " %-19s = %.1fMb\n", 'self', Devel::Size::total_size($self)/1024/1024;
  106. exit if $self->internal;
  107. }
  108. }
  109. sub shortest_path {
  110. my $self = shift;
  111. my ($from, $to) = @_;
  112. return Slic3r::Polyline->new($from, $to)
  113. if !@{$self->_space->nodes};
  114. # create a temporary configuration space
  115. my $space = $self->_space->clone;
  116. # add from/to points to the temporary configuration space
  117. my $node_from = $self->_add_point_to_space($from, $space);
  118. my $node_to = $self->_add_point_to_space($to, $space);
  119. # compute shortest path
  120. my $path = $space->shortest_path($node_from, $node_to);
  121. if (!$path->is_valid) {
  122. Slic3r::debugf "Failed to compute shortest path.\n";
  123. return Slic3r::Polyline->new($from, $to);
  124. }
  125. if (0) {
  126. require "Slic3r/SVG.pm";
  127. Slic3r::SVG::output("path.svg",
  128. no_arrows => 1,
  129. expolygons => $self->islands,
  130. lines => $space->get_lines,
  131. red_points => [$from, $to],
  132. red_polylines => [$path],
  133. );
  134. exit;
  135. }
  136. return $path;
  137. }
  138. # returns the index of the new node
  139. sub _add_point_to_space {
  140. my ($self, $point, $space) = @_;
  141. my $n = $space->add_nodes($point);
  142. # check whether we are inside an island or outside
  143. my $inside = defined first { $self->islands->[$_]->contains_point($point) } 0..$#{$self->islands};
  144. # find candidates by checking visibility from $from to them
  145. foreach my $idx (0..$#{$space->nodes}) {
  146. my $line = Slic3r::Line->new($point, $space->nodes->[$idx]);
  147. # if $point is inside an island, it is visible from $idx when island contains their line
  148. # if $point is outside an island, it is visible from $idx when their line does not cross any island
  149. if (
  150. ($inside && defined first { $_->contains_line($line) } @{$self->_inner})
  151. || (!$inside && !@{intersection_pl(
  152. [ $line->as_polyline ],
  153. [ map @$_, @{$self->islands} ],
  154. )})
  155. ) {
  156. # $n ($point) and $idx are visible
  157. $space->add_edge($n, $idx, $line->length);
  158. }
  159. }
  160. # if we found no visibility, retry with larger margins
  161. if (!exists $space->edges->{$n} && $inside) {
  162. foreach my $idx (0..$#{$space->nodes}) {
  163. my $line = Slic3r::Line->new($point, $space->nodes->[$idx]);
  164. if (defined first { $_->contains_line($line) } @{$self->islands}) {
  165. # $n ($point) and $idx are visible
  166. $space->add_edge($n, $idx, $line->length);
  167. }
  168. }
  169. }
  170. warn "Temporary node is not visible from any other node"
  171. if !exists $space->edges->{$n};
  172. return $n;
  173. }
  174. package Slic3r::GCode::MotionPlanner::ConfigurationSpace;
  175. use Moo;
  176. has 'nodes' => (is => 'rw', default => sub { [] }); # [ Point, ... ]
  177. has 'edges' => (is => 'rw', default => sub { {} }); # node_idx => { node_idx => distance, ... }
  178. has '_infinity' => (is => 'rw');
  179. sub clone {
  180. my $self = shift;
  181. return (ref $self)->new(
  182. nodes => [ map $_->clone, @{$self->nodes} ],
  183. edges => { map { $_ => { %{$self->edges->{$_}} } } keys %{$self->edges} },
  184. _infinity => $self->_infinity,
  185. );
  186. }
  187. sub nodes_count {
  188. my $self = shift;
  189. return scalar(@{ $self->nodes });
  190. }
  191. sub add_nodes {
  192. my ($self, @nodes) = @_;
  193. my $offset = $self->nodes_count;
  194. push @{ $self->nodes }, @nodes;
  195. return $offset;
  196. }
  197. sub add_edge {
  198. my ($self, $a, $b, $dist) = @_;
  199. $self->edges->{$a}{$b} = $self->edges->{$b}{$a} = $dist;
  200. }
  201. sub shortest_path {
  202. my ($self, $node_from, $node_to) = @_;
  203. my $edges = $self->edges;
  204. my (%dist, %visited, %prev);
  205. $dist{$_} = $self->_infinity for keys %$edges;
  206. $dist{$node_from} = 0;
  207. my @queue = ($node_from);
  208. while (@queue) {
  209. my $u = -1;
  210. {
  211. # find node in @queue with smallest distance in %dist and has not been visited
  212. my $d = -1;
  213. foreach my $n (@queue) {
  214. next if $visited{$n};
  215. if ($u == -1 || $dist{$n} < $d) {
  216. $u = $n;
  217. $d = $dist{$n};
  218. }
  219. }
  220. }
  221. last if $u == $node_to;
  222. # remove $u from @queue
  223. @queue = grep $_ != $u, @queue;
  224. $visited{$u} = 1;
  225. # loop through neighbors of $u
  226. foreach my $v (keys %{ $edges->{$u} }) {
  227. my $alt = $dist{$u} + $edges->{$u}{$v};
  228. if ($alt < $dist{$v}) {
  229. $dist{$v} = $alt;
  230. $prev{$v} = $u;
  231. if (!$visited{$v}) {
  232. push @queue, $v;
  233. }
  234. }
  235. }
  236. }
  237. my @points = ();
  238. {
  239. my $u = $node_to;
  240. while (exists $prev{$u}) {
  241. unshift @points, $self->nodes->[$u];
  242. $u = $prev{$u};
  243. }
  244. unshift @points, $self->nodes->[$node_from];
  245. }
  246. return Slic3r::Polyline->new(@points);
  247. }
  248. # for debugging purposes
  249. sub get_lines {
  250. my $self = shift;
  251. my @lines = ();
  252. my %lines = ();
  253. for my $i (keys %{$self->edges}) {
  254. for my $j (keys %{$self->edges->{$i}}) {
  255. my $line_id = join '_', sort $i, $j;
  256. next if $lines{$line_id};
  257. $lines{$line_id} = 1;
  258. push @lines, Slic3r::Line->new(map $self->nodes->[$_], $i, $j);
  259. }
  260. }
  261. return [@lines];
  262. }
  263. 1;