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; |