mirror of
https://github.com/kristov/ldraw2stl.git
synced 2025-05-15 14:20:11 -07:00
Initial commit
This commit is contained in:
commit
b76f67e092
15
README.md
Normal file
15
README.md
Normal file
@ -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.
|
59
bin/dat2stl
Executable file
59
bin/dat2stl
Executable file
@ -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 <<END;
|
||||
Usage: $0 --file <input file> [--scale=<N> --ldrawdir=/usr/share/ldraw]
|
||||
|
||||
Takes an ldraw part .dat file as input and converts it into an STL file.
|
||||
|
||||
--file <string>
|
||||
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 <int>
|
||||
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 <string>
|
||||
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;
|
330
lib/LDraw/Parser.pm
Normal file
330
lib/LDraw/Parser.pm
Normal file
@ -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;
|
Loading…
x
Reference in New Issue
Block a user