File Coverage

blib/lib/CAD/Mesh3D/FormatSTL.pm
Criterion Covered Total %
statement 170 191 89.0
branch 59 84 70.2
condition 11 15 73.3
subroutine 22 23 95.6
pod 9 9 100.0
total 271 322 84.1


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