line
stmt
bran
cond
sub
pod
time
code
1
package CAD::Mesh3D;
2
8
8
811375
use warnings;
8
18
8
488
3
8
8
68
use strict;
8
20
8
235
4
8
8
43
use Carp;
8
12
8
634
5
8
8
139
use 5.010; # M::V::R requires 5.010, so might as well make use of the defined-or // notation :-)
8
29
6
8
8
4950
use Math::Vector::Real 0.18;
8
162083
8
682
7
8
8
3384
use CAD::Format::STL qw//;
8
86648
8
450
8
our $VERSION = '0.006003';
9
10
=head1 NAME
11
12
CAD::Mesh3D - Create and Manipulate 3D Vertexes and Meshes and output for 3D printing
13
14
=head1 SYNOPSIS
15
16
use CAD::Mesh3D qw(+STL :create :formats);
17
my $vect = createVertex();
18
my $tri = createFacet($v1, $v2, $v3);
19
my $mesh = createMesh();
20
$mesh->addToMesh($tri);
21
...
22
$mesh->output(STL => $filehandle_or_filename, $ascii_or_binary);
23
24
=head1 DESCRIPTION
25
26
A framework to create and manipulate 3D vertexes and meshes, suitable for generating STL files
27
(or other similar formats) for 3D printing.
28
29
A B is the container for the surface of the shape or object being generated. The surface is broken down
30
into locally-flat pieces known as B. Each B is a triangle made from three points, called
31
B (also spelled as vertices). Each B is made up of three x, y, and z B, which
32
are just floating-point values to represent the position in 3D space.
33
34
=cut
35
36
################################################################
37
# Exports
38
################################################################
39
40
8
8
82
use Exporter 5.57 (); # v5.57 was needed for getting import() without @ISA (# use Exporter 5.57 'import';)
8
176
8
2585
41
our @ISA = qw/Exporter/;
42
our @EXPORT_CREATE = qw(createVertex createFacet createQuadrangleFacets createMesh addToMesh);
43
our @EXPORT_VERTEX = qw(createVertex getx gety getz);
44
our @EXPORT_MATH = qw(unitDelta unitCross unitNormal facetNormal);
45
our @EXPORT_FORMATS = qw(enableFormat output input);
46
our @EXPORT_OK = (@EXPORT_CREATE, @EXPORT_MATH, @EXPORT_FORMATS, @EXPORT_VERTEX);
47
our @EXPORT = @EXPORT_FORMATS;
48
our %EXPORT_TAGS = (
49
create => \@EXPORT_CREATE,
50
vertex => \@EXPORT_VERTEX,
51
math => \@EXPORT_MATH,
52
formats => \@EXPORT_FORMATS,
53
all => \@EXPORT_OK,
54
);
55
56
sub import
57
{
58
12
12
93
my @list = @_;
59
12
26
my @passthru;
60
61
# pass most arguments thru, but if it starts with +, then try to enable that format
62
12
35
foreach my $arg (@list) {
63
27
100
93
if( $arg =~ /^\+/ ) {
64
3
10
$arg =~ s/^\+//;
65
3
17
enableFormat($arg);
66
3
9
next;
67
}
68
24
55
push @passthru, $arg;
69
}
70
12
18599
CAD::Mesh3D->export_to_level(1, @passthru);
71
}
72
73
################################################################
74
# "object" creation
75
################################################################
76
8
8
94
use constant { XCOORD=>0, YCOORD=>1, ZCOORD=>2 }; # avoid magic numbers
8
55
8
17206
77
78
################################################################
79
# "object" creation
80
################################################################
81
# TODO = make the error checking into self-contained routines -- there's
82
# too much duplicated work
83
84
=head1 FUNCTIONS
85
86
=head2 OBJECT CREATION
87
88
The following functions will create the B, B, and B array-references.
89
They can be imported into your script I using the C<:create> tag.
90
91
=head3 createVertex
92
93
my $v = createVertex( $x, $y, $z );
94
95
Creates a B using the given C<$x, $y, $z> floating-point values
96
to represent the x, y, and z coordinates in 3D space.
97
98
=cut
99
100
@CAD::Mesh3D::Vertex::ISA = qw/Math::Vector::Real/;
101
sub createVertex {
102
135
100
135
1
802385
croak sprintf("!ERROR! createVertex(x,y,z): requires 3 coordinates; you supplied %d", scalar @_)
103
unless 3==@_;
104
132
239
return bless V(@_), 'CAD::Mesh3D::Vertex';
105
}
106
107
=head3 createFacet
108
109
my $f = createFacet( $a, $b, $c );
110
111
Creates a B using the three B arguments as the corner points of the triangle.
112
113
Note that the order of the B's B matters, and follows the
114
L to determine the "outside" of
115
the B: if you are looking at the B such that the points are arranged in a
116
counter-clockwise order, then everything from the B towards you (and behind you) is
117
"outside" the surface, and everything beyond the B is "inside" the surface.
118
119
=cut
120
121
sub createFacet {
122
68
100
68
1
15030
croak sprintf("!ERROR! createFacet(t1,t2,t3): requires 3 Vertexes; you supplied %d", scalar @_)
123
unless 3==@_;
124
65
102
foreach my $v ( @_ ) {
125
180
100
100
355
croak sprintf("!ERROR! createFacet(t1,t2,t3): each Vertex must be an array ref or equivalent object; you supplied a scalar\"%s\"", $v//'')
126
unless ref $v;
127
128
174
100
339
croak sprintf("!ERROR! createFacet(t1,t2,t3): each Vertex must be an array ref or equivalent object; you supplied \"%s\"", ref $v)
129
unless UNIVERSAL::isa($v,'ARRAY'); # use function notation, in case $v is not blessed
130
131
171
100
546
croak sprintf("!ERROR! createFacet(t1,t2,t3): each Vertex requires 3 coordinates; you supplied %d: <%s>", scalar @$v, join(",", @$v))
132
unless 3==@$v;
133
}
134
50
144
return bless [@_[0..2]], __PACKAGE__."::Facet";
135
}
136
137
=head4 createQuadrangleFacets
138
139
my @f = createQuadrangleFacets( $a, $b, $c, $d );
140
141
Creates two B using the four B arguments as the corners of a quadrangle
142
(like with C, the arguments are ordered by the right-hand rule). This returns
143
a list of two triangular B, for the triangles B and B.
144
145
=cut
146
147
sub createQuadrangleFacets {
148
4
100
4
1
2183
croak sprintf("!ERROR! createQuadrangleFacets(t1,t2,t3,t4): requires 4 Vertexes; you supplied %d", scalar @_)
149
unless 4==@_;
150
1
4
my ($a,$b,$c,$d) = @_;
151
1
4
return ( createFacet($a,$b,$c), createFacet($a,$c,$d) );
152
}
153
154
=head4 getx
155
156
=head4 gety
157
158
=head4 getz
159
160
my $v = createVertex(1,2,3);
161
my $x = getx($v); # 1
162
my $y = gety($v); # 2
163
my $z = getz($v); # 3
164
165
Grabs the individual x, y, or z coordinate from a vertex
166
167
=cut
168
169
1
1
1
2140
sub getx($) { shift()->[XCOORD] }
170
1
1
1
3
sub gety($) { shift()->[YCOORD] }
171
1
1
1
4
sub getz($) { shift()->[ZCOORD] }
172
173
=head3 createMesh
174
175
my $m = createMesh(); # empty
176
my $s = createMesh($f, ...); # pre-populated
177
178
Creates a B, optionally pre-populating the Mesh with the supplied B.
179
180
=cut
181
182
sub createMesh {
183
19
19
1
16648
foreach my $tri ( @_ ) {
184
55
100
113
croak sprintf("!ERROR! createMesh(...): each triangle must be defined; this one was undef")
185
unless defined $tri;
186
187
54
100
141
croak sprintf("!ERROR! createMesh(...): each triangle requires 3 Vertexes; you supplied %d: <%s>", scalar @$tri, join(",", @$tri))
188
unless 3==@$tri;
189
190
51
67
foreach my $v ( @$tri ) {
191
140
100
100
240
croak sprintf("!ERROR! createMesh(...): each Vertex must be an array ref or equivalent object; you supplied a scalar\"%s\"", $v//'')
192
unless ref $v;
193
194
136
100
228
croak sprintf("!ERROR! createMesh(...): each Vertex must be an array ref or equivalent object; you supplied \"%s\"", ref $v)
195
unless UNIVERSAL::isa($v, 'ARRAY');
196
197
135
100
243
croak sprintf("!ERROR! createMesh(...): each Vertex in each triangle requires 3 coordinates; you supplied %d: <%s>", scalar @$v, join(",", @$v))
198
unless 3==@$v;
199
}
200
}
201
7
139
return bless [@_];
202
}
203
204
=head4 addToMesh
205
206
$mesh->addToMesh($f);
207
$mesh->addToMesh($f1, ... $fN);
208
addToMesh($mesh, $f1, ... $fN);
209
210
Adds B to an existing B.
211
212
=cut
213
214
sub addToMesh {
215
11
11
1
7099
my $mesh = shift;
216
11
100
57
croak sprintf("!ERROR! addToMesh(\$mesh, \@triangles): mesh must have already been created")
217
unless UNIVERSAL::isa($mesh, 'ARRAY');
218
10
21
foreach my $tri ( @_ ) {
219
10
100
100
51
croak sprintf("!ERROR! addToMesh(...): each triangle must be an array ref or equivalent object; you supplied a scalar \"%s\"", $tri//'')
220
unless ref $tri;
221
222
8
100
31
croak sprintf("!ERROR! addToMesh(...): each triangle must be an array ref or equivalent object; you supplied \"%s\"", ref $tri)
223
unless UNIVERSAL::isa($tri, 'ARRAY');
224
225
7
100
31
croak sprintf("!ERROR! addToMesh(...): each triangle requires 3 Vertexes; you supplied %d: <%s>", scalar @$tri, join(",", @$tri))
226
unless 3==@$tri;
227
228
6
14
foreach my $v ( @$tri ) {
229
15
100
100
55
croak sprintf("!ERROR! addToMesh(...): each Vertex must be an array ref or equivalent object; you supplied a scalar \"%s\"", $v//'')
230
unless ref $v;
231
232
13
100
41
croak sprintf("!ERROR! addToMesh(...): each Vertex must be an array ref or equivalent object; you supplied \"%s\"", ref $v)
233
unless UNIVERSAL::isa($v, 'ARRAY');
234
235
12
100
44
croak sprintf("!ERROR! addToMesh(...): each Vertex in each triangle requires 3 coordinates; you supplied %d: <%s>", scalar @$v, join(",", @$v))
236
unless 3==@$v;
237
}
238
239
2
5
push @$mesh, $tri;
240
}
241
2
5
return $mesh;
242
}
243
244
################################################################
245
# math
246
################################################################
247
248
=head2 MATH FUNCTIONS
249
250
use CAD::Mesh3D qw/:math/;
251
252
Most of the math on the three-dimensional B are handled by
253
L; all the vector methods will work on B,
254
as documented for L.
255
However, three-dimensional math can take some special functions that
256
aren't included in the generic matrix library. CAD::Mesh3D implements
257
a few of these special-purpose functions for you.
258
259
They can be called as methods on the B variables, or
260
imported as functions into your script using the C<:math> tag.
261
262
=head3 unitDelta
263
264
my $uAB = unitDelta( $A, $B );
265
# or
266
my $uAB = $A->unitDelta($B);
267
268
Returns a vector (using same structure as a B), which gives the
269
direction from B to B. This is scaled so that
270
the vector has a magnitude of 1.0.
271
272
=cut
273
274
sub CAD::Mesh3D::Vertex::unitDelta {
275
# TODO = argument checking
276
11
11
15
my ($beg, $end) = @_;
277
#my $del = $end - $beg;
278
#return $del->versor();
279
11
11
my $dx = $end->[XCOORD] - $beg->[XCOORD];
280
11
12
my $dy = $end->[YCOORD] - $beg->[YCOORD];
281
11
14
my $dz = $end->[ZCOORD] - $beg->[ZCOORD];
282
11
19
my $m = sqrt( $dx*$dx + $dy*$dy + $dz*$dz );
283
11
100
36
return $m ? [ $dx/$m, $dy/$m, $dz/$m ] : [0,0,0];
284
}
285
286
sub unitDelta {
287
# this is the exportable wrapper at the Mesh3D level
288
11
100
11
1
75
croak "usage: unitDelta( \$vertexA, \$vertexB)" if UNIVERSAL::isa($_[0], 'CAD::Mesh3D'); # don't allow method calls on ::Mesh3D objects: ie, die on $m->unitDelta($A,$B)
289
10
16
CAD::Mesh3D::Vertex::unitDelta(@_)
290
}
291
292
=head3 unitCross
293
294
my $uN = unitCross( $uAB, $uBC );
295
# or
296
my $uN = $uAB->unitCross($uBC);
297
298
Returns the cross product for the two vectors, which gives a vector
299
perpendicular to both. This is scaled so that the vector has a
300
magnitude of 1.0.
301
302
A typical usage would be for finding the direction to the "outside"
303
(the normal-vector) using the right-hand rule. For a B with
304
points A, B, and C, first, find the direction from A to B, and from B
305
to C; the C of those two deltas gives you the normal-vector
306
(and, in fact, that's how S> is implemented).
307
308
my $uAB = unitDelta( $A, $B );
309
my $uBC = unitDelta( $B, $C );
310
my $uN = unitCross( $uAB, $uBC );
311
312
=cut
313
314
sub CAD::Mesh3D::Vertex::unitCross {
315
# TODO = argument checking
316
9
9
11
my ($v1, $v2) = @_; # two vectors
317
9
14
my $dx = $v1->[1]*$v2->[2] - $v1->[2]*$v2->[1];
318
9
12
my $dy = $v1->[2]*$v2->[0] - $v1->[0]*$v2->[2];
319
9
10
my $dz = $v1->[0]*$v2->[1] - $v1->[1]*$v2->[0];
320
9
15
my $m = sqrt( $dx*$dx + $dy*$dy + $dz*$dz );
321
9
100
43
return $m ? [ $dx/$m, $dy/$m, $dz/$m ] : [0,0,0];
322
}
323
324
sub unitCross {
325
# this is the exportable wrapper at the Mesh3D level
326
9
100
9
1
54
croak "usage: unitCross( \$vertexA, \$vertexB)" if UNIVERSAL::isa($_[0], 'CAD::Mesh3D'); # don't allow method calls on ::Mesh3D objects: ie, die on $m->unitCross($A,$B)
327
8
15
CAD::Mesh3D::Vertex::unitCross(@_)
328
}
329
330
=head3 facetNormal
331
332
=head3 unitNormal
333
334
my $uN = facetNormal( $facet );
335
# or
336
my $uN = $facet->normal();
337
# or
338
my $uN = unitNormal( $vertex1, $vertex2, $vertex3 )
339
340
Uses S> and S> to find the normal-vector
341
for the given B, given the right-hand rule order for the B's
342
vertexes.
343
344
=cut
345
346
sub CAD::Mesh3D::Facet::normal($) {
347
# TODO = argument checking
348
4
4
5
my ($A,$B,$C) = @{ shift() }; # three vertexes of the facet
4
8
349
4
7
my $uAB = unitDelta( $A, $B );
350
4
10
my $uBC = unitDelta( $B, $C );
351
4
6
return unitCross( $uAB, $uBC );
352
}
353
354
sub facetNormal {
355
# this is the exportable wrapper at the Mesh3D level
356
3
100
3
1
41
croak "usage: facetNormal( \$facetF )" if UNIVERSAL::isa($_[0], 'CAD::Mesh3D'); # don't allow method calls on ::Mesh3D objects: ie, die on $m->facetNormal($F)
357
2
3
CAD::Mesh3D::Facet::normal($_[0])
358
}
359
360
sub unitNormal {
361
# this is the exportable wrapper at the Mesh3D level
362
2
100
2
1
41
croak "usage: unitNormal( \$vertexA, \$vertexB, \$vertexC )" if UNIVERSAL::isa($_[0], 'CAD::Mesh3D'); # don't allow method calls on ::Mesh3D objects: ie, die on $m->unitNormal(@$F)
363
1
2
CAD::Mesh3D::Facet::normal( createFacet(@_) )
364
}
365
366
################################################################
367
# enabled formats
368
################################################################
369
our %EnabledFormats = ();
370
371
=head2 FORMATS
372
373
If you want to be able to output your mesh into a format, or input a mesh from a format, you need to enable them.
374
This makes it simple to incorporate an add-on C.
375
376
Note to developers: L documents how to write a submodule (usually in the C
377
namespace) to provide the appropriate input and/or output functions for a given format. L is a
378
format that ships with B, and provides an example of how to implement a format module.
379
380
The C, C, and C functions can be imported using the C<:formats> tag.
381
382
=head3 enableFormat
383
384
use CAD::Mesh3D qw/+STL :formats/; # for the format 'STL'
385
# or
386
enableFormat( $format )
387
# or
388
enableFormat( $format => $moduleName )
389
390
C<$moduleName> should be the name of the module that will provide the C<$format> routines. It will default to 'CAD::Mesh3D::$format'.
391
The C<$format> is case-sensitive, so C will try to enable two separate formats.
392
393
=cut
394
395
sub enableFormat {
396
9
100
9
1
216727
my $formatName = defined $_[0] ? $_[0] : croak "!ERROR! enableFormat(...): requires name of format";
397
8
100
26
my $formatModule = defined $_[1] ? $_[1] : "CAD::Mesh3D::$formatName";
398
8
45
(my $key = $formatModule . '.pm') =~ s{::}{/}g;
399
8
100
16
eval { require $key unless exists $INC{$key}; 1; } or do {
8
100
2620
7
29
400
1
6
local $" = ", ";
401
1
32
croak "!ERROR! enableFormat( @_ ): \n\tcould not import $formatModule\n\t$@";
402
};
403
7
30
my %io = ();
404
7
100
22
eval { %io = $formatModule->_io_functions(); 1; } or do {
7
371
6
35
405
1
7
local $" = ", ";
406
1
26
croak "!ERROR! enableFormat( @_ ): \n\t$formatModule doesn't seem to correctly provide the input and/or output functions\n\t";
407
};
408
6
100
1
32
$io{input} = sub { croak "Input function for $formatName is not available" } unless defined $io{input};
1
15
409
6
100
1
19
$io{output} = sub { croak "Output function for $formatName is not available" } unless defined $io{output};
1
24
410
# carp "STL input() = $io{input}" if defined $io{input};
411
# carp "STL output() = $io{output}" if defined $io{output};
412
# see https://subversion.assembla.com/svn/pryrt/trunk/perl/perlmonks/mesh3d-unasked-question-20190215.pl for workaround using function
413
414
6
41
$EnabledFormats{$formatName} = { %io, module => $formatModule };
415
}
416
417
################################################################
418
# file output
419
################################################################
420
421
=head3 output
422
423
Output the B to a 3D output file in the given format
424
425
use CAD::Mesh3D qw/+STL :formats/;
426
$mesh->output('STL' => $file);
427
$mesh->output('STL' => $file, @args );
428
429
Outputs the given C<$mesh> to the indicated file.
430
431
The C<$file> argument is either an already-opened filehandle, or the name of the file
432
(if the full path is not specified, it will default to your script's directory),
433
or "STDOUT" or "STDERR" to direct the output to the standard handles.
434
435
You will need to look at the documentation for your selected format to see what additional
436
C<@args> it might want. Often, the args will be used for setting format options, like
437
picking between ASCII and binary file formats, or similar.
438
439
You also may need to whether your chosen format even supports file output; it is possible
440
that some do not. (For example, some formats may have a binary structure that is free
441
to read, but requires paying a license to write.)
442
443
=cut
444
445
sub output {
446
18
18
1
123086
my ($mesh, $format, @file_and_args) = @_;
447
18
86
$EnabledFormats{$format}{output}->( $mesh, @file_and_args );
448
}
449
450
=head3 input
451
452
use CAD::Mesh3D qw/+STL :formats/;
453
my $mesh = input( 'STL' => $file, @args );
454
455
Creates a B by reading the given file using the specified format.
456
457
The C<$file> argument is either an already-opened filehandle, or the name of the file
458
(if the full path is not specified, it will default to your script's directory),
459
or "STDIN" to grab the input from the standard input handle.
460
461
You will need to look at the documentation for your selected format to see what additional
462
C<@args> it might want. Often, the args will be used for setting format options, like
463
picking between ASCII and binary file formats, or similar.
464
465
You also may need to whether your chosen format even supports file input; it is possible
466
that some do not. (For example, some formats, like a PNG image, may not contain the
467
necessary 3d information to create a mesh.)
468
469
=cut
470
471
sub input {
472
8
8
1
225233
my ($format, @file_and_args) = @_;
473
8
61
$EnabledFormats{$format}{input}->( @file_and_args );
474
}
475
476
=head1 SEE ALSO
477
478
=over
479
480
=item * L - This provides matrix math
481
482
The B were implemented using this module, to easily handle the
483
B and B calculations.
484
485
=item * L - This provides simple input and output between
486
STL files and an array-of-arrays perl data structure.
487
488
Adding more features to this module (especially the math on the B and C)
489
and making a generic interface (which can be made to work with other formats) were the two
490
primary motivators behind the CAD::Mesh3D development.
491
492
This module is still used as the backend for the L format-module.
493
494
=back
495
496
=head1 TODO
497
498
=over
499
500
=item * Add more math for B and B, as new functions are identified
501
as being useful.
502
503
=back
504
505
=head1 AUTHOR
506
507
Peter C. Jones Cpetercj AT cpan DOT orgE>
508
509
=begin html
510
511
512
513
514
515
516
517
=end html
518
519
=head1 COPYRIGHT
520
521
Copyright (C) 2017,2018,2019,2020,2021,2024 Peter C. Jones
522
523
=head1 LICENSE
524
525
This program is free software; you can redistribute it and/or modify it
526
under the terms of either: the GNU General Public License as published
527
by the Free Software Foundation; or the Artistic License.
528
529
See L for more information.
530
531
=cut
532
533
1;