toolpath.pl
#!/usr/bin/perl
require "getopts.pl";
require "morph.ph";
require "funcs.ph";
use strict;
use rhino_parse;
use gcode_write;
use polygon;
use File::Copy;
# ./toolpath.pl -u laptop -i tmp1.3dm -d tmp2.3dm -r -g thing.ngc
# or
# ./toolpath.pl -c config.file
# tour methods
# salesman
# grid_tour
# line_tour
&Getopts('i:d:g:P:tcv');
use vars qw($opt_i $opt_d $opt_g $opt_c $opt_v $opt_P $opt_t $global);
my ($global);
my ($second, $minute, $hour) = localtime();
$global->{'total_salesman'} = 0;
$global->{'clock_time'} = "$hour:$minute:$second";
$global->{'verbose'} = 0;
$global->{'tour_executable'} = "/usr/bin/LKH.UNIX";
$global->{'default_move_feed_rate'} = 30;
$global->{'default_cut_feed_rate'} = 10;
$global->{'tag'} = "";
my $cuts_layer = "CUTS";
my $parts_layer = "PARTS";
my $path_layer = "PATH";
my $tour_method = "salesman";
my (%config, $input_file, $gcode_file, $dump_file, $base_url);
# opt start...
$global->{'verbose'} = 1 if ($opt_v == 1);
$global->{'report_tag'} = 1 if ($opt_t == 1);
if ($opt_c == 1) {
# this only works when called from axis
# could create another option to pass this the
# .ini file explicitly
foreach my $key (sort(keys %ENV)) {
$opt_c = $ENV{$key} if ($key eq "INI_FILE_NAME");
}
die("cant find config file") if ($opt_c == 1 && $global->{'verbose'} != 0);
exit() if ($opt_c == 1);
print "Found config file: $opt_c\n" if ( $global->{'verbose'} != 0);
# config file
open(F_IN, "$opt_c") || die("cant open: $opt_c");
while(<F_IN>) {
s/\n//;
# print $_ . "\n";
if (/^[^\#]/ && /(.*)\s+=\s+(.*)/) {
# print "HERE: $1 : $2\n";
$config{$1} = $2;
}
}
close(F_IN);
}
if(length($opt_i) > 0) {
$input_file = $opt_i;
}
elsif (length($config{'TOOLPATH_INPUT_FILE'}) > 0) {
$input_file = $config{'TOOLPATH_INPUT_FILE'};
if (length($config{'TOOLPATH_INPUT_DIR'}) > 0) {
$input_file = $config{'TOOLPATH_INPUT_DIR'} . "/" . $input_file;
}
}
else {
die "must supply an input file with -i";
}
my $gcode_deposit_dir;
if (length($config{'TOOLPATH_GCODE_FILE'}) > 0) {
$gcode_file = $config{'TOOLPATH_GCODE_FILE'};
if (length($config{'PROGRAM_PREFIX'}) > 0) {
$gcode_deposit_dir = $config{'PROGRAM_PREFIX'};
}
}
elsif (length($opt_g) > 0) {
$gcode_file = $opt_g;
}
else {}
if (length($config{'TOOLPATH_DUMP_FILE'}) > 0) {
$dump_file = $config{'TOOLPATH_DUMP_FILE'};
if (length($config{'TOOLPATH_INPUT_DIR'}) > 0) {
$dump_file = $config{'TOOLPATH_INPUT_DIR'} . "/" . $dump_file;
}
}
elsif (length($opt_d) > 0) {
$dump_file = $opt_d;
}
else {}
$global->{'default_power_setting'} = 95;
if (length($opt_P) > 0) {
$global->{'default_power_setting'} = $opt_P;
}
if (length($config{'GCODE_DWELL_TIME'}) > 0) {
$global->{'gcode_dwell_time'} = $config{'GCODE_DWELL_TIME'};
}
else {
$global->{'gcode_dwell_time'} = 0.2;
}
$global->{'t_str'} = "";
toolpath( $gcode_deposit_dir, $input_file, $gcode_file, $cuts_layer, $path_layer, $tour_method, $dump_file);
print $global->{'t_str'} if ($global->{'verbose'} != 0);
# --------------------------------------------------
sub toolpath {
($gcode_deposit_dir,
$input_file,
$gcode_file,
$cuts_layer,
$path_layer,
$tour_method,
$dump_file) = @_;
my $start_time = time();
my $file_struct = rhino_parse->new($input_file);
my $polyline_struct = polygon->new();
$polyline_struct->set($file_struct);
my $dump = rhino_parse->new();
$dump->add_layer($cuts_layer, "255 0 0");
$dump->add_layer($parts_layer, "0 0 255");
$dump->add_layer($path_layer, "0 255 0");
my $user_part_path;
my $gcode;
if (length($gcode_file) > 0) {
$gcode = gcode_write->new(1,$global);
}
my $points;
my $count = 0;
my $parts = polygon->new();
foreach my $p ($polyline_struct->polylines_by_layer($parts_layer)) {
$parts->add($polyline_struct->{$p});
$points->{$count}->{'x'} = $polyline_struct->{$p}->{0}->{'x'};
$points->{$count}->{'y'} = $polyline_struct->{$p}->{0}->{'y'};
$count++;
}
$points->{'count'} = $count;
my $cuts = polygon->new();
foreach my $p ($polyline_struct->polylines_by_layer($cuts_layer)) {
$cuts->add($polyline_struct->{$p});
}
my($user_part_path) = $polyline_struct->get_vertices_by_layer($path_layer);
my @tour = create_tour(points => $points,
tour_method => $tour_method,
part_path => $user_part_path,
parts => $parts,
origin_x => 0,
origin_y => 0);
if ($cuts->polyline_is_clockwise2d($points)) {
my @l = @tour;
@tour = (shift(@l), reverse(@l));
}
if ($parts->{'count'} != 0) {
cut_parts($dump, $parts, $cuts, \@tour, $gcode, $polyline_struct);
write_part_tour($dump, $points, @tour) if (length($dump_file) > 0);
if (defined($gcode)) {
$global->{'tag'} = $gcode->write_gcode($gcode_file, $global->{'clock_time'});
my $f = $gcode_deposit_dir . "/" . $gcode_file;
move($gcode_file, $f);
}
}
else {
$global->{'t_str'} .= "ERROR: got no parts to cut on layer: $parts_layer"
}
my $response;
$global->{'t_str'} .= "look for this tag:: " if ($global->{'verbose'} != 0);
$global->{'t_str'} .= "$global->{'tag'}\n" if ($global->{'verbose'} != 0);
$global->{'t_str'} .= "DONE\n" if ($global->{'verbose'} != 0);
print "TAG :: $global->{'tag'}\n" if ($global->{'report_tag'} != 0);
}
sub cut_parts {
my($dump, $parts, $cuts, $tour, $gcode, $polyline_struct) = @_;
my $done;
foreach my $part (@$tour) {
$global->{'t_str'} .= " part: $part\n" if ($global->{'verbose'} != 0);
my $cut_set = polygon->new();
my $points;
my $count = 0;
foreach my $c (polylines_overlap_polyline($cuts,
$parts->{$part},
$polyline_struct)) {
if ($done->{$c} != 1) {
$points->{$count}->{'x'} = $cuts->{$c}->{0}->{'x'};
$points->{$count}->{'y'} = $cuts->{$c}->{0}->{'y'};
$points->{$count}->{'part_number'} = $c;
$count++;
} else {
# for some reason cuts get found more than once.
}
$done->{$c} = 1;
}
$points->{'count'} = $count;
my $x = $parts->{$part}->{$parts->{$part}->{'count'}-1}->{'x'};
my $y = $parts->{$part}->{$parts->{$part}->{'count'}-1}->{'y'};
if ($points->{'count'} > 0) {
$global->{'t_str'} .= " Creating part path... $count cuts\n" if ($global->{'verbose'} != 0);
my @l = create_tour(points => $points,
tour_method => "salesman",
origin_x => $x,
origin_y => $y);
$global->{'t_str'} .= " ...done\n" if ($global->{'verbose'} != 0);
foreach my $p (@l) {
my $ptr = $cuts->{$points->{$p}->{'part_number'}};
$dump->add_polygon($ptr, "CUTS") if (length($dump_file) > 0);
$gcode->write_gcode_polyline($ptr) if (defined($gcode));
}
write_part_tour($dump, $points, @l) if (length($dump_file) > 0);
}
else {
$global->{'t_str'} .= " no cuts in part: $part\n" if ($global->{'verbose'} != 0);
}
$dump->add_polygon($parts->{$part}, "PARTS") if (length($dump_file) > 0);
$gcode->write_gcode_polyline($parts->{$part}) if (defined($gcode));
}
return($done);
}
sub _get_distance {
my ($x1, $y1, $x2, $y2) = @_;
my $xdistance=abs(($x1-$x2)*($x1-$x2));
my $ydistance=abs(($y1-$y2)*($y1-$y2));
return (sqrt($xdistance+$ydistance));
}
sub write_part_tour {
my($dump, $pts, @l) = @_;
my $count = 0;
my $path;
foreach my $part (@l) {
$path->{$count}->{'x'} = $pts->{$part}->{'x'};
$path->{$count}->{'y'} = $pts->{$part}->{'y'};
$path->{$count}->{'z'} = 0;
$count++;
}
$path->{'count'} = $count;
$dump->add_polygon($path, "PATH");
}
sub polylines_overlap_polyline {
my($polylines, $part, $polyline_struct) = @_;
my (@l);
for (my $j=0; $j < $polylines->{'count'}; $j++) {
my $ptr = $polylines->{$j};
if ($polyline_struct->polyline_overlaps($ptr, $part)) {
push(@l, $j);
}
}
return(@l);
}
sub name_gcodefile {
my($n, $count) = @_;
if ($n =~ /(\..*)/) {
my $ext = $1;
$n =~ s/\..*//;
$n = $n . "_" . $count . $ext;
} else {
die ("was expecting some type of extension in gcode file name (e.g., \".ngc\")");
}
return($n);
}
sub create_tour {
my $opts;
while (@_ && !ref $_[0]) {
my $key = shift;
$opts->{$key} = shift;
}
my $points = $opts->{points};
$global->{'t_str'} .= "ERROR no points sent to create_tour\n" unless $points;
if ($opts->{part_path}) {
$opts->{tour_method} = "line_tour";
}
my $tour_method = $opts->{tour_method};
$global->{'t_str'} .= "ERROR no method sent to create_tour" unless $tour_method;
my @l;
if ($tour_method eq "salesman") {
@l = create_salesman_tour($points);
} elsif ($tour_method eq "line_tour") {
@l = tour_parts_by_line($opts);
} else {
@l = create_dumb_tour($points);
}
if (length($opts->{origin_x}) > 0 && length($opts->{origin_y}) > 0) {
@l = rotate_loop($opts->{origin_x}, $opts->{origin_y}, $points, @l);
}
return(@l);
}
sub rotate_loop {
my($x, $y, $points, @list) = @_;
my ($i, $item, $d1, $d2, $count);
my $d2 = _get_distance($x,$y,$points->{0}->{'x'},$points->{0}->{'y'});
$item = 0;
$count = 0;
foreach $i (@list) {
$d1 = _get_distance($x,$y,
$points->{$i}->{'x'},
$points->{$i}->{'y'});
if ($d1 < $d2) {
$item = $count;
$d2 = $d1;
}
$count++;
}
my @l;
for($i=$item; $i<$points->{'count'}; $i++) {
push(@l, $list[$i]);
}
for($i=0; $i<$item; $i++) {
push(@l, $list[$i]);
}
return(@l);
}
sub tour_parts_by_line {
my $opts = shift;
my $path = $opts->{part_path};
$global->{'t_str'} .= "ERROR no part_path sent to tour_parts_by_line" unless $path;
my $parts = $opts->{parts};
$global->{'t_str'} .= "ERROR no parts sent to tour_parts_by_line" unless $parts;
my $self = $opts->{parts};
my ($line, $done, @list);
for (my $i=0;$i<$path->{'count'}-1;$i++) {
$line->{0}->{'x'} = $path->{$i}->{'x'};
$line->{0}->{'y'} = $path->{$i}->{'y'};
$line->{1}->{'x'} = $path->{$i + 1}->{'x'};
$line->{1}->{'y'} = $path->{$i + 1}->{'y'};
$line->{'count'} = 2;
my $x = $line->{0}->{'x'};
my $y = $line->{0}->{'y'};
my @tmp;
for(my $j=0;$j<$parts->{'count'};$j++) {
if ($done->{$j} != 1) {
my $ptr = $parts->{$j};
my $box = _get_bounding_box($ptr);
my $r = $self->polyline_crosses($line, $box);
if ($r) {
my $dist = _get_nearest_dist($x, $y, $r);
push(@tmp, $dist . " " . $j);
$done->{$j} = 1;
}
}
}
foreach my $e (sort by_number (@tmp)) {
$e =~ s/.* //;
push(@list, $e);
}
}
return(@list);
}
sub _get_nearest_dist {
my($x, $y, $ptr) = @_;
my ($i, $d_sq);
my $rot = 0;
my $dmin_sq = sqrt(($ptr->{0}->{'x'}-$x)**2 + ($ptr->{0}->{'y'}-$y)**2);
for(my $i=0; $i < $ptr->{'count'}; $i++) {
$d_sq = sqrt(($ptr->{$i}->{'x'}-$x)**2 + ($ptr->{$i}->{'y'}-$y)**2);
$dmin_sq = min($dmin_sq, $d_sq);
}
return($dmin_sq);
}
sub _get_bounding_box {
my $p = shift;
my $b;
$b->{0}->{'x'} = $p->{'min_x'};
$b->{0}->{'y'} = $p->{'min_y'};
$b->{1}->{'x'} = $p->{'max_x'};
$b->{1}->{'y'} = $p->{'min_y'};
$b->{2}->{'x'} = $p->{'max_x'};
$b->{2}->{'y'} = $p->{'max_y'};
$b->{3}->{'x'} = $p->{'min_x'};
$b->{3}->{'y'} = $p->{'max_y'};
$b->{4}->{'x'} = $p->{'min_x'};
$b->{4}->{'y'} = $p->{'min_y'};
$b->{'count'} = 5;
return($b);
}
sub dump_polyline_struct {
my ($s) = shift;
for(my $j=0; $j < $s->{'count'}; $j++) {
$global->{'t_str'} .= "LINE: $j\n" if ($global->{'verbose'} != 0);
dump_vector($s->{$j});
}
}
sub dump_vector {
my ($p) = shift;
for(my $j=0; $j < $p->{'count'}; $j++) {
$global->{'t_str'} .= "$j :: $p->{$j}->{'x'} :: $p->{$j}->{'y'}\n" if ($global->{'verbose'} != 0);
}
}
sub create_dumb_tour {
my($points) = @_;
# deal with situations with small number of datapoints
if ($points->{'count'} == 1) {
return(0);
}
if ($points->{'count'} == 0) {
$global->{'t_str'} .= "ERROR call to create tour with polyline containing no points\n";
exit(1);
}
my @l;
my $i;
for($i=0;$i<$points->{'count'};$i++) {
push (@l, $points->{$i}->{'x'} . " " . $i);
}
my(@l2);
foreach $i (sort by_number (@l)) {
$i =~ s/.* //;
push(@l2, $i);
}
return(@l2);
}
# This traveling salesman solution (LKH.UNIX) thanks to Keld Helsgaun.
# http://www.akira.ruc.dk/~keld/
sub create_salesman_tour {
my($points) = @_;
# deal with situations with small number of datapoints
if ($points->{'count'} == 1) {
return(0);
}
my $start_time = time();
my $param_file = "tmp_param_file";
my $input_file = "tmp_input_file";
my $output_file = "tmp_output_file";
open(PAR_FILE,">$param_file") || die("cant write file: $param_file");
print PAR_FILE "PROBLEM_FILE = $input_file\n";
print PAR_FILE "TOUR_FILE = $output_file\n";
close(PAR_FILE);
if ($points->{'count'} == 0) {
$global->{'t_str'} .= "ERROR call to create tour with polyline containing no points\n";
}
if ($points->{'count'} == 2) {
return(0,1);
}
open(F_OUT, ">$input_file") || die("cant write: $input_file");
print F_OUT "NAME : inputfile\n";
print F_OUT "TYPE : TSP\n";
print F_OUT "COMMENT : COMMENT\n";
print F_OUT "DIMENSION : $points->{'count'}\n";
print F_OUT "EDGE_WEIGHT_TYPE : EUC_2D\n";
print F_OUT "NODE_COORD_SECTION\n";
my ($t1, $t2);
if ($points->{'count'} > 120) {
$global->{'t_str'} .= "ERROR Be advised: the toolpath is processing $points->{'count'} points. Lists with\n";
$global->{'t_str'} .= "ERROR that many vertices require a bit of a wait.\n";
$t1 = time();
}
my $i;
for($i=0;$i<$points->{'count'};$i++) {
printf F_OUT ("%d %lf %lf\n",
$i+1,
$points->{$i}->{'x'},
$points->{$i}->{'y'});
}
print F_OUT "EOF\n";
close(F_OUT);
my $cmd = "echo $param_file | $global->{'tour_executable'} > /dev/null";
system($cmd);
my(@l);
my $start = 0;
open(F_IN, "$output_file") || die("cant open: $output_file");
while(<F_IN>) {
s/\n//;
$start = 0 if(/-1/);
if ($start == 1) {
my $n = int($_-1);
push(@l,$n);
}
$start = 1 if(/TOUR_SECTION/);
}
close(F_IN);
if (length($t1) > 0) {
$t2 = time();
$global->{'t_str'} .= ("WARNING: %d seconds TSP time\n", $t2 - $t1);
}
unlink($param_file);
unlink($input_file);
unlink($output_file);
my $end = time();
$global->{'total_salesman'} += ($end - $start_time);
return(@l);
}