commit b76f67e09272daceff198b33c24af738128372f8 Author: ceade Date: Sat Aug 19 18:30:12 2017 +0200 Initial commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..c0f3f9b --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +# ldraw2stl + +Convert LEGO LDraw files to STL, for super-sizing and 3d printing!! + +1) Get the ldraw parts archive at http://www.ldraw.org or apt-get install ldraw-parts + +2) Install LeoCAD so you can find your parts + +3) Make a note of the .dat file name in LeoCAD, and then run: + + bin/dat2stl --file /usr/share/ldraw/parts/3894.dat --scale 4 > 3894.stl + +For a 4X scale one of those! + +Depends on Moose. diff --git a/bin/dat2stl b/bin/dat2stl new file mode 100755 index 0000000..2fa5443 --- /dev/null +++ b/bin/dat2stl @@ -0,0 +1,59 @@ +#!/usr/bin/perl + +use strict; +use warnings; +use FindBin; +use lib "$FindBin::Bin/../lib"; +use LDraw::Parser; +use Getopt::Long; + +my $opts = {}; +GetOptions( + $opts, + 'help', + 'scale=s', + 'ldrawdir=s', + 'file=s', +); + +if ( $opts->{help} ) { + print_usage(); + exit 0; +} + +if ( !$opts->{file} ) { + print "ERROR: --file is required! (try --help)\n"; + exit 1; +} + +sub print_usage { + print < [--scale= --ldrawdir=/usr/share/ldraw] + +Takes an ldraw part .dat file as input and converts it into an STL file. + + --file + The full path to the input .dat part file. Regardless of where this file + is, sub-parts of the file will be searched in --ldrawdir. + + --scale + Also scale the STL by N. This is separate from the LDU scaling that is + used to convert internally from LDU (LDraw Unit) to mm (STL). + + --ldrawdir + The location of the ldraw parts library package. Note: it is expected + that this contains the directories "p", "parts" and "models". The Debian + non-free package "ldraw-parts" installs to /usr/share/ldraw and that is + the default value for this tool. + +END +} + +my $parser = LDraw::Parser->new( { + file => $opts->{file}, + $opts->{scale} ? ( scale => $opts->{scale} ) : (), + $opts->{ldrawdir} ? ( ldraw_path => $opts->{ldrawdir} ) : (), +} ); + +$parser->parse; +print $parser->to_stl; diff --git a/lib/LDraw/Parser.pm b/lib/LDraw/Parser.pm new file mode 100644 index 0000000..9dc3aaa --- /dev/null +++ b/lib/LDraw/Parser.pm @@ -0,0 +1,330 @@ +package LDraw::Parser; + +use Moose; + +has file => ( + is => 'ro', + isa => 'Str', + required => 1, + documentation => 'The file to parse', +); + +has ldraw_path => ( + is => 'ro', + isa => 'Str', + default => '/usr/share/ldraw', + documentation => 'Where to find ldraw files', +); + +has scale => ( + is => 'rw', + isa => 'Num', + default => 1.0, + documentation => 'Scale the model', +); + +has mm_per_ldu => ( + is => 'rw', + isa => 'Num', + default => 0.4, + documentation => 'Number of mm per LDU (LDraw Unit)', +); + +has invert => ( + is => 'rw', + isa => 'Bool', + default => 0, + documentation => 'Invert this part', +); + +use constant X => 0; +use constant Y => 1; +use constant Z => 2; + +sub parse { + my ( $self ) = @_; + return $self->parse_file( $self->file ); +} + +sub parse_file { + my ( $self, $file ) = @_; + open( my $fh, '<', $file ) || die "$file: $!"; + $self->parse_handle( $fh ); + close $fh; +} + +sub parse_handle { + my ( $self, $handle ) = @_; + while ( my $line = <$handle> ) { + chomp $line; + $self->parse_line( $line ); + } +} + +sub parse_line { + my ( $self, $line ) = @_; + + $line =~ s/^\s+//; + + if ( $line =~ /^([0-9]+)\s+(.+)$/ ) { + my ( $line_type, $rest ) = ( $1, $2 ); + if ( $line_type == 0 ) { + $self->parse_comment_or_meta( $rest ); + } + elsif ( $line_type == 1 ) { + $self->parse_sub_file_reference( $rest ); + $self->invert( 0 ); + } + elsif ( $line_type == 2 ) { + $self->parse_line_command( $rest ); + } + elsif ( $line_type == 3 ) { + $self->parse_triange_command( $rest ); + } + elsif ( $line_type == 4 ) { + $self->parse_quadrilateral_command( $rest ); + } + elsif ( $line_type == 5 ) { + $self->parse_optional( $rest ); + } + else { + warn "unhandled line type: $line_type"; + } + } +} + +sub parse_comment_or_meta { + my ( $self, $rest ) = @_; + + my @items = split( /\s+/, $rest ); + my $first = shift @items; + + if ( $first && $first eq 'BFC' ) { + $self->handle_bfc_command( @items ); + } +} + +sub handle_bfc_command { + my ( $self, @items ) = @_; + + my $first = shift @items; + + if ( $first && $first eq 'INVERTNEXT' ) { + $self->invert( 1 ); + } +} + +sub parse_sub_file_reference { + my ( $self, $rest ) = @_; + # 16 0 -10 0 9 0 0 0 1 0 0 0 -9 2-4edge.dat + my @items = split( /\s+/, $rest ); + my $color = shift @items; + my $x = shift @items; + my $y = shift @items; + my $z = shift @items; + my $a = shift @items; + my $b = shift @items; + my $c = shift @items; + my $d = shift @items; + my $e = shift @items; + my $f = shift @items; + my $g = shift @items; + my $h = shift @items; + my $i = shift @items; + +# / a d g 0 \ / a b c x \ +# | b e h 0 | | d e f y | +# | c f i 0 | | g h i z | +# \ x y z 1 / \ 0 0 0 1 / + + my $mat = [ + $a, $b, $c, $x, + $d, $e, $f, $y, + $g, $h, $i, $z, + 0, 0, 0, 1, + ]; + + if ( scalar( @items ) != 1 ) { + warn "um, filename is made up of multiple parts (or none)"; + } + + my $filename = lc( $items[0] ); + $filename =~ s/\\/\//g; + + my $p_filename = join( '/', $self->ldraw_path, 'p', $filename ); + my $hires_filename = join( '/', $self->ldraw_path, 'p/48', $filename ); + my $parts_filename = join( '/', $self->ldraw_path, 'parts', $filename ); + my $models_filename = join( '/', $self->ldraw_path, 'models', $filename ); + + my $subpart_filename; + if ( -e $hires_filename ) { + $subpart_filename = $hires_filename; + } + elsif ( -e $p_filename ) { + $subpart_filename = $p_filename; + } + elsif (-e $parts_filename ) { + $subpart_filename = $parts_filename; + } + elsif ( -e $models_filename ) { + $subpart_filename = $models_filename; + } + else { + warn "unable to find file: $filename in normal paths"; + return; + } + + my $subparser = __PACKAGE__->new( { + file => $subpart_filename, + ldraw_path => $self->ldraw_path, + invert => $self->invert, + } ); + $subparser->parse; + + for my $triangle ( @{ $subparser->{triangles} } ) { + for my $vec ( @{ $triangle } ) { + my @new_vec = max4xv3( $mat, $vec ); + $vec->[0] = $new_vec[0]; + $vec->[1] = $new_vec[1]; + $vec->[2] = $new_vec[2]; + } + push @{ $self->{triangles} }, $triangle; + } +} + +sub parse_line_command { + my ( $self, $rest ) = @_; +} + +sub parse_triange_command { + my ( $self, $rest ) = @_; + # 16 8.9 -10 58.73 6.36 -10 53.64 9 -10 55.5 + my @items = split( /\s+/, $rest ); + my $color = shift @items; + my $p1 = [ $items[0], $items[1], $items[2] ]; + my $p2 = [ $items[3], $items[4], $items[5] ]; + my $p3 = [ $items[6], $items[7], $items[8] ]; + my $n = [ $self->calc_surface_normal( $p1, $p2, $p3 ) ]; + push @{ $self->{triangles} }, [ $p1, $p2, $p3, $n ]; +} + +sub parse_quadrilateral_command { + my ( $self, $rest ) = @_; + # 16 1.27 10 68.9 -6.363 10 66.363 10.6 10 79.2 7.1 10 73.27 + my @items = split( /\s+/, $rest ); + my $color = shift @items; + my $x1 = shift @items; + my $y1 = shift @items; + my $z1 = shift @items; + my $x2 = shift @items; + my $y2 = shift @items; + my $z2 = shift @items; + my $x3 = shift @items; + my $y3 = shift @items; + my $z3 = shift @items; + my $x4 = shift @items; + my $y4 = shift @items; + my $z4 = shift @items; + my $na = [ $self->calc_surface_normal( [ $x1, $y1, $z1 ], [ $x2, $y2, $z2 ], [ $x3, $y3, $z3 ] ) ]; + my $nb = [ $self->calc_surface_normal( [ $x3, $y3, $z3 ], [ $x4, $y4, $z4 ], [ $x1, $y1, $z1 ] ) ]; + push @{ $self->{triangles} }, [ + [ $x1, $y1, $z1 ], + [ $x2, $y2, $z2 ], + [ $x3, $y3, $z3 ], + $na, + ]; + push @{ $self->{triangles} }, [ + [ $x3, $y3, $z3 ], + [ $x4, $y4, $z4 ], + [ $x1, $y1, $z1 ], + $nb, + ]; +} + +sub WTF_parse_quadrilateral_command { + my ( $self, $rest ) = @_; + # 16 1.27 10 68.9 -6.363 10 66.363 10.6 10 79.2 7.1 10 73.27 + my @items = split( /\s+/, $rest ); + my $color = shift @items; + my $p1 = [ $items[0], $items[1], $items[2] ]; + my $p2 = [ $items[3], $items[4], $items[5] ]; + my $p3 = [ $items[6], $items[7], $items[8] ]; + my $p4 = [ $items[9], $items[10], $items[11] ]; + my $na = [ $self->calc_surface_normal( $p1, $p2, $p3 ) ]; + my $nb = [ $self->calc_surface_normal( $p3, $p4, $p1 ) ]; + push @{ $self->{triangles} }, [ $p1, $p2, $p3, $na ]; + push @{ $self->{triangles} }, [ $p3, $p4, $p1, $nb ]; +} + +sub parse_optional { + my ( $self, $rest ) = @_; +} + +sub calc_surface_normal { + my ( $self, $ip1, $ip2, $ip3 ) = @_; + + my ( $p1, $p2, $p3 ) = ( $ip1, $ip2, $ip3 ); + if ( $self->invert ) { + ( $p1, $p2, $p3 ) = ( $ip1, $ip3, $ip2 ); + } + + my ( $N, $U, $V ) = ( [], [], [] ); + + $U->[X] = $p2->[X] - $p1->[X]; + $U->[Y] = $p2->[Y] - $p1->[Y]; + $U->[Z] = $p2->[Z] - $p1->[Z]; + + $V->[X] = $p3->[X] - $p1->[X]; + $V->[Y] = $p3->[Y] - $p1->[Y]; + $V->[Z] = $p3->[Z] - $p1->[Z]; + + $N->[X] = $U->[Y] * $V->[Z] - $U->[Z] * $V->[Y]; + $N->[Y] = $U->[Z] * $V->[X] - $U->[X] * $V->[Z]; + $N->[Z] = $U->[X] * $V->[Y] - $U->[Y] * $V->[X]; + + return ( $N->[X], $N->[Y], $N->[Z] ); +} + +sub max4xv3 { + my ( $mat, $vec ) = @_; + + my ( $a1, $a2, $a3, $a4, + $b1, $b2, $b3, $b4, + $c1, $c2, $c3, $c4 ) = @{ $mat }; + + my ( $x_old, $y_old, $z_old ) = @{ $vec }; + + my $x_new = $a1 * $x_old + $a2 * $y_old + $a3 * $z_old + $a4; + my $y_new = $b1 * $x_old + $b2 * $y_old + $b3 * $z_old + $b4; + my $z_new = $c1 * $x_old + $c2 * $y_old + $c3 * $z_old + $c4; + + return ( $x_new, $y_new, $z_new ); +} + +sub to_stl { + my ( $self ) = @_; + + my $scale = $self->scale || 1; + my $mm_per_ldu = $self->mm_per_ldu; + + my $stl = ""; + $stl .= "solid GiantLegoRocks\n"; + + for my $triangle ( @{ $self->{triangles} } ) { + my ( $p1, $p2, $p3, $n ) = @{ $triangle }; + $stl .= "facet normal " . join( ' ', map { sprintf( '%0.4f', $_ ) } @{ $n } ) . "\n"; + $stl .= " outer loop\n"; + for my $vec ( ( $p1, $p2, $p3 ) ) { + my @transvec = map { sprintf( '%0.4f', $_ ) } map { $_ * $mm_per_ldu * $scale } @{ $vec }; + $stl .= " vertex " . join( ' ', @transvec ) . "\n"; + } + $stl .= " endloop\n"; + $stl .= "endfacet\n"; + } + + $stl .= "endsolid GiantLegoRocks\n"; + + return $stl; +} + +1;