From 9a0073a0c5f442a6962d8c4134004d3ac24cf677 Mon Sep 17 00:00:00 2001 From: ceade Date: Wed, 19 Mar 2025 23:41:12 +0100 Subject: [PATCH] Hopefully better support for Windows users I was concatenating paths together using the unix slash `/`. This change used `File::Spec::catpath` instead, however I haven't been able to test it on Windows, and there may be other path issues. Also note: without debugging on the user doesn't see when files are not found (eg: due to path issues), so it silently creates an empty STL file. --- bin/dat2stl | 17 +++++- lib/LDraw/Parser.pm | 138 ++++++++++++++++++++++++++++++++++---------- 2 files changed, 124 insertions(+), 31 deletions(-) diff --git a/bin/dat2stl b/bin/dat2stl index e277f6a..0b747e9 100755 --- a/bin/dat2stl +++ b/bin/dat2stl @@ -15,14 +15,20 @@ GetOptions( 'ldrawdir=s', 'file=s', 'debug', + 'nomodel', ); -if ( $opts->{help} ) { +if (!keys %{$opts}) { print_usage(); exit 0; } -if ( !$opts->{file} ) { +if ($opts->{help}) { + print_usage(); + exit 0; +} + +if (!$opts->{file}) { print "ERROR: --file is required! (try --help)\n"; exit 1; } @@ -50,6 +56,10 @@ Takes an ldraw part .dat file as input and converts it into an STL file. --debug Print debugging messages to STDERR + --nomodel + Do not print the stl output. I am using this to run the script over all + parts to try to detect issues. + END } @@ -61,4 +71,7 @@ my $parser = LDraw::Parser->new( { } ); $parser->parse; +if ($opts->{nomodel}) { + exit 0; +} print $parser->to_stl; diff --git a/lib/LDraw/Parser.pm b/lib/LDraw/Parser.pm index 4a3bd40..93f8c58 100644 --- a/lib/LDraw/Parser.pm +++ b/lib/LDraw/Parser.pm @@ -2,6 +2,28 @@ package LDraw::Parser; use strict; use warnings; +use File::Spec; + +# A meta command is a comment line (type 0) followed by some magic. Unfortunately, being a +# comment line it can also be followed by regular old comments. Here are some common words +# that are the first word in a comment line, which are not meta commands we need to +# consider. +# +my @META_IGNORE = ( + "Hi-Res", + "Name:", + "Author:", + "!LDRAW_ORG", + "!LICENSE", + "!HISTORY", + "Technic", + "Box", + "Cylinder", + "Peg", + "Rectangle", + "Stud", +); +my %MI = map {lc($_) => 1} @META_IGNORE; sub new { my ($class, $args) = @_; @@ -52,13 +74,23 @@ use constant Y => 1; use constant Z => 2; sub DEBUG { - my ( $self, $message, @args) = @_; + my ($self, $message, @args) = @_; return if !$self->debug; my $indent = " " x $self->d_indent; - if ( @args ) { + if (@args) { $message = sprintf($message, @args); } - print STDERR sprintf("%s%s\n", $indent, $message); + print STDERR sprintf("%sDEBUG: %s\n", $indent, $message); +} + +sub WARN { + my ($self, $class, $message, @args) = @_; + my $indent = " " x $self->d_indent; + if (@args) { + $message = sprintf($message, @args); + } + $self->{_warn_classes}->{$class}++; + print STDERR sprintf("%sWARN: [%s] %s\n", $indent, $class, $message); } sub parse { @@ -81,8 +113,11 @@ sub parse_handle { } } +# Lines start with a line type, which is an integer. The type defines the format of the +# rest of the line. +# sub parse_line { - my ( $self, $line ) = @_; + my ($self, $line) = @_; $line =~ s/^\s+//; @@ -107,23 +142,45 @@ sub parse_line { $self->parse_optional( $rest ); } else { - warn "unhandled line type: $line_type"; + $self->WARN("UNKNOWN_LINE_TYPE", "unhandled line type: %s", $line_type); } } } +# Comments can usually be ignored, except for the "BFC" meta command. This is used to +# define the winding order of triangles in the file (Back Face Culling). +# +# "Changing the winding setting will only affect the current file. It will not modify the +# winding of subfiles." +# +# I need to check this, because I think my logic might be flawed here. +# sub parse_comment_or_meta { - my ( $self, $rest ) = @_; - my @items = split( /\s+/, $rest ); + my ($self, $rest) = @_; + my @items = split(/\s+/, $rest); my $first = shift @items; - if ( $first && $first eq 'BFC' ) { - $self->handle_bfc_command( @items ); + if (!$first) { + return; } + if ($first eq '//') { + # The form 0 // is preferred as the // marker clearly indicates that the + # line is a comment, thereby permitting parsers to stop processing the line. The + # form 0 is deprecated. + return; + } + if ($MI{lc($first)}) { + return; + } + if ($first eq 'BFC') { + $self->handle_bfc_command(@items); + return; + } + #$self->WARN("UNKNOWN_META", "unknown meta command: %s", $first); } sub handle_bfc_command { - my ( $self, @items ) = @_; + my ($self, @items) = @_; my $first = shift @items; @@ -133,7 +190,7 @@ sub handle_bfc_command { } if ($first eq 'INVERTNEXT') { $self->{_invertnext} = 1; - $self->DEBUG('META: INVERTNEXT found while invert[%d]', $self->invert); + #$self->DEBUG('META: INVERTNEXT found while invert[%d]', $self->invert); return; } if ($first eq 'CERTIFY') { @@ -147,10 +204,17 @@ sub handle_bfc_command { $self->DEBUG('META: Unknown BFC: %s', $items[0]); } +# A sub-file reference is a shape described in another file, placed in a certain location +# in the model. Note: this is recursive, so sub-files can contain references to other +# sub-files. The first number is a color (ignored) followed by a 3x3 translation matrix +# for how to position the sub-file shape within the model. This matrix encodes rotation +# and translation, and is converted here into a 4x4 matrix with "identity" set for the +# skew part of the matrix. +# sub parse_sub_file_reference { - my ( $self, $rest ) = @_; + 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 @items = split(/\s+/, $rest); my $color = shift @items; my $x = shift @items; my $y = shift @items; @@ -165,10 +229,11 @@ sub parse_sub_file_reference { 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 / + # Possible "shapes" of the matrix. The correct is the one on the right ( + # / 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, @@ -177,29 +242,40 @@ sub parse_sub_file_reference { 0, 0, 0, 1, ]; - if ( scalar( @items ) != 1 ) { + if (scalar(@items) != 1) { warn "um, filename is made up of multiple parts (or none)"; } - my $filename = lc( $items[0] ); + 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 ); + # This is the layout of the ldraw library: + # + # ldraw + # ├── models + # ├── p + # │   ├── 48 + # │   └── 8 + # └── parts + # ├── s + # └── textures + # + my $p_filename = File::Spec->catfile($self->ldraw_path, 'p', $filename); + my $hires_filename = File::Spec->catfile($self->ldraw_path, 'p', '48', $filename); + my $parts_filename = File::Spec->catfile($self->ldraw_path, 'parts', $filename); + my $models_filename = File::Spec->catfile($self->ldraw_path, 'models', $filename); my $subpart_filename; - if ( -e $hires_filename ) { + if (-e $hires_filename) { $subpart_filename = $hires_filename; } - elsif ( -e $p_filename ) { + elsif (-e $p_filename) { $subpart_filename = $p_filename; } - elsif (-e $parts_filename ) { + elsif (-e $parts_filename) { $subpart_filename = $parts_filename; } - elsif ( -e $models_filename ) { + elsif (-e $models_filename) { $subpart_filename = $models_filename; } else { @@ -209,14 +285,18 @@ sub parse_sub_file_reference { 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}); + #$self->DEBUG('FILE: %s BEFORE det[%d], invert[%d] _invertnext[%d]', $subpart_filename, $det, $invert, $self->{_invertnext}); + + # This logic around the `invert`, `_invertnext` and matrix determinant needs to be + # figured out properly. + # 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}); + #$self->DEBUG('FILE: %s AFTER det[%d], invert[%d] _invertnext[%d]', $subpart_filename, $det, $invert, $self->{_invertnext}); my $subparser = __PACKAGE__->new( { file => $subpart_filename,