File Coverage

blib/lib/PDL/IO/STL.pm
Criterion Covered Total %
statement 124 152 81.5
branch 42 72 58.3
condition 8 16 50.0
subroutine 17 19 89.4
pod 2 4 50.0
total 193 263 73.3


line stmt bran cond sub pod time code
1             package PDL::IO::STL;
2              
3 1     1   425 use strict;
  1         2  
  1         26  
4 1     1   3 use warnings;
  1         1  
  1         70  
5              
6             our $VERSION = '0.001';
7             our @EXPORT_OK = qw( rstl wstl );
8             our %EXPORT_TAGS = (Func=>[@EXPORT_OK]);
9             our @ISA = ('PDL::Exporter');
10              
11 1     1   4 use PDL::LiteF;
  1         1  
  1         3  
12 1     1   4 use PDL::Options;
  1         1  
  1         39  
13 1     1   3 use PDL::Exporter;
  1         2  
  1         3  
14 1     1   547 use PDL::IO::Misc; # for little/big-endian
  1         3  
  1         8  
15              
16             =head1 NAME
17              
18             PDL::IO::STL - read/write 3D stereolithography files
19              
20             =head1 SYNOPSIS
21              
22             use PDL;
23             use PDL::IO::STL;
24              
25             ($vertices, $faceidx, $colours) = rstl('owl.stl'); # read an STL file
26             wstl('file.stl', $vertices, $faceidx, $colours); # write an STL file
27              
28             =head1 DESCRIPTION
29              
30             Normal-vector information is currently ignored.
31             The "attribute byte count", used sometimes to store colour information,
32             is currently ignored.
33              
34             This module is based on L, but with C on
35             opened filehandles and little-endian (i.e. network) order forced on the
36             binary format.
37              
38             =head1 FUNCTIONS
39              
40             =head2 rstl
41              
42             =for ref
43              
44             Read an STL file (ASCII or binary), returning vertices and face-indices.
45              
46             =for example
47              
48             ($vertices, $faceidx, $colours) = rstl('owl.stl'); # read an STL file
49              
50             =cut
51              
52 4     4 1 63 sub rstl { PDL->rstl(@_); }
53             sub PDL::rstl {
54 4     4 0 11 my $class = shift;
55 4 50 33     28 barf 'Usage: $x = rstl($file) -or- $x = PDL->rstl($file)' if @_ < 1 || @_ > 2;
56 4         37 my $file = shift;
57             # allow filehandle
58 4 100 100     22 unless((ref($file) || '') eq 'GLOB') {
59 3 50       275 open(my $fh, '<', $file) or
60             barf "cannot open '$file' for reading $!";
61 3         16 binmode $fh;
62 3         10 $file = $fh;
63             }
64 4 50       90 barf('must have seekable filehandle') if !seek($file, 0,0);
65 4         20 my $mode = _detect($file);
66 4 50       32 seek($file, 0, 0) or barf "cannot reset filehandle";
67 4 100       16 my $func = $mode eq 'ascii' ? \&_read_ascii : \&_read_binary;
68 4         16 $func->($file);
69             }
70              
71             sub _detect {
72 4     4   9 my $fh = shift;
73 4         10 my $location = tell $fh;
74 4 50       7 my $buf; read($fh, $buf, 5) or barf $@;
  4         215  
75 4 100       63 seek($fh, $location, 0), return 'ascii' if $buf eq 'solid';
76 1         11 seek($fh, $location + 80, 0);
77 1         4 my $count = eval {
78 1 50       2 my $buf; read($fh, $buf, 4) or barf $@;
  1         12  
79 1         6 unpack('L<', $buf);
80             };
81 1 50       5 $@ and seek($fh, $location, 0), return 'ascii'; # if we hit eof, not binary
82 1 50       4 $count or barf "detection failed - no facets?";
83 1         14 my $size = (stat($fh))[7];
84 1 50       5 barf "failed to stat '$fh'" if !defined $size;
85             # calculate the expected file size
86 1         3 my $expect =
87             + 80 # header
88             + 4 # count
89             + $count * (
90             + 4 # normal, pt,pt,pt (vectors)
91             * 3 # values per vector
92             * 4 # bytes per value
93             + 2 # the trailing 'short'
94             );
95 1 50       7 return ($size - $location >= $expect) ? 'binary' : 'ascii';
96             }
97              
98             my $p_re = qr/([^ ]+)\s+([^ ]+)\s+([^ ]+)$/;
99             sub _read_ascii {
100 3     3   7 my ($fh) = @_;
101             my $getline = sub {
102 132     132   414 while(my $line = <$fh>) {
103 132         865 $line =~ s/\s*$//; # allow any eol
104 132 50       255 length($line) or next;
105 132         373 return($line);
106             }
107 0         0 return;
108 3         21 };
109 3         8 my (@tri, $part);
110 3         6 while(my $line = $getline->()) {
111 24 100       108 if($line =~ m/^\s*solid\s*(.*)/) {
    100          
112 3         11 $part = $1;
113 3         10 next;
114             }
115             elsif($line =~ m/^\s*endsolid\s*(.*)/) {
116 3         10 my $name = $1;
117 3 100       12 if (length $name) { # only catch if wrong; if absent, fine
118 1 50       6 barf "invalid 'endsolid' entry with no current part" if !defined $part;
119 1 50       22 barf "end of part '$name' should have been '$part'" if $name ne $part;
120             }
121 3         7 $part = undef;
122 3         8 last;
123             }
124 18 50       37 barf "what? ($line)" if !defined $part;
125 18 50       353 my @n = ($line =~ m/^\s*facet\s+normal\s+$p_re/) or
126             barf "how did that happen? ($line)";
127 18         45 my $next = $getline->();
128 18 50 33     101 unless($next and ($next =~ m/^\s*outer\s+loop$/)) {
129 0         0 barf "facet doesn't start with 'outer loop' ($next)";
130             }
131 18         28 my @this_tri;
132 18         37 while(my $line = $getline->()) {
133 72 100       181 ($line =~ m/^\s*endloop$/) and last;
134 54 50       428 if($line =~ m/^\s*vertex\s+$p_re/) {
135 54         277 push(@this_tri, [$1, $2, $3]);
136             }
137             }
138 18 50       41 barf "need three vertices per facet (not @{[ 0+@this_tri ]})" if @this_tri != 3;
  0         0  
139 18         39 my $end = $getline->();
140 18 50 33     95 ($end and ($end =~ m/^\s*endfacet/)) or
141             barf "bad endfacet $line";
142 18         66 push @tri, \@this_tri;
143             }
144 3 50       8 barf "part '$part' was left open" if defined $part;
145 3         15 _as_ndarray(pdl PDL::float(), \@tri);
146             }
147              
148             sub _as_ndarray {
149 4     4   13 my ($pdl) = @_;
150 4         25 my $uniqv = $pdl->uniqvec;
151 4         403 ($uniqv, $pdl->vsearchvec($uniqv), undef);
152             }
153              
154             sub _read_binary {
155 1     1   23 my ($fh) = @_;
156 1         3 barf "bigfloat" unless(length(pack("f", 1)) == 4);
157             # TODO try to read part name from header (up to \0)
158 1         6 seek($fh, 80, 0);
159 1 50       2 my $buf; read($fh, $buf, 4) or warn "EOF?"; my $triangles = unpack('L<', $buf);
  1         11  
  1         4  
160 1         3 my $bytes = 50 * $triangles; # norm+3vertices * 3float + short with length of extra
161 1         5 my $bytespdl = zeroes PDL::byte(), 50, $triangles;
162 1         5 my $bytesread = read($fh, ${$bytespdl->get_dataref}, $bytes);
  1         8  
163 1 50       4 barf "Tried to read $bytes but only got $bytesread" if $bytesread != $bytes;
164 1         5 $bytespdl->upd_data;
165 1         6 my $floatpdl = zeroes PDL::float(), 3, 4, $triangles;
166 1         4 ${$floatpdl->get_dataref} = ${$bytespdl->slice('0:47')->get_dataref};
  1         5  
  1         8  
167 1         6 $floatpdl->upd_data;
168 1 50       6 $floatpdl->type->bswap->($floatpdl) if isbigendian();
169             # TODO check that the unit normal is within a thousandth of a radian
170             # (0.001 rad is ~0.06deg)
171 1         5 _as_ndarray($floatpdl->slice(':,1:3'));
172             }
173              
174             =head2 wstl
175              
176             =for ref
177              
178             Simple PDL STL writer
179              
180             =for example
181              
182             wstl 'file.stl', $vertices, $faceidx;
183             wstl 'file.stl', $vertices, $faceidx, \%OPTIONS;
184             wstl $fh, $vertices, $faceidx, \%OPTIONS;
185              
186             Passing a file-handle is supported, so multiple parts can be written to
187             an ASCII file with several calls.
188              
189             C accepts several options that may be passed in as a hash ref
190             if desired:
191              
192             =over 3
193              
194             =item mode (default='binary')
195              
196             Whether to write out the file as ASCII or binary.
197              
198             =item name (default='part')
199              
200             The part name to use.
201              
202             =back
203              
204             =cut
205              
206             our $wstl_options = PDL::Options->new( { mode=>'binary', name=>'part' } );
207             my %valid_mode = map +($_=>1), qw(ascii binary);
208 2     2 1 2399 sub wstl { PDL->wstl(@_); }
209             sub PDL::wstl {
210 2 100 66 2 0 18 barf 'Usage: wstl($file,$vertices,$faceidx,[$colours],[{options}])' if @_<3 || @_>5;
211 1         4 my (undef, $file, $v, $f, $c) = @_;
212 1         9 my $u_opt = ifhref($_[-1]);
213 1         10 my $opt = $wstl_options->options($u_opt);
214 1         3 my $mode = $opt->{mode};
215 1 50       7 barf "invalid write mode '$mode'" if !$valid_mode{$mode};
216             # allow filehandle
217 1 50 50     8 unless((ref($file) || '') eq 'GLOB') {
218 0 0       0 open(my $fh, '>', $file) or
219             barf "cannot open '$file' for writing $!";
220 0         0 binmode $fh;
221 0         0 $file = $fh;
222             }
223 1 50       4 my $func = $mode eq 'ascii' ? \&_write_ascii : \&_write_binary;
224 1         7 $func->($file, $v, $f, $c, $opt->{name});
225 1         21 1;
226             }
227              
228             sub _write_binary {
229 1     1   4 my ($fh, $v, $f, $c, $name) = @_;
230 1     1   374 print $fh $name, "\0" x (80 - do {use bytes; length($name)});
  1         356  
  1         5  
  1         2  
  1         25  
231 1         13 print $fh pack 'L<', $f->dim(1);
232 1         3 foreach my $facet (@{ $v->dice_axis(1, $f->flat)->splitdim(1,3)->unpdl }) {
  1         6  
233 12         28 print $fh map {map pack('f<', $_), @$_} [0,0,0], @$facet;
  48         229  
234 12         40 print $fh "\0" x 2;
235             }
236             }
237              
238             sub _write_ascii {
239 0     0     my ($fh, $v, $f, $c, $name) = @_;
240 0           my $spaces = '';
241 0     0     my $print = sub {print $fh $spaces . join(' ', @_) . "\n"};
  0            
242 0           $print->('solid', $name);
243 0           $spaces = ' 'x2;
244 0           foreach my $facet (@{ $v->dice_axis(1, $f->flat)->splitdim(1,3)->unpdl }) {
  0            
245 0           my ($n, @pts) = ([0,0,0], @$facet);
246 0           $print->('facet normal', @$n);
247 0           $spaces = ' 'x4;
248 0           $print->('outer loop');
249 0           $spaces = ' 'x6;
250 0 0         (@pts == 3) or barf "invalid facet";
251 0           foreach my $pt (@pts) {
252 0           $print->('vertex', @$pt);
253             }
254 0           $spaces = ' 'x4;
255 0           $print->('endloop');
256 0           $spaces = ' 'x2;
257 0           $print->('endfacet');
258             }
259 0           $spaces = '';
260 0           $print->('endsolid', $name);
261             }
262              
263             =head1 AUTHOR
264              
265             Ed J, based on Eric Wilhelm's code in L.
266              
267             =cut
268              
269             1;