mirror of
https://github.com/kristov/ldraw2stl.git
synced 2025-05-15 06:10:11 -07:00
With no cache: $ time bin/dat2stl --ldrawdir=ldraw --file ldraw/parts/11295.dat > 11295.stl real 0m1.857s user 0m1.764s sys 0m0.092s $ time bin/dat2stl --ldrawdir=ldraw --file ldraw/parts/11295.dat > 11295.stl real 0m1.834s user 0m1.786s sys 0m0.048s With cache: $ time bin/dat2stl --cache --ldrawdir=ldraw --file ldraw/parts/11295.dat > 11295.stl real 0m1.084s user 0m1.044s sys 0m0.040s $ time bin/dat2stl --cache --ldrawdir=ldraw --file ldraw/parts/11295.dat > 11295.stl real 0m1.076s user 0m1.028s sys 0m0.048s
649 lines
19 KiB
Perl
649 lines
19 KiB
Perl
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) = @_;
|
|
die "file required" unless $args->{file};
|
|
return bless({
|
|
file => $args->{file},
|
|
cache => $args->{cache},
|
|
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,
|
|
ccw_winding => 1,
|
|
_invertnext => 0,
|
|
triangles => [],
|
|
}, $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', @_); }
|
|
|
|
# Specify the winding order of triangles in this part
|
|
sub ccw_winding { return shift->_getter_setter('ccw_winding', @_); }
|
|
|
|
# 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("%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 {
|
|
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 );
|
|
}
|
|
}
|
|
|
|
# The BFC CERTIFY [CCW|CW] meta command determines the winding. But if we are inverting
|
|
# the part, we have to invert this winding.
|
|
#
|
|
sub use_ccw_winding {
|
|
my ($self) = @_;
|
|
return ($self->invert) ? !$self->ccw_winding : $self->ccw_winding;
|
|
}
|
|
|
|
# The default winding of triangles is CCW (Counter ClockWise). A triangle wound CCW on the
|
|
# X,Y plane will have it's normal vector pointing in the positive Z direction. This is
|
|
# usually towards the screen, so likely towards a light source. By default, all ldraw
|
|
# geometry is wound CCW, and this is made explicit by the meta command:
|
|
#
|
|
# 0 BFC CERTIFIED CCW
|
|
#
|
|
# However, a part file may change this winding order by using "BFC CERTIFIED CW". This
|
|
# will flip the normal vector to be pointing in the other direction. For example, if we
|
|
# are generating the inside surface of a tube rather than the outside.
|
|
#
|
|
# The "invert" parameter changes this winding for a part. So if the part is CCW, the
|
|
# invert param flips this to CW. This inversion is "sticky", meaning it will be applied to
|
|
# a part and all it's sub-parts, until the inversion rule is flipped.
|
|
#
|
|
# The inversion rule is flipped under two circumstances: 1. An "INVERTNEXT" BFC meta
|
|
# command is seen, and 2. If this code detects there is a reflection transformation.
|
|
#
|
|
# The "INVERTNEXT" meta command in theory applies to the next line in a file, however in
|
|
# this code it only affects the next sub-part (parse_sub_file_reference). I have yet to
|
|
# see it be applied to a triangle or quad line.
|
|
#
|
|
# A reflection transformation will flip the winding order of the triangles. Therefore, the
|
|
# invert param is set so that the sub-part is generated with the inverted winding. When
|
|
# the reflection transformation is applied, the winding is set back to the expected
|
|
# winding for the sub-part.
|
|
#
|
|
sub compute_inversion {
|
|
my ($self, $mat) = @_;
|
|
|
|
# Use the passed invert state, unless we are doing an INVERTNEXT
|
|
my $invert = ($self->{_invertnext}) ? !$self->invert : $self->invert;
|
|
|
|
# A negative determinant means there is some form or reflection happening. When this
|
|
# matrix is applied to the vertexes of the sub-part, the winding order of the vertexes
|
|
# is reversed. So if we detect a negative determinant, we have to flip the winding
|
|
# order of the sub-part so that when this matrix is applied the original intended
|
|
# winding is preserved.
|
|
my $det = mat4determinant($mat);
|
|
$invert = ($det < 0) ? !$invert : $invert;
|
|
|
|
return $invert;
|
|
}
|
|
|
|
# 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) = @_;
|
|
|
|
$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 {
|
|
$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 $first = shift @items;
|
|
|
|
if (!$first) {
|
|
return;
|
|
}
|
|
if ($first eq '//') {
|
|
# The form 0 // <comment> is preferred as the // marker clearly indicates that the
|
|
# line is a comment, thereby permitting parsers to stop processing the line. The
|
|
# form 0 <comment> 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 $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') {
|
|
my $winding = $items[0];
|
|
if (!$winding) {
|
|
$self->DEBUG('META: CERTIFY with no winding - default CCW');
|
|
return;
|
|
}
|
|
if ($winding eq 'CW') {
|
|
$self->ccw_winding(0);
|
|
}
|
|
return;
|
|
}
|
|
$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) = @_;
|
|
# 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;
|
|
|
|
# The form of this matrix is:
|
|
#
|
|
# / a b c x \
|
|
# | d e f y |
|
|
# | g h i z |
|
|
# \ 0 0 0 1 /
|
|
#
|
|
# Note: The x,y,z translation part are the first 3 arguments.
|
|
|
|
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;
|
|
|
|
# This is the layout of the ldraw library:
|
|
#
|
|
# ldraw
|
|
# ├── models
|
|
# ├── p
|
|
# │ ├── 48
|
|
# │ └── 8
|
|
# └── parts
|
|
# ├── s
|
|
# └── textures
|
|
#
|
|
# From the Readme.txt file:
|
|
#
|
|
# \MODELS\ - This directory is where your model .dat files are stored.
|
|
# There are two sample model .dat files installed for you
|
|
# to look at - Car.dat and Pyramid.dat.
|
|
# \P\ - This directory is where parts primitives are located.
|
|
# Parts primitives are tyically highly reusable components
|
|
# used by the part files in the LDraw library.
|
|
# \P\48\ - This directory is where high resolution parts primitives
|
|
# are located. These are typically used for large curved
|
|
# parts where excessive scaling of the regular curved
|
|
# primitives would produce an undesriable result.
|
|
# \PARTS\ - This directory holds all the actual parts that can be used
|
|
# in creating or rendering your models. A list of these
|
|
# parts can be seen by viewing the parts.lst file.
|
|
# \PARTS\S\ - This directory holds sub-parts that are used by the LDraw
|
|
# parts to optimise file size and improve parts development
|
|
# efficiancy.
|
|
#
|
|
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 $parts_s_filename = File::Spec->catfile($self->ldraw_path, 'parts', 's', $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 $parts_s_filename) {
|
|
$subpart_filename = $parts_s_filename;
|
|
}
|
|
else {
|
|
warn "unable to find file: $filename in normal paths";
|
|
return;
|
|
}
|
|
|
|
my $triangles;
|
|
my $invert = $self->compute_inversion($mat);
|
|
if (defined $self->{cache}) {
|
|
$triangles = $self->{cache}->get($subpart_filename, $invert);
|
|
}
|
|
if (!$triangles) {
|
|
# Not using a cache, or cache miss
|
|
my $subparser = LDraw::Parser->new({
|
|
file => $subpart_filename,
|
|
ldraw_path => $self->ldraw_path,
|
|
debug => $self->debug,
|
|
invert => $invert,
|
|
d_indent => $self->d_indent + 2,
|
|
(defined $self->{cache}) ? (cache => $self->{cache}) : (),
|
|
});
|
|
$subparser->parse;
|
|
$triangles = $subparser->{triangles};
|
|
if (defined $self->{cache}) {
|
|
$self->{cache}->put($subpart_filename, $invert, $triangles);
|
|
}
|
|
}
|
|
$self->{_invertnext} = 0;
|
|
|
|
for my $triangle (@{$triangles}) {
|
|
my $n_triangle = [];
|
|
for my $vec (@{$triangle}) {
|
|
push @{$n_triangle}, mat4xv3($mat, $vec);
|
|
}
|
|
push @{$self->{triangles}}, $n_triangle;
|
|
#use Data::Dumper;
|
|
#warn Dumper($self->{triangles});
|
|
}
|
|
}
|
|
|
|
# Lines are used for outlining the model so it is easier to see edges. Because we are
|
|
# generating data for an STL we don't need lines.
|
|
sub parse_line_command {
|
|
my ( $self, $rest ) = @_;
|
|
}
|
|
|
|
# Optional lines are strange things, but they appear to be about not drawing lines that
|
|
# are occluded by the model. I think. Regardless, we don't need lines or optional lines
|
|
# for STL generation.
|
|
sub parse_optional {
|
|
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->use_ccw_winding) {
|
|
$self->_add_triangle([
|
|
[$items[0], $items[1], $items[2]],
|
|
[$items[3], $items[4], $items[5]],
|
|
[$items[6], $items[7], $items[8]],
|
|
]);
|
|
}
|
|
else {
|
|
$self->_add_triangle([
|
|
[$items[0], $items[1], $items[2]],
|
|
[$items[6], $items[7], $items[8]],
|
|
[$items[3], $items[4], $items[5]],
|
|
]);
|
|
}
|
|
}
|
|
|
|
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->use_ccw_winding) {
|
|
$self->_add_triangle([
|
|
[$x1, $y1, $z1],
|
|
[$x2, $y2, $z2],
|
|
[$x3, $y3, $z3],
|
|
]);
|
|
$self->_add_triangle([
|
|
[$x3, $y3, $z3],
|
|
[$x4, $y4, $z4],
|
|
[$x1, $y1, $z1],
|
|
]);
|
|
}
|
|
else {
|
|
$self->_add_triangle([
|
|
[$x1, $y1, $z1],
|
|
[$x3, $y3, $z3],
|
|
[$x2, $y2, $z2],
|
|
]);
|
|
$self->_add_triangle([
|
|
[$x3, $y3, $z3],
|
|
[$x1, $y1, $z1],
|
|
[$x4, $y4, $z4],
|
|
]);
|
|
}
|
|
}
|
|
|
|
sub _add_triangle {
|
|
my ($self, $points) = @_;
|
|
push @{$self->{triangles}}, $points;
|
|
}
|
|
|
|
sub calc_surface_normal {
|
|
my ($self, $points) = @_;
|
|
my ($p1, $p2, $p3) = ($points->[0], $points->[1], $points->[2]);
|
|
|
|
my $U = [
|
|
$p2->[0] - $p1->[0],
|
|
$p2->[1] - $p1->[1],
|
|
$p2->[2] - $p1->[2],
|
|
];
|
|
my $V = [
|
|
$p3->[0] - $p1->[0],
|
|
$p3->[1] - $p1->[1],
|
|
$p3->[2] - $p1->[2],
|
|
];
|
|
|
|
my $N = [
|
|
$U->[1] * $V->[2] - $U->[2] * $V->[1],
|
|
$U->[2] * $V->[0] - $U->[0] * $V->[2],
|
|
$U->[0] * $V->[1] - $U->[1] * $V->[0],
|
|
];
|
|
|
|
my $len = sqrt($N->[0] ** 2 + $N->[1] ** 2 + $N->[2] ** 2);
|
|
if ($len == 0) {
|
|
return [0, 0, 0];
|
|
}
|
|
|
|
return [
|
|
$N->[0] / $len,
|
|
$N->[1] / $len,
|
|
$N->[2] / $len,
|
|
];
|
|
}
|
|
|
|
sub mat4xv3 {
|
|
my ($mat, $vec) = @_;
|
|
|
|
my ($a1, $a2, $a3, $a4,
|
|
$b1, $b2, $b3, $b4,
|
|
$c1, $c2, $c3, $c4) = @{$mat};
|
|
|
|
my ($u, $v, $z) = @{$vec};
|
|
|
|
my $x_new = $a1 * $u + $a2 * $v + $a3 * $z + $a4;
|
|
my $y_new = $b1 * $u + $b2 * $v + $b3 * $z + $b4;
|
|
my $z_new = $c1 * $u + $c2 * $v + $c3 * $z + $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 _transvec {
|
|
my ($mm_per_ldu, $scale, $vec) = @_;
|
|
return [map {sprintf('%0.4f', $_ * $mm_per_ldu * $scale)} @{$vec}];
|
|
}
|
|
|
|
sub stl_buffer {
|
|
my ($self) = @_;
|
|
|
|
my $scale = $self->scale || 1;
|
|
my $mm_per_ldu = $self->mm_per_ldu;
|
|
|
|
my @facets;
|
|
for my $triangle (@{$self->{triangles}}) {
|
|
my ($p1, $p2, $p3) = map {_transvec($mm_per_ldu, $scale, $_)} @{$triangle};
|
|
my $n = $self->calc_surface_normal([$p1, $p2, $p3]);
|
|
my $facet = {
|
|
normal => $n,
|
|
vertexes => [],
|
|
};
|
|
for my $vec (($p1, $p2, $p3)) {
|
|
push @{$facet->{vertexes}}, $vec;
|
|
}
|
|
push @facets, $facet;
|
|
}
|
|
return \@facets;
|
|
}
|
|
|
|
sub gl_buffer {
|
|
my ($self) = @_;
|
|
|
|
my $scale = $self->scale || 1;
|
|
my $mm_per_ldu = $self->mm_per_ldu;
|
|
|
|
my @normals;
|
|
my @vertexes;
|
|
for my $triangle (@{$self->{triangles}}) {
|
|
my ($p1, $p2, $p3) = map {_transvec($mm_per_ldu, $scale, $_)} @{$triangle};
|
|
my $n = $self->calc_surface_normal([$p1, $p2, $p3]);
|
|
my @vertnorms = map {sprintf('%0.4f', $_)} @{$n};
|
|
# OpenGL requires an identical normal for each of the 3 vertexes in the triangle
|
|
# (usually anyway). We won't bother generating indexes, because this should be
|
|
# rendered using `glDrawArrays(GL_TRIANGLES, 0, n)`.
|
|
push @normals, @vertnorms;
|
|
push @normals, @vertnorms;
|
|
push @normals, @vertnorms;
|
|
for my $vec (($p1, $p2, $p3)) {
|
|
push @vertexes, map {sprintf('%0.4f', $_)} @{$vec};
|
|
}
|
|
}
|
|
return {
|
|
normals => \@normals,
|
|
vertexes => \@vertexes,
|
|
};
|
|
}
|
|
|
|
package LDraw::Parser::Cache;
|
|
|
|
sub new {
|
|
my ($class) = @_;
|
|
return bless({_cache => {}}, $class);
|
|
}
|
|
|
|
sub get {
|
|
my ($self, $file, $invert) = @_;
|
|
my $key = sprintf("%s__%d", $file, $invert);
|
|
if (defined $self->{_cache}->{$key}) {
|
|
return $self->{_cache}->{$key};
|
|
}
|
|
return;
|
|
}
|
|
|
|
sub put {
|
|
my ($self, $file, $invert, $trianges) = @_;
|
|
my $key = sprintf("%s__%d", $file, $invert);
|
|
if (defined $self->{_cache}->{$key}) {
|
|
die sprintf("repeated put for %s", $key);
|
|
}
|
|
$self->{_cache}->{$key} = $trianges;
|
|
return;
|
|
}
|
|
|
|
1;
|