File Coverage

blib/lib/CAD/Mesh3D.pm
Criterion Covered Total %
statement 114 114 100.0
branch 64 64 100.0
condition 8 8 100.0
subroutine 29 29 100.0
pod 15 15 100.0
total 230 230 100.0


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             issues
513             github ci status
514             appveyor build status
515             Coverage Status
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;