package LDraw::Parser; use strict; use warnings; sub new { my ($class, $args) = @_; die "file required" unless $args->{file}; return bless({ file => $args->{file}, ldraw_path => $args->{ldraw_path} // '/usr/share/ldraw', scale => $args->{scale} // 1, mm_per_ldu => $args->{mm_per_ldu} // 0.4, invert => $args->{invert} // 0, debug => $args->{debug} // 0, d_indent => $args->{d_indent} // 0, _invertnext => 0, }, $class); } sub _getter_setter { my ($self, $key, $value) = @_; if (defined $value) { $self->{$key} = $value; } return $self->{$key}; } # The file to parse sub file { return shift->_getter_setter('file', @_); } # Where to find ldraw files sub ldraw_path { return shift->_getter_setter('ldraw_path', @_); } # Scale the model sub scale { return shift->_getter_setter('scale', @_); } # Number of mm per LDU (LDraw Unit) sub mm_per_ldu { return shift->_getter_setter('mm_per_ldu', @_); } # Invert this part sub invert { return shift->_getter_setter('invert', @_); } # Print debugging messages to stderr sub debug { return shift->_getter_setter('debug', @_); } # Indentation for debug messages (for subfiles) sub d_indent { return shift->_getter_setter('d_indent', @_); } use constant X => 0; use constant Y => 1; use constant Z => 2; sub DEBUG { my ( $self, $message, @args) = @_; return if !$self->debug; my $indent = " " x $self->d_indent; if ( @args ) { $message = sprintf($message, @args); } print STDERR sprintf("%s%s\n", $indent, $message); } 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 ); } 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) { $self->DEBUG('META: invalid BFC'); return; } if ($first eq 'INVERTNEXT') { $self->{_invertnext} = 1; $self->DEBUG('META: INVERTNEXT found while invert[%d]', $self->invert); return; } if ($first eq 'CERTIFY') { if (!$items[0]) { $self->DEBUG('META: CERTIFY with no winding - default CCW'); return; } #$self->DEBUG('META: BFC CERTIFY %s', $items[0]); return; } $self->DEBUG('META: Unknown BFC: %s', $items[0]); } 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 $det = mat4determinant($mat); my $invert = $self->invert; $self->DEBUG('FILE: %s BEFORE det[%d], invert[%d] _invertnext[%d]', $subpart_filename, $det, $invert, $self->{_invertnext}); if ($det < 0) { $invert = 1; } if ($self->{_invertnext}) { $invert = $invert ? 0 : 1; } $self->DEBUG('FILE: %s AFTER det[%d], invert[%d] _invertnext[%d]', $subpart_filename, $det, $invert, $self->{_invertnext}); my $subparser = __PACKAGE__->new( { file => $subpart_filename, ldraw_path => $self->ldraw_path, debug => $self->debug, invert => $invert, d_indent => $self->d_indent + 2, } ); $subparser->parse; $self->{_invertnext} = 0; for my $triangle ( @{ $subparser->{triangles} } ) { for my $vec ( @{ $triangle } ) { my @new_vec = mat4xv3( $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; if ($self->invert) { $self->_add_triangle([ [$items[0], $items[1], $items[2]], [$items[6], $items[7], $items[8]], [$items[3], $items[4], $items[5]], ]); } else { $self->_add_triangle([ [$items[0], $items[1], $items[2]], [$items[3], $items[4], $items[5]], [$items[6], $items[7], $items[8]], ]); } } 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; if ($self->invert) { $self->_add_triangle([ [$x1, $y1, $z1], [$x3, $y3, $z3], [$x2, $y2, $z2], ]); $self->_add_triangle([ [$x3, $y3, $z3], [$x1, $y1, $z1], [$x4, $y4, $z4], ]); } else { $self->_add_triangle([ [$x1, $y1, $z1], [$x2, $y2, $z2], [$x3, $y3, $z3], ]); $self->_add_triangle([ [$x3, $y3, $z3], [$x4, $y4, $z4], [$x1, $y1, $z1], ]); } } sub _add_triangle { my ($self, $points) = @_; $points->[3] = $self->calc_surface_normal($points); push @{$self->{triangles}}, $points; } sub parse_optional { my ( $self, $rest ) = @_; } sub calc_surface_normal { my ($self, $points) = @_; my ($p1, $p2, $p3) = ($points->[0], $points->[1], $points->[2]); 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 mat4xv3 { 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 mat4determinant { my ($mat) = @_; my $a00 = $mat->[0]; my $a01 = $mat->[1]; my $a02 = $mat->[2]; my $a03 = $mat->[3]; my $a10 = $mat->[4]; my $a11 = $mat->[5]; my $a12 = $mat->[6]; my $a13 = $mat->[7]; my $a20 = $mat->[8]; my $a21 = $mat->[9]; my $a22 = $mat->[10]; my $a23 = $mat->[11]; my $a30 = $mat->[12]; my $a31 = $mat->[13]; my $a32 = $mat->[14]; my $a33 = $mat->[15]; my $b00 = $a00 * $a11 - $a01 * $a10; my $b01 = $a00 * $a12 - $a02 * $a10; my $b02 = $a00 * $a13 - $a03 * $a10; my $b03 = $a01 * $a12 - $a02 * $a11; my $b04 = $a01 * $a13 - $a03 * $a11; my $b05 = $a02 * $a13 - $a03 * $a12; my $b06 = $a20 * $a31 - $a21 * $a30; my $b07 = $a20 * $a32 - $a22 * $a30; my $b08 = $a20 * $a33 - $a23 * $a30; my $b09 = $a21 * $a32 - $a22 * $a31; my $b10 = $a21 * $a33 - $a23 * $a31; my $b11 = $a22 * $a33 - $a23 * $a32; return $b00 * $b11 - $b01 * $b10 + $b02 * $b09 + $b03 * $b08 - $b04 * $b07 + $b05 * $b06; } 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; __DATA__ ## In handler for "!LDRAW": // If the scale of the object is negated then the triangle winding order // needs to be flipped. var matrix = currentParseScope.matrix; if ( matrix.determinant() < 0 && ( scope.separateObjects && isPrimitiveType( type ) || ! scope.separateObjects ) ) { currentParseScope.inverted = ! currentParseScope.inverted; } triangles = currentParseScope.triangles; lineSegments = currentParseScope.lineSegments; conditionalSegments = currentParseScope.conditionalSegments; break; ## Handling sub-file: // Line type 1: Sub-object file case '1': var material = parseColourCode( lp ); var posX = parseFloat( lp.getToken() ); var posY = parseFloat( lp.getToken() ); var posZ = parseFloat( lp.getToken() ); var m0 = parseFloat( lp.getToken() ); var m1 = parseFloat( lp.getToken() ); var m2 = parseFloat( lp.getToken() ); var m3 = parseFloat( lp.getToken() ); var m4 = parseFloat( lp.getToken() ); var m5 = parseFloat( lp.getToken() ); var m6 = parseFloat( lp.getToken() ); var m7 = parseFloat( lp.getToken() ); var m8 = parseFloat( lp.getToken() ); var matrix = new Matrix4().set( m0, m1, m2, posX, m3, m4, m5, posY, m6, m7, m8, posZ, 0, 0, 0, 1 ); var fileName = lp.getRemainingString().trim().replace( /\\/g, "/" ); if ( scope.fileMap[ fileName ] ) { // Found the subobject path in the preloaded file path map fileName = scope.fileMap[ fileName ]; } else { // Standardized subfolders if ( fileName.startsWith( 's/' ) ) { fileName = 'parts/' + fileName; } else if ( fileName.startsWith( '48/' ) ) { fileName = 'p/' + fileName; } } subobjects.push( { material: material, matrix: matrix, fileName: fileName, originalFileName: fileName, locationState: LDrawLoader.FILE_LOCATION_AS_IS, url: null, triedLowerCase: false, inverted: bfcInverted !== currentParseScope.inverted, startingConstructionStep: startingConstructionStep } ); bfcInverted = false;