File Coverage

blib/lib/CAD/Format/STL.pm
Criterion Covered Total %
statement 131 189 69.3
branch 36 84 42.8
condition 7 15 46.6
subroutine 17 23 73.9
pod 9 9 100.0
total 200 320 62.5


line stmt bran cond sub pod time code
1             package CAD::Format::STL;
2             $VERSION = v0.2.1;
3              
4 3     3   111660 use warnings;
  3         7  
  3         109  
5 3     3   17 use strict;
  3         5  
  3         108  
6 3     3   27 use Carp;
  3         5  
  3         269  
7              
8 3     3   1759 use CAD::Format::STL::part;
  3         10  
  3         148  
9              
10             =head1 NAME
11              
12             CAD::Format::STL - read/write 3D stereolithography files
13              
14             =head1 SYNOPSIS
15              
16             Reading:
17              
18             my $stl = CAD::Format::STL->new->load("foo.stl");
19             # what about the part/multipart?
20             my @facets = $stl->part->facets;
21              
22             Writing:
23              
24             my $stl = CAD::Format::STL->new;
25             my $part = $stl->add_part("my part");
26             $part->add_facets(@faces);
27             $stl->save("foo.stl");
28             # or $stl->save(binary => "foo.stl");
29              
30             Streaming read/write:
31              
32             my $reader = CAD::Format::STL->reader("foo.stl");
33             my $writer = CAD::Format::STL->writer(binary => "bar.stl");
34             while(my $part = $reader->next_part) {
35             my $part_name = $part->name;
36             $writer->start_solid($part_name);
37             while(my @data = $part->facet) {
38             my ($normal, @vertices) = @data;
39             my @v1 = @{$vertices[0]};
40             my @v2 = @{$vertices[0]};
41             my @v3 = @{$vertices[0]};
42             # that's just for illustration
43             $writer->facet(\@v1, \@v2, \@v3);
44             # note the omitted normal
45             }
46             $writer->end_solid;
47             }
48              
49             =begin design
50              
51             The reader auto-detects whether it is binary (but assumes ascii when
52             seek can't go backwards.)
53              
54             The reader and writer both take 1, 2, or {1,2}+2n arguments.
55              
56             This package and/or the reader/writer are subclassable (though getting
57             $self->reader to instantiate a subclass implies that you have subclassed
58             $self.)
59              
60             A cached_facet (or raw_facet) method is necessary to ensure uniform
61             tranformation of shared points (and optimize the computation.) This
62             would return the normal and points as a list of scalars rather than
63             arrays, with a later call to unpack_point() or something. The caller
64             needs to be able to handle the caching (or else there is a callback for
65             non-cached (or an override for unpack_point().)
66              
67             Maybe $self->set_writer() and set_reader() immutable object methods?
68              
69             =end design
70              
71             =head1 ABOUT
72              
73             This module provides object-oriented methods to read and write the STL
74             (Stereo Lithography) file format in both binary and ASCII forms. The
75             STL format is a simple set of 3D triangles.
76              
77             =cut
78              
79 3     3   23 use Class::Accessor::Classy;
  3         6  
  3         14  
80             lo 'parts';
81 3     3   549 no Class::Accessor::Classy;
  3         5  
  3         14  
82              
83             =head1 Constructor
84              
85             =head2 new
86              
87             my $stl = CAD::Format::STL->new;
88              
89             =cut
90              
91             sub new {
92 2     2 1 27 my $package = shift;
93 2   33     85 my $class = ref($package) || $package;
94 2         9 my $self = {parts => []};
95 2         5 bless($self, $class);
96 2         8 return($self);
97             } # end subroutine new definition
98             ########################################################################
99              
100             =head2 add_part
101              
102             Create a new part in the stl.
103              
104             my $part = $stl->add_part("name");
105              
106             Optionally, add the faces directly:
107              
108             my $part = $stl->add_part("name", @faces);
109              
110             =cut
111              
112             sub add_part {
113 4     4 1 11741 my $self = shift;
114 4         14 my ($name, @faces) = @_;
115              
116 4         29 my $part = CAD::Format::STL::part->new($name, @faces);
117 4         25 push(@{$self->{parts}}, $part);
  4         14  
118 4         14 return($part);
119             } # end subroutine add_part definition
120             ########################################################################
121              
122             =head2 part
123              
124             Get the part at $index. Negative indices are valid.
125              
126             my $part = $stl->part($index);
127              
128             Throws an error if there is no such part.
129              
130             =cut
131              
132             sub part {
133 4     4 1 3062 my $self = shift;
134 4         8 my ($index) = @_;
135              
136 4 50       5 @{$self->{parts}} or croak("file has no parts");
  4         15  
137              
138 4   100     14 $index ||= 0;
139 4 100       348 exists($self->{parts}[$index]) or croak("no part $index");
140 2         8 return($self->{parts}[$index]);
141             } # end subroutine part definition
142             ########################################################################
143              
144             =head1 I/O Methods
145              
146             =head2 load
147              
148             Load an STL file (auto-detects binary/ascii)
149              
150             $stl = $stl->load("filename.stl");
151              
152             Optionally, explicitly declare binary mode:
153              
154             $stl = $stl->load(binary => "filename.stl");
155              
156             The $self object is returned to allow e.g. chaining to C.
157              
158             The filename may also be a filehandle.
159              
160             =cut
161              
162             sub load {
163 1     1 1 15 my $self = shift;
164 1         4 my ($file, @and) = @_;
165              
166 1         1 my $mode;
167 1 50       4 if(@and) {
168 0 0       0 (@and > 1) and croak('too many arguments to load()');
169 0         0 $mode = $file;
170 0         0 ($file) = @and;
171             }
172              
173             # allow filehandle
174 1 50 50     10 unless((ref($file) || '') eq 'GLOB') {
175 1 50       51 open(my $fh, '<', $file) or
176             die "cannot open '$file' for reading $!";
177 1         3 $file = $fh;
178             }
179              
180             # detection
181 1 50       3 unless($mode) {
182 1 50       8 unless(seek($file, 0,0)) {
183 0         0 croak('must have explicit mode for non-seekable filehandle');
184             }
185             # now, detection...
186             $mode = sub {
187 1     1   17 my $fh = shift;
188 1         5 seek($fh, 80, 0);
189 1         2 my $count = eval {
190 1 50       1 my $buf; read($fh, $buf, 4) or die;
  1         21  
191 1         7 unpack('L', $buf);
192             };
193 1 50       5 $@ and return 'ascii'; # if we hit eof, it can't be binary
194 1 50       4 $count or die "detection failed - no facets?";
195 1         13 my $size = (stat($fh))[7];
196             # calculate the expected file size
197 1         3 my $expect =
198             + 80 # header
199             + 4 # count
200             + $count * (
201             + 4 # normal, pt,pt,pt (vectors)
202             * 4 # bytes per value
203             * 3 # values per vector
204             + 2 # the trailing 'short'
205             );
206 1 50       5 return ($size == $expect) ? 'binary' : 'ascii';
207 1         13 }->($file);
208 1 50       16 seek($file, 0, 0) or die "cannot reset filehandle";
209             }
210              
211 1         4 my $method = '_read_' . lc($mode);
212 1 50       11 $self->can($method) or croak("invalid read mode '$mode'");
213              
214 1         4 $self->$method($file);
215 1         17 return($self);
216             } # end subroutine load definition
217             ########################################################################
218              
219             =head2 _read_ascii
220              
221             $self->_read_ascii($filehandle);
222              
223             =cut
224              
225             sub _read_ascii {
226 1     1   2 my $self = shift;
227 1         2 my ($fh) = @_;
228              
229             my $getline = sub {
230 87     87   442 while(my $line = <$fh>) {
231 86         497 $line =~ s/\s*$//; # allow any eol
232 86 50       186 length($line) or next;
233 86         245 return($line);
234             }
235 1         4 return;
236 1         6 };
237 1         7 my $p_re = qr/([^ ]+)\s+([^ ]+)\s+([^ ]+)$/;
238              
239 1         1 my $part;
240 1         6 while(my $line = $getline->()) {
241              
242 14 100       64 if($line =~ m/^\s*solid (.*)/) {
    100          
    50          
243 1         5 $part = $self->add_part($1);
244             }
245             elsif($line =~ m/^\s*endsolid (.*)/) {
246 1         4 my $name = $1;
247 1 50       4 $part or die "invalid 'endsolid' entry with no current part";
248 1 50       37 ($name eq $part->name) or
249             die "end of part '$name' should have been '",
250             $part->name, "'";
251 1         10 $part = undef;
252             }
253             elsif($part) {
254 12 50       163 my @n = ($line =~ m/^\s*facet\s+normal\s+$p_re/) or
255             die "how did that happen? ($line)";
256             #warn "got ", join('|', @n);
257 12         30 my @facet = (\@n);
258              
259 12         24 my $next = $getline->();
260 12 50 33     84 unless($next and ($next =~ m/^\s*outer\s+loop$/)) {
261 0         0 die "facet doesn't start with 'outer loop' ($next)";
262             }
263 12         15 push(@facet, do {
264 12         14 my @got;
265 12         22 while(my $line = $getline->()) {
266 48 100       125 ($line =~ m/^\s*endloop$/) and last;
267 36 50       329 if($line =~ m/^\s*vertex\s+$p_re/) {
268 36         228 push(@got, [$1, $2, $3]);
269             }
270             }
271 12         31 @got;
272             });
273 12 50       31 (scalar(@facet) == 4) or
274             die "need three vertices per facet (not $#facet)";
275 12         23 my $end = $getline->();
276 12 50 33     70 ($end and ($end =~ m/^\s*endfacet/)) or
277             die "bad endfacet $line";
278 12         52 $part->add_facets([@facet]);
279             }
280             else {
281 0         0 die "what? ($line)";
282             }
283             }
284 1 50       10 $part and die "part '", $part->name, "' was left open";
285             } # end subroutine _read_ascii definition
286             ########################################################################
287              
288             =head2 get_
289              
290             These functions are currently only used internally.
291              
292             =over
293              
294             =item get_triangle
295              
296             =item get_ulong
297              
298             =item get_float32
299              
300             =item get_short
301              
302             =back
303              
304             =cut
305              
306             sub get_triangle {
307 0     0 1 0 my ($fh) = @_;
308              
309 0         0 my ($n, $x, $y, $z) = map({[map({get_float32($fh)} 1..3)]} 1..4);
  0         0  
  0         0  
310 0         0 my $scrap = get_short($fh);
311 0         0 return($n, $x, $y, $z);
312             }
313              
314             sub get_ulong {
315 0     0 1 0 my ($fh) = @_;
316              
317 0         0 my $buf;
318 0 0       0 read($fh, $buf, 4) or warn "EOF?";
319 0         0 return(unpack('L', $buf));
320             }
321              
322             sub get_float32 {
323 0     0 1 0 my ($fh) = @_;
324              
325 0         0 my $buf;
326 0 0       0 read($fh, $buf, 4) or warn "EOF?";
327 0         0 return(unpack('f', $buf));
328             }
329              
330             sub get_short {
331 0     0 1 0 my ($fh) = @_;
332              
333 0         0 my $buf;
334 0 0       0 read($fh, $buf, 2) or warn "EOF?";
335 0         0 return(unpack('S', $buf));
336             }
337              
338             =head2 _read_binary
339              
340             $self->_read_binary($filehandle);
341              
342             =cut
343              
344             sub _read_binary {
345 0     0   0 my $self = shift;
346 0         0 my ($fh) = @_;
347              
348 0 0       0 $self->parts and die "binary STL files must have only one part";
349              
350 0         0 die "bigfloat" unless(length(pack("f", 1)) == 4);
351             # TODO try to read part name from header (up to \0)
352 0         0 my $name = 'a part';
353 0         0 seek($fh, 80, 0);
354              
355 0         0 my $triangles = get_ulong($fh);
356 0         0 my $part = $self->add_part($name);
357              
358 0         0 my $count = 0;
359 0         0 while(1) {
360 0         0 my @tr = get_triangle($fh);
361             # TODO check that the unit normal is within a thousandth of a radian
362             # (0.001 rad is ~0.06deg)
363 0         0 $part->add_facets([@tr]);
364 0         0 $count++;
365 0 0       0 eof($fh) and last;
366             }
367 0 0       0 ($count == $triangles) or
368             die "ERROR: got $count facets (expected $triangles)";
369             } # end subroutine _read_binary definition
370             ########################################################################
371              
372             =head2 save
373              
374             $stl->save("filename.stl");
375              
376             $stl->save(binary => "filename.stl");
377              
378             =cut
379              
380             sub save {
381 1     1 1 2608 my $self = shift;
382 1         3 my ($file, @and) = @_;
383              
384 1         2 my $mode;
385 1 50       6 if(@and) {
386 0 0       0 (@and > 1) and croak('too many arguments to save()');
387 0         0 $mode = $file;
388 0         0 ($file) = @and;
389             }
390              
391             # allow filehandle
392 1 50 50     10 unless((ref($file) || '') eq 'GLOB') {
393 0 0       0 open(my $fh, '>', $file) or
394             die "cannot open '$file' for writing $!";
395 0         0 $file = $fh;
396             }
397              
398 1 50       5 $mode = 'ascii' unless($mode);
399              
400 1         5 my $method = '_write_' . lc($mode);
401 1 50       16 $self->can($method) or croak("invalid write mode '$mode'");
402              
403 1         9 $self->$method($file);
404             } # end subroutine save definition
405             ########################################################################
406              
407             =head2 _write_binary
408              
409             $self->_write_binary($filehandle);
410              
411             =cut
412              
413             sub _write_binary {
414 0     0   0 my $self = shift;
415 0         0 my ($fh) = @_;
416              
417 0         0 my ($part, @and) = $self->parts;
418 0 0       0 @and and die 'cannot write binary files with multiple parts';
419              
420 0         0 my $name = $part->name; # utf8 is ok
421 3     3   11707 print $fh $name, "\0" x (80 - do {use bytes; length($name)});
  3         28  
  3         22  
  0         0  
  0         0  
422 0         0 my @facets = $part->facets;
423 0         0 print $fh pack('L', scalar(@facets));
424 0         0 foreach my $facet (@facets) {
425 0         0 print $fh map({map({pack('f', $_)} @$_)} @$facet);
  0         0  
  0         0  
426 0         0 print $fh "\0" x 2;
427             }
428              
429             } # end subroutine _write_binary definition
430             ########################################################################
431              
432             =head2 _write_ascii
433              
434             $self->_write_ascii($filehandle);
435              
436             =cut
437              
438             sub _write_ascii {
439 1     1   3 my $self = shift;
440 1         1 my ($fh) = @_;
441              
442 1         4 my $spaces = '';
443 1     85   6 my $print = sub {print $fh $spaces, @_, "\n"};
  85         192  
444 1 50       40 my @parts = $self->parts or croak("no parts to write");
445 1         104 foreach my $part (@parts) {
446 1         28 $print->('solid ', $part->name);
447 1         2 $spaces = ' 'x2;
448 1         25 foreach my $facet ($part->facets) {
449 12         35 my ($n, @pts) = @$facet;
450 12         122 $print->(join(' ', 'facet normal', @$n));
451 12         20 $spaces = ' 'x4;
452 12         14 $print->('outer loop');
453 12         15 $spaces = ' 'x6;
454 12 50       27 (@pts == 3) or die "invalid facet";
455 12         75 foreach my $pt (@pts) {
456 36         112 $print->(join(' ', 'vertex', @$pt));
457             }
458 12         15 $spaces = ' 'x4;
459 12         19 $print->('endloop');
460 12         15 $spaces = ' 'x2;
461 12         18 $print->('endfacet');
462             }
463 1         3 $spaces = '';
464 1         35 print $fh 'endsolid ', $part->name, "\n";
465             }
466             } # end subroutine _write_ascii definition
467             ########################################################################
468              
469             =head1 AUTHOR
470              
471             Eric Wilhelm @
472              
473             http://scratchcomputing.com/
474              
475             =head1 BUGS
476              
477             If you found this module on CPAN, please report any bugs or feature
478             requests through the web interface at L. I will be
479             notified, and then you'll automatically be notified of progress on your
480             bug as I make changes.
481              
482             If you pulled this development version from my /svn/, please contact me
483             directly.
484              
485             =head1 COPYRIGHT
486              
487             Copyright (C) 2007 Eric L. Wilhelm, All Rights Reserved.
488              
489             =head1 NO WARRANTY
490              
491             Absolutely, positively NO WARRANTY, neither express or implied, is
492             offered with this software. You use this software at your own risk. In
493             case of loss, no person or entity owes you anything whatsoever. You
494             have been warned.
495              
496             =head1 LICENSE
497              
498             This program is free software; you can redistribute it and/or modify it
499             under the same terms as Perl itself.
500              
501             =cut
502              
503             # vi:ts=2:sw=2:et:sta
504             1;