More work on PathsToGCode

svg-paths
Alessandro Ranellucci 2013-12-05 01:24:07 +01:00
parent dcc89126cf
commit 42a2ea31ec
6 changed files with 354 additions and 15 deletions

View File

@ -1,25 +1,27 @@
package Slic3r::PathsToGCode;
use Moo;
use Slic3r::Geometry qw(X Y unscale);
use XML::SAX;
has 'start_gcode' => (is => 'ro', default => sub {<<"EOF"});
has 'start_gcode' => (is => 'ro', default => sub {<<"EOF"});
G21 ; set units to millimeters
G90 ; use absolute coordinates
EOF
has 'end_gcode' => (is => 'ro', default => sub {''});
has 'path_speed' => (is => 'ro'); # mm/sec
has 'travel_speed' => (is => 'ro'); # mm/sec
has 'placement' => (is => 'ro', default => sub { 'align' }); # center/align
has 'origin' => (is => 'ro', default => sub { Slic3r::Point->new(0,0) }); # scaled Slic3r::Point
has 'scale' => (is => 'ro', default => sub { 1 }); # factor
has 'end_gcode' => (is => 'ro', default => sub {''});
has 'before_path_gcode' => (is => 'ro', default => sub {''});
has 'after_path_gcode' => (is => 'ro', default => sub {''});
has 'placement' => (is => 'ro', default => sub { 'align' }); # center/align
has 'origin' => (is => 'ro', default => sub { Slic3r::Point->new(0,0) }); # scaled Slic3r::Point
has 'scale' => (is => 'ro', default => sub { 1 }); # factor
has 'reorder' => (is => 'ro', default => sub { 1 }); # bool
has 'start_near' => (is => 'ro', default => sub { Slic3r::Point->new(0,0) }); # scaled Slic3r::Point
sub process_svg {
my ($self, $svgfile, $gcodefile) = @_;
open my $fh, '>', $gcodefile or die "Failed to open output file $gcodefile\n";
print $fh $self->start_gcode;
printf $fh "G1 F%d\n", $self->speed * 60 if $self->speed;
my $parser = XML::SAX::ParserFactory->parser(
Handler => (my $handler = Slic3r::PathsToGCode::SVGParser->new),
@ -27,9 +29,31 @@ sub process_svg {
$parser->parse_uri($svgfile);
my $collection = Slic3r::Polyline::Collection->new(@{ $handler->{_paths} });
my $bb = $collection->bounding_box;
if ($self->placement eq 'align') {
$collection->scale($self->scale);
{
my $bb = $collection->bounding_box;
$collection->translate(-$bb->x_min + $self->origin->x, -$bb->y_min + $self->origin->y); #))
if ($self->placement eq 'center') {
my $size = $bb->size;
$collection->translate(-$size->[X]/2, -$size->[Y]/2);
}
}
# reorder paths if requested
my @polylines = ();
if ($self->reorder) {
my $c = $collection->chained_path_from($self->start_near, 0);
@polylines = map $_->clone, @$c;
} else {
@polylines = @$collection;
}
foreach my $polyline (@polylines) {
my @points = @$polyline;
printf $fh "G1 X%.4f Y%.4f\n", map unscale($_), @$_ for shift @points;
print $fh $self->before_path_gcode . "\n" if $self->before_path_gcode;
printf $fh "G1 X%.4f Y%.4f\n", map unscale($_), @$_ for @points;
print $fh $self->after_path_gcode . "\n" if $self->after_path_gcode;
}
close $fh;
@ -37,7 +61,8 @@ sub process_svg {
package Slic3r::PathsToGCode::SVGParser;
use base qw(XML::SAX::Base);
use Image::SVG::Path qw(extract_path_info);
use Image::SVG::Path qw();
use Slic3r::Geometry qw(X Y);
sub new {
my $self = shift->SUPER::new(@_);
@ -55,18 +80,253 @@ sub start_element {
foreach my $event (@path_info) {
if ($event->{type} eq 'moveto') {
push @polylines, Slic3r::Polyline->new;
$polylines[-1]->append(Slic3r::Point->new_scale(@{ $event->{point} }));
$polylines[-1]->append(Slic3r::Point->new_scale($event->{point}[X], -$event->{point}[Y]));
} elsif ($event->{type} =~ /cubic-bezier/ || $event->{type} eq 'line-to') {
$polylines[-1]->append(Slic3r::Point->new_scale(@{ $event->{end} }));
$polylines[-1]->append(Slic3r::Point->new_scale($event->{end}[X], -$event->{end}[Y]));
} elsif ($event->{type} eq 'closepath') {
$polylines[-1]->append($polylines[-1]->first_point);
} else {
die "Failed to parse path item " . $event->{type};
}
}
push @{$self->{_paths}}, @polylines;
} elsif ($el->{LocalName} eq 'polyline') {
my @points = map { my @c = split /,/; Slic3r::Point->new_scale($c[0], -$c[1]) }
split /\s+/, $el->{Attributes}{'{}points'}{Value};
push @{$self->{_paths}}, Slic3r::Polyline->new(@points);
}
}
## The following is temporarily extracted from Image::SVG::Path
## waiting for my patch to be merged.
use Carp;
sub position_type
{
my ($curve_type) = @_;
if (lc $curve_type eq $curve_type) {
return "relative";
}
elsif (uc $curve_type eq $curve_type) {
return "absolute";
}
else {
croak "I don't know what to do with '$curve_type'";
}
}
sub add_coords
{
my ($first_ref, $to_add_ref) = @_;
$first_ref->[0] += $to_add_ref->[0];
$first_ref->[1] += $to_add_ref->[1];
}
# The following regular expression splits the path into pieces
my $split_re = qr/(?:,|(?=-)|\s+)/;
sub extract_path_info
{
my ($path, $options_ref) = @_;
my $me = 'extract_path_info';
if (! $path) {
croak "$me: no input";
}
# Create an empty options so that we don't have to
# keep testing whether the "options" string is defined or not
# before trying to read a hash value from it.
if ($options_ref) {
if (ref $options_ref ne 'HASH') {
croak "$me: second argument should be a hash reference";
}
}
else {
$options_ref = {};
}
if (! wantarray) {
croak "$me: extract_path_info returns an array of values";
}
my $verbose = $options_ref->{verbose};
if ($verbose) {
print "$me: I am trying to split up '$path'.\n";
}
my @path_info;
my $has_moveto = ($path =~ /^([Mm])\s*,?\s*([-0-9.,]+)(.*)$/);
if (! $has_moveto) {
croak "No moveto at start of path '$path'";
}
my ($moveto_type, $move_to, $curves) = ($1, $2, $3);
if ($verbose) {
print "$me: The initial moveto looks like '$moveto_type' '$move_to'.\n";
}
# Deal with the initial "move to" command.
my $position = position_type ($moveto_type);
my ($x, $y) = split $split_re, $move_to, 2;
push @path_info, {
type => 'moveto',
position => $position,
point => [$x, $y],
svg_key => $moveto_type,
};
# Deal with the rest of the path.
my @curves;
while ($curves =~ /([cslqtahvz])\s*([-0-9.,\s]*)/gi) {
push @curves, [$1, $2];
}
if (@curves == 0) {
croak "No curves found in '$curves'";
}
for my $curve_data (@curves) {
my ($curve_type, $curve) = @$curve_data;
$curve =~ s/^,//;
my @numbers = split $split_re, $curve;
if ($verbose) {
print "Extracted numbers: @numbers\n";
}
if (uc $curve_type eq 'C') {
my $expect_numbers = 6;
if (@numbers % 6 != 0) {
croak "Wrong number of values for a C curve " .
scalar @numbers . " in '$path'";
}
my $position = position_type ($curve_type);
for (my $i = 0; $i < @numbers / 6; $i++) {
my $offset = 6 * $i;
my @control1 = @numbers[$offset + 0, $offset + 1];
my @control2 = @numbers[$offset + 2, $offset + 3];
my @end = @numbers[$offset + 4, $offset + 5];
# Put each of these abbreviated things into the list
# as a separate path.
push @path_info, {
type => 'cubic-bezier',
position => $position,
control1 => \@control1,
control2 => \@control2,
end => \@end,
svg_key => $curve_type,
};
}
}
elsif (uc $curve_type eq 'S') {
my $expect_numbers = 4;
if (@numbers % $expect_numbers != 0) {
croak "Wrong number of values for an S curve " .
scalar @numbers . " in '$path'";
}
my $position = position_type ($curve_type);
for (my $i = 0; $i < @numbers / $expect_numbers; $i++) {
my $offset = $expect_numbers * $i;
my @control2 = @numbers[$offset + 0, $offset + 1];
my @end = @numbers[$offset + 2, $offset + 3];
push @path_info, {
type => 'shortcut-cubic-bezier',
position => $position,
control2 => \@control2,
end => \@end,
svg_key => $curve_type,
};
}
}
elsif (uc $curve_type eq 'L') {
my $expect_numbers = 2;
if (@numbers % $expect_numbers != 0) {
croak "Wrong number of values for an L command " .
scalar @numbers . " in '$path'";
}
my $position = position_type ($curve_type);
for (my $i = 0; $i < @numbers / $expect_numbers; $i++) {
my $offset = $expect_numbers * $i;
push @path_info, {
type => 'line-to',
position => $position,
end => [@numbers[$offset, $offset + 1]],
svg_key => $curve_type,
}
}
}
elsif (uc $curve_type eq 'Z') {
if (@numbers > 0) {
croak "Wrong number of values for a Z command " .
scalar @numbers . " in '$path'";
}
my $position = position_type ($curve_type);
push @path_info, {
type => 'closepath',
position => $position,
svg_key => $curve_type,
}
}
else {
croak "I don't know what to do with a curve type '$curve_type'";
}
}
# Now sort it out if the user wants to get rid of the absolute
# paths etc.
my $absolute = $options_ref->{absolute};
my $no_shortcuts = $options_ref->{no_shortcuts};
if ($absolute) {
if ($verbose) {
print "Making all coordinates absolute.\n";
}
my $abs_pos;
my $previous;
for my $element (@path_info) {
if ($element->{type} eq 'moveto') {
if ($element->{position} eq 'relative') {
my $ip = $options_ref->{initial_position};
if ($ip) {
if (ref $ip ne 'ARRAY' ||
scalar @$ip != 2) {
croak "The initial position supplied doesn't look like a pair of coordinates";
}
add_coords ($element->{point}, $ip);
}
}
$abs_pos = $element->{point};
}
elsif ($element->{type} eq 'line-to') {
if ($element->{position} eq 'relative') {
add_coords ($element->{end}, $abs_pos);
}
$abs_pos = $element->{end};
}
elsif ($element->{type} eq 'cubic-bezier') {
if ($element->{position} eq 'relative') {
add_coords ($element->{control1}, $abs_pos);
add_coords ($element->{control2}, $abs_pos);
add_coords ($element->{end}, $abs_pos);
}
$abs_pos = $element->{end};
}
elsif ($element->{type} eq 'shortcut-cubic-bezier') {
if ($element->{position} eq 'relative') {
add_coords ($element->{control2}, $abs_pos);
add_coords ($element->{end}, $abs_pos);
}
if ($no_shortcuts) {
if (!$previous) {
die "No previous element";
}
if ($previous->{type} ne 'cubic-bezier') {
die "Bad previous element type $previous->{type}";
}
$element->{type} = 'cubic-bezier';
$element->{svg_key} = 'C';
$element->{control1} = [
2 * $abs_pos->[0] - $previous->{control2}->[0],
2 * $abs_pos->[1] - $previous->{control2}->[1],
];
}
$abs_pos = $element->{end};
}
$element->{position} = 'absolute';
$element->{svg_key} = uc $element->{svg_key};
$previous = $element;
}
}
return @path_info;
}
1;

View File

@ -33,4 +33,11 @@ sub align_to_origin {
return $self->translate(-$bb->x_min, -$bb->y_min);
}
package Slic3r::Polyline::Collection;
sub bounding_box {
my $self = shift;
return Slic3r::Geometry::BoundingBox->new_from_points([ map @$_, @$self ]);
}
1;

54
utils/svg-to-gcode.pl Normal file
View File

@ -0,0 +1,54 @@
#!/usr/bin/perl
# This script converts a SVG file to GCode
use strict;
use warnings;
BEGIN {
use FindBin;
use lib "$FindBin::Bin/../lib";
}
use Getopt::Long qw(:config no_auto_abbrev);
use Slic3r;
use Slic3r::PathsToGCode;
$|++;
my %opt = ();
{
my %options = (
'help' => sub { usage() },
'scale' => \$opt{scale},
);
GetOptions(%options) or usage(1);
$ARGV[0] or usage(1);
}
{
my $input_file = $ARGV[0];
my $output_file = $input_file;
$output_file =~ s/\.svg$/.gcode/i;
my $converter = Slic3r::PathsToGCode->new(
scale => $opt{scale} // 1,
);
$converter->process_svg($input_file, $output_file);
printf "G-code written to $output_file\n";
}
sub usage {
my ($exit_code) = @_;
print <<"EOF";
Usage: svg-to-gcode.pl [ OPTIONS ] file.svg
--help Output this usage screen and exit
--scale SCALE Scale factor (default: 1)
EOF
exit ($exit_code || 0);
}
__END__

View File

@ -2,6 +2,20 @@
namespace Slic3r {
void
PolylineCollection::scale(double factor)
{
for (Polylines::iterator it = this->polylines.begin(); it != this->polylines.end(); ++it)
it->scale(factor);
}
void
PolylineCollection::translate(double x, double y)
{
for (Polylines::iterator it = this->polylines.begin(); it != this->polylines.end(); ++it)
it->translate(x, y);
}
PolylineCollection*
PolylineCollection::chained_path(bool no_reverse) const
{

View File

@ -10,6 +10,8 @@ class PolylineCollection
{
public:
Polylines polylines;
void scale(double factor);
void translate(double x, double y);
PolylineCollection* chained_path(bool no_reverse) const;
PolylineCollection* chained_path_from(const Point* start_near, bool no_reverse) const;
Point* leftmost_point() const;

View File

@ -11,6 +11,8 @@
%code{% const char* CLASS = "Slic3r::Polyline::Collection"; RETVAL = new PolylineCollection(*THIS); %};
void clear()
%code{% THIS->polylines.clear(); %};
void scale(double factor);
void translate(double x, double y);
PolylineCollection* chained_path(bool no_reverse)
%code{% const char* CLASS = "Slic3r::Polyline::Collection"; RETVAL = THIS->chained_path(no_reverse); %};
PolylineCollection* chained_path_from(Point* start_near, bool no_reverse)