123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317 |
- package Slic3r::GCode::MotionPlanner;
- use Moo;
- has 'islands' => (is => 'ro', required => 1); # arrayref of ExPolygons
- has 'internal' => (is => 'ro', default => sub { 1 });
- has '_space' => (is => 'ro', default => sub { Slic3r::GCode::MotionPlanner::ConfigurationSpace->new });
- has '_inner' => (is => 'ro', default => sub { [] }); # arrayref of ExPolygons
- use List::Util qw(first max);
- use Slic3r::Geometry qw(A B scale epsilon);
- use Slic3r::Geometry::Clipper qw(offset offset_ex diff_ex intersection_pl);
- # clearance (in mm) from the perimeters
- has '_inner_margin' => (is => 'ro', default => sub { scale 1 });
- has '_outer_margin' => (is => 'ro', default => sub { scale 2 });
- # this factor weigths the crossing of a perimeter
- # vs. the alternative path. a value of 5 means that
- # a perimeter will be crossed if the alternative path
- # is >= 5x the length of the straight line we could
- # follow if we decided to cross the perimeter.
- # a nearly-infinite value for this will only permit
- # perimeter crossing when there's no alternative path.
- use constant CROSSING_PENALTY => 20;
- use constant POINT_DISTANCE => 10; # unscaled
- # setup our configuration space
- sub BUILD {
- my $self = shift;
-
- my $point_distance = scale POINT_DISTANCE;
- my $nodes = $self->_space->nodes;
- my $edges = $self->_space->edges;
-
- # process individual islands
- for my $i (0 .. $#{$self->islands}) {
- my $expolygon = $self->islands->[$i];
-
- # find external margin
- my $outer = offset([ @$expolygon ], +$self->_outer_margin);
- my @outer_points = map @{$_->equally_spaced_points($point_distance)}, @$outer;
-
- # add outer points to graph
- my $o_outer = $self->_space->add_nodes(@outer_points);
-
- # find pairs of visible outer points and add them to the graph
- for my $i (0 .. $#outer_points) {
- for my $j (($i+1) .. $#outer_points) {
- my ($a, $b) = ($outer_points[$i], $outer_points[$j]);
- my $line = Slic3r::Polyline->new($a, $b);
- # outer points are visible when their line has empty intersection with islands
- my $intersection = intersection_pl(
- [ $line ],
- [ map @$_, @{$self->islands} ],
- );
- if (!@$intersection) {
- $self->_space->add_edge($i+$o_outer, $j+$o_outer, $line->length);
- }
- }
- }
-
- if ($self->internal) {
- # find internal margin
- my $inner = offset_ex([ @$expolygon ], -$self->_inner_margin);
- push @{ $self->_inner }, @$inner;
- my @inner_points = map @{$_->equally_spaced_points($point_distance)}, map @$_, @$inner;
-
- # add points to graph and get their offset
- my $o_inner = $self->_space->add_nodes(@inner_points);
-
- # find pairs of visible inner points and add them to the graph
- for my $i (0 .. $#inner_points) {
- for my $j (($i+1) .. $#inner_points) {
- my ($a, $b) = ($inner_points[$i], $inner_points[$j]);
- my $line = Slic3r::Line->new($a, $b);
- # turn $inner into an ExPolygonCollection and use $inner->contains_line()
- if (first { $_->contains_line($line) } @$inner) {
- $self->_space->add_edge($i+$o_inner, $j+$o_inner, $line->length);
- }
- }
- }
-
- # generate the stripe around slice contours
- my $contour = diff_ex(
- $outer,
- [ map @$_, @$inner ],
- );
-
- # find pairs of visible points in this area and add them to the graph
- for my $i (0 .. $#inner_points) {
- for my $j (0 .. $#outer_points) {
- my ($a, $b) = ($inner_points[$i], $outer_points[$j]);
- my $line = Slic3r::Line->new($a, $b);
- # turn $contour into an ExPolygonCollection and use $contour->contains_line()
- if (first { $_->contains_line($line) } @$contour) {
- $self->_space->add_edge($i+$o_inner, $j+$o_outer, $line->length * CROSSING_PENALTY);
- }
- }
- }
- }
- }
-
- # since Perl has no infinity symbol and we don't want to overcomplicate
- # the Dijkstra algorithm with string constants or -1 values
- $self->_space->_infinity(10 * (max(map values %$_, values %{$self->_space->edges}) // 0));
-
- if (0) {
- require "Slic3r/SVG.pm";
- Slic3r::SVG::output("space.svg",
- no_arrows => 1,
- expolygons => $self->islands,
- lines => $self->_space->get_lines,
- points => $self->_space->nodes,
- );
- printf "%d islands\n", scalar @{$self->islands};
-
- eval "use Devel::Size";
- print "MEMORY USAGE:\n";
- printf " %-19s = %.1fMb\n", $_, Devel::Size::total_size($self->$_)/1024/1024
- for qw(_space islands);
- printf " %-19s = %.1fMb\n", $_, Devel::Size::total_size($self->_space->$_)/1024/1024
- for qw(nodes edges);
- printf " %-19s = %.1fMb\n", 'self', Devel::Size::total_size($self)/1024/1024;
-
- exit if $self->internal;
- }
- }
- sub shortest_path {
- my $self = shift;
- my ($from, $to) = @_;
-
- return Slic3r::Polyline->new($from, $to)
- if !@{$self->_space->nodes};
-
- # create a temporary configuration space
- my $space = $self->_space->clone;
-
- # add from/to points to the temporary configuration space
- my $node_from = $self->_add_point_to_space($from, $space);
- my $node_to = $self->_add_point_to_space($to, $space);
-
- # compute shortest path
- my $path = $space->shortest_path($node_from, $node_to);
-
- if (!$path->is_valid) {
- Slic3r::debugf "Failed to compute shortest path.\n";
- return Slic3r::Polyline->new($from, $to);
- }
-
- if (0) {
- require "Slic3r/SVG.pm";
- Slic3r::SVG::output("path.svg",
- no_arrows => 1,
- expolygons => $self->islands,
- lines => $space->get_lines,
- red_points => [$from, $to],
- red_polylines => [$path],
- );
- exit;
- }
-
- return $path;
- }
- # returns the index of the new node
- sub _add_point_to_space {
- my ($self, $point, $space) = @_;
-
- my $n = $space->add_nodes($point);
-
- # check whether we are inside an island or outside
- my $inside = defined first { $self->islands->[$_]->contains_point($point) } 0..$#{$self->islands};
- # find candidates by checking visibility from $from to them
- foreach my $idx (0..$#{$space->nodes}) {
- my $line = Slic3r::Line->new($point, $space->nodes->[$idx]);
- # if $point is inside an island, it is visible from $idx when island contains their line
- # if $point is outside an island, it is visible from $idx when their line does not cross any island
- if (
- ($inside && defined first { $_->contains_line($line) } @{$self->_inner})
- || (!$inside && !@{intersection_pl(
- [ $line->as_polyline ],
- [ map @$_, @{$self->islands} ],
- )})
- ) {
- # $n ($point) and $idx are visible
- $space->add_edge($n, $idx, $line->length);
- }
- }
-
- # if we found no visibility, retry with larger margins
- if (!exists $space->edges->{$n} && $inside) {
- foreach my $idx (0..$#{$space->nodes}) {
- my $line = Slic3r::Line->new($point, $space->nodes->[$idx]);
- if (defined first { $_->contains_line($line) } @{$self->islands}) {
- # $n ($point) and $idx are visible
- $space->add_edge($n, $idx, $line->length);
- }
- }
- }
-
- warn "Temporary node is not visible from any other node"
- if !exists $space->edges->{$n};
-
- return $n;
- }
- package Slic3r::GCode::MotionPlanner::ConfigurationSpace;
- use Moo;
- has 'nodes' => (is => 'rw', default => sub { [] }); # [ Point, ... ]
- has 'edges' => (is => 'rw', default => sub { {} }); # node_idx => { node_idx => distance, ... }
- has '_infinity' => (is => 'rw');
- sub clone {
- my $self = shift;
-
- return (ref $self)->new(
- nodes => [ map $_->clone, @{$self->nodes} ],
- edges => { map { $_ => { %{$self->edges->{$_}} } } keys %{$self->edges} },
- _infinity => $self->_infinity,
- );
- }
- sub nodes_count {
- my $self = shift;
- return scalar(@{ $self->nodes });
- }
- sub add_nodes {
- my ($self, @nodes) = @_;
-
- my $offset = $self->nodes_count;
- push @{ $self->nodes }, @nodes;
- return $offset;
- }
- sub add_edge {
- my ($self, $a, $b, $dist) = @_;
- $self->edges->{$a}{$b} = $self->edges->{$b}{$a} = $dist;
- }
- sub shortest_path {
- my ($self, $node_from, $node_to) = @_;
-
- my $edges = $self->edges;
- my (%dist, %visited, %prev);
- $dist{$_} = $self->_infinity for keys %$edges;
- $dist{$node_from} = 0;
-
- my @queue = ($node_from);
- while (@queue) {
- my $u = -1;
- {
- # find node in @queue with smallest distance in %dist and has not been visited
- my $d = -1;
- foreach my $n (@queue) {
- next if $visited{$n};
- if ($u == -1 || $dist{$n} < $d) {
- $u = $n;
- $d = $dist{$n};
- }
- }
- }
- last if $u == $node_to;
-
- # remove $u from @queue
- @queue = grep $_ != $u, @queue;
- $visited{$u} = 1;
-
- # loop through neighbors of $u
- foreach my $v (keys %{ $edges->{$u} }) {
- my $alt = $dist{$u} + $edges->{$u}{$v};
- if ($alt < $dist{$v}) {
- $dist{$v} = $alt;
- $prev{$v} = $u;
- if (!$visited{$v}) {
- push @queue, $v;
- }
- }
- }
- }
-
- my @points = ();
- {
- my $u = $node_to;
- while (exists $prev{$u}) {
- unshift @points, $self->nodes->[$u];
- $u = $prev{$u};
- }
- unshift @points, $self->nodes->[$node_from];
- }
-
- return Slic3r::Polyline->new(@points);
- }
- # for debugging purposes
- sub get_lines {
- my $self = shift;
-
- my @lines = ();
- my %lines = ();
- for my $i (keys %{$self->edges}) {
- for my $j (keys %{$self->edges->{$i}}) {
- my $line_id = join '_', sort $i, $j;
- next if $lines{$line_id};
- $lines{$line_id} = 1;
- push @lines, Slic3r::Line->new(map $self->nodes->[$_], $i, $j);
- }
- }
-
- return [@lines];
- }
- 1;
|