File Coverage

blib/lib/CAD/Drawing/Template.pm
Criterion Covered Total %
statement 10 12 83.3
branch n/a
condition n/a
subroutine 4 4 100.0
pod n/a
total 14 16 87.5


line stmt bran cond sub pod time code
1             package CAD::Drawing::Template;
2             our $VERSION = '0.01';
3              
4             # This code is copyright 2004 Eric L. Wilhelm.
5             # See below for licensing details.
6              
7 1     1   38568 use warnings;
  1         3  
  1         39  
8 1     1   6 use strict;
  1         2  
  1         50  
9              
10 1     1   7 use Carp;
  1         5  
  1         102  
11              
12 1     1   522 use CAD::Drawing;
  0            
  0            
13             use CAD::Calc qw(
14             iswithin
15             print_line
16             );
17             use Storable qw(dclone);
18              
19             our @tags = qw(
20             data
21             vtable
22             geo
23             block
24             function
25             );
26              
27             # allow later configurability:
28             my $comment_layer = "comment";
29             my $fit_layer = "fit";
30             ########################################################################
31             =pod
32              
33             =head1 NAME
34              
35             CAD::Drawing::Template - Replace tags with text and geometry.
36              
37             =head1 SYNOPSIS
38              
39             my $bp = CAD::Drawing::Template->new();
40             $bp->load('my_template.dxf');
41             # set some values for the boiler-plate:
42             $bp->set_data(foo => 'value for foo');
43             my @parts = qw(E8955 Q4200);
44             $bp->set_vtable(parts => \@parts);
45             $bp->set_geom(birdseye => 'birdseye.dwg');
46             my $drw = $bp->done(pass => qr/^shipping/, die => 0);
47             $drw->save('output.dxf');
48              
49             =head1 Input Templates
50              
51             Input templates must be CAD::Drawing compatible files or objects. These
52             are brought into the CAD::Drawing::Template object via load() or
53             import() and searched for 'texts' items which match the formats listed
54             below.
55              
56             The tags may be on any layer in the drawing except 'comments' and 'fit'
57             which are reserved names. The 'comments' layer is completely discarded,
58             and the 'fit' layer must only contain rectangles (which are necessary
59             for scaling calculations, but are also discarded.)
60              
61             =head1 Tag Formats
62              
63             The 'tags' are 'texts' entities (single-line text in dwg/dxf formats)
64             which must begin and end with matching angle-brackets ('<' and '>'.)
65             These text entities are sourced for their insertion point, text height,
66             and name. Future versions of this module will support orientations,
67             fonts, and options within the tag text itself.
68              
69             In general, tags are formatted as <$type:$name>. Where $type is one of
70             the types defined below and $name is the name of the tag (to be used in
71             addressing it via the set_*() functions.
72              
73             Tag names should adhere to the same rules as perl variable names:
74              
75             1. Alphanumeric characters (and underscores) only (a-z, A-Z, 0-9, _)
76             2. Must start with a letter ("a2", not "2a", and not "_2")
77             3. Case senSitive
78              
79             The following tag types are supported. Examples show the text string
80             that would be in the template.
81              
82             =over
83              
84             =item data
85              
86             A 'data' tag is replaced with a single scalar value.
87              
88             Examples:
89              
90            
91            
92            
93              
94             =item vtable
95              
96             A 'vtable' tag is replaced with a list of values, each one some distance
97             below the previous, with the top line's insertion point at the tag's
98             insertion point.
99              
100             Examples:
101              
102            
103            
104              
105             =item geo
106              
107             Loads a drawing and fits it into a rectangle.
108              
109             NOTE: The rectangle must be on a layer named 'fit' and contain the
110             insertion point of the tag. Each tag must be within a
111             rectangle on the 'fit' layer and each rectangle on the 'fit' layer must
112             have exactly one tag inside of it. If this is not true,
113             death ensues. These rectangles are removed from the drawing before
114             output.
115              
116             While a rectangle may contain two 'geo' tags, each tag must be contained
117             in one rectangle (the innermost containing rectangle wins.)
118              
119             Examples:
120              
121            
122            
123              
124             =item block
125              
126             Loads a drawing to the insertion point.
127              
128             Examples:
129              
130            
131            
132              
133             =item function
134              
135             A 'function' tag calls a perl function, and afterwards behaves like a
136             data tag. There is no set_function() function, since this tag is
137             supposed to be fully-automatic.
138              
139             The function is assumed to be a member of a Perl module. If that module
140             is not already loaded, it is require()'d within an eval() statement
141             before the function is called. There is no provision for passing values
142             to these functions. The function is called in a list context, and the
143             results joined by spaces. Any errors encountered in calling the
144             function will be croak()'d along with the function name.
145              
146             If the module is contained under a non-standard path (one which is not
147             included in @INC), it should be preceded by a directory path. This
148             directory is then brought into @INC via the 'use lib' pragma.
149              
150             Examples:
151              
152             # uses main::date()
153            
154            
155            
156              
157             =back
158              
159             =head1 AUTHOR
160              
161             Eric L. Wilhelm
162              
163             http://scratchcomputing.com
164              
165             =head1 COPYRIGHT
166              
167             This module is copyright (C) 2004-2006 by Eric L. Wilhelm.
168              
169             =head1 LICENSE
170              
171             This module is distributed under the same terms as Perl. See the Perl
172             source package for details.
173              
174             You may use this software under one of the following licenses:
175              
176             (1) GNU General Public License
177             (found at http://www.gnu.org/copyleft/gpl.html)
178             (2) Artistic License
179             (found at http://www.perl.com/pub/language/misc/Artistic.html)
180              
181             =head1 Modifications
182              
183             The source code of this module is made freely available and
184             distributable under the GPL or Artistic License. Modifications to and
185             use of this software must adhere to one of these licenses. Changes to
186             the code should be noted as such and this notification (as well as the
187             above copyright information) must remain intact on all copies of the
188             code.
189              
190             Additionally, while the author is actively developing this code,
191             notification of any intended changes or extensions would be most helpful
192             in avoiding repeated work for all parties involved. Please contact the
193             author with any such development plans.
194              
195             =head1 SEE ALSO
196              
197             CAD::Drawing
198              
199             =cut
200             ########################################################################
201              
202             =head1 Constructors
203              
204             =head2 new
205              
206             my $bp = CAD::Drawing::Template->new(%options);
207              
208             =over
209              
210             =item Valid options
211              
212             pass => [@list], # type:name strings only
213              
214             =back
215              
216             =cut
217             sub new {
218             my $caller = shift;
219             my $class = ref($caller) || $caller;
220             my $self = {@_};
221             bless($self, $class);
222             return($self);
223             } # end subroutine new definition
224             ########################################################################
225              
226             =head2 clone
227              
228             Duplicates the boiler-plate as a snapshot in time (useful to save effort
229             in loops.)
230              
231             my $bp2 = $bp->clone();
232              
233             =cut
234             sub clone {
235             my $self = shift;
236             # accept the same options as done() ?
237             my $ret = dclone($self);
238             return($ret);
239             } # end subroutine clone definition
240             ########################################################################
241              
242             =head1 Template Handling
243              
244             Getting template data in and finished data out.
245              
246             =head2 load
247              
248             $bp->load($filename);
249             # or:
250             $bp->load($drawing_object);
251              
252             =cut
253             sub load {
254             my $self = shift;
255             my $file = shift;
256             if(ref($file)) {
257             $self->{drw} = $file;
258             }
259             else {
260             my $drw = CAD::Drawing->new();
261             $drw->load($file, {nl => ['comment']});
262             $self->{drw} = $drw;
263             }
264             $self->find_tags();
265             } # end subroutine load definition
266             ########################################################################
267              
268             =head2 done
269              
270             $drw = $bp->done(%options);
271              
272             Options:
273              
274             pass - array ref of pass-able tags ("type:name" strings)
275             strict - croak on tags not listed in pass
276             warnings - carp warnings
277             default - "drop" or "pass" (default) action for un-passed tags
278              
279             =cut
280             sub done {
281             my $self = shift;
282             my %options = @_;
283             my %pass;
284             if($options{pass}) {
285             (ref($options{pass}) eq "ARRAY") or
286             croak("done() option 'pass' must be an array\n");
287             %pass = map({$_ => 1} @{$options{pass}});
288             }
289             else {
290             carp "strict option without pass"
291             if $options{strict} and $options{warnings};
292             }
293             foreach my $type (keys(%{$self->{tags}})) {
294             foreach my $name (keys(%{$self->{tags}{$type}})) {
295             $pass{"$type:$name"} and next;
296             my $message = "tag not set: '$type:$name'";
297             $options{strict} and
298             die "\n DEATH: $message\n";
299             $options{warnings} and
300             warn "$message\n";
301             if($options{default} eq "drop") {
302             $options{warnings} and
303             warn "implicit drop of tag: '$type:$name'\n";
304             my $tag = $self->{tags}{$type}{$name};
305             my $drw = $self->{drw};
306             $drw->remove($tag->{addr});
307              
308             }
309             else {
310             $options{warnings} and
311             warn "implicit passing tag: '$type:$name'\n";
312             }
313             }
314             }
315             my $drw = $self->{drw};
316             return($drw);
317             } # end subroutine done definition
318             ########################################################################
319              
320             =head2 tag_list
321              
322             $bp->tag_list();
323              
324             =cut
325             sub tag_list {
326             my $self = shift;
327             my @ret;
328             foreach my $type (keys(%{$self->{tags}})) {
329             foreach my $item (keys(%{$self->{tags}{$type}})) {
330             push(@ret, "$type:$item");
331             }
332             }
333             return(@ret);
334             } # end subroutine tag_list definition
335             ########################################################################
336              
337             =head1 Methods
338              
339             These methods allow you to manipulate the template.
340              
341             =head2 set_data
342              
343             Replace the tag's text with a string.
344              
345             $bp->set_data($name => $value);
346              
347             # replace the tag with the department's name:
348             $dep = 'Department of Redundancy Department';
349             $bp->set_data(department => $dep);
350              
351             =cut
352             sub set_data {
353             my $self = shift;
354             my ($name, $val) = @_;
355             my $type = 'data';
356             $self->{tags}{$type}{$name} or
357             die "no such tag $type:$name\n";
358             my $drw = $self->{drw};
359             my $tag = $self->{tags}{$type}{$name};
360             $drw->Set({string => $val}, $tag->{addr});
361             delete($self->{tags}{$type}{$name});
362             } # end subroutine set_data definition
363             ########################################################################
364              
365             =head2 set_vtable
366              
367             Remove the tag entity, and create a series of texts, each spaced
368             slightly below the previous.
369              
370             $bp->set_vtable($name => \@list);
371              
372             # uses the tag:
373             # create a table of revision notes:
374             my @rev = (
375             ' 1 Changed fonts for PHB',
376             ' 2 Changed fonts back (for same)',
377             ' 3 Removed all text',
378             );
379             $bp->set_vtable(revision => \@rev);
380              
381             =cut
382             sub set_vtable {
383             my $self = shift;
384             my ($name, $val) = @_;
385             my $type = 'vtable';
386             $self->{tags}{$type}{$name} or
387             die "no such tag $type:$name\n";
388             my $drw = $self->{drw};
389             my $tag = $self->{tags}{$type}{$name};
390             my $h = $drw->Get("height", $tag->{addr});
391             my @pt = $drw->Get("pt", $tag->{addr});
392             $drw->remove($tag->{addr});
393             $drw->addtextlines(\@pt, join("\n", @$val),
394             {height => $h, spacing => 1.2});
395             delete($self->{tags}{$type}{$name});
396             } # end subroutine set_vtable definition
397             ########################################################################
398              
399             =head2 set_geo
400              
401             Load a drawing into the template, scaling it to fit within an enclosing
402             rectangle.
403              
404             $bp->set_geo($name => $filename);
405             # or:
406             $bp->set_geo($name => $drawing_object);
407              
408             =cut
409             sub set_geo {
410             my $self = shift;
411             my ($name, $source) = @_;
412             # print "apply geo $name\n";
413             my $type = 'geo';
414             $self->{tags}{$type}{$name} or
415             die "no such tag $type:$name\n";
416             my $in = $self->load_drawing($name, $source);
417             my $drw = $self->{drw};
418             my $tag = $self->{tags}{$type}{$name};
419             my @rec = @{$tag->{rectangle}{pts}};
420             # print "rectangle: @{$rec[0]} x @{$rec[1]}\n";
421             $in->fit_to_bound(\@rec, 0);
422             my @list = $in->GroupClone($drw);
423             $drw->remove($tag->{addr});
424             $drw->remove($tag->{rectangle}{addr});
425             delete($self->{tags}{$type}{$name});
426             # $drw->show(hang => 1);
427              
428             } # end subroutine set_geo definition
429             ########################################################################
430              
431             =head2 set_block
432              
433             Identical to set_geo, except no scaling is performed.
434              
435             $bp->set_block($name => $filename);
436             # or:
437             $bp->set_block($name => $drawing_object);
438              
439             =cut
440             sub set_block {
441             my $self = shift;
442             my ($name, $source) = @_;
443             my $type = 'block';
444             $self->{tags}{$type}{$name} or
445             die "no such tag $type:$name\n";
446             my $in = $self->load_drawing($name, $source);
447             my $drw = $self->{drw};
448             my $tag = $self->{tags}{$type}{$name};
449             my @pt = @{$tag->{pt}};
450             my @list = $drw->place($in, \@pt);
451             $drw->remove($tag->{addr});
452             delete($self->{tags}{$type}{$name});
453             } # end subroutine set_block definition
454             ########################################################################
455              
456             =head1 Guts
457              
458             These methods are used internally.
459              
460             =head2 find_tags
461              
462             Grabs the addresses of all tags which match the regex m/^<.*>$/. Any
463             which were are in the array @{$self->{pass}} are left untouched.
464              
465             After finding all of the tags, execute any tags which were
466             found.
467              
468             $bp->find_tags();
469              
470             =cut
471             sub find_tags {
472             my $self = shift;
473             my $drw = $self->{drw};
474             my %pass;
475             if(my $pass = $self->{pass}) {
476             (ref($pass) eq "ARRAY") or
477             croak "pass => $pass is not an array ref";
478             foreach my $tag (@$pass) {
479             $pass{$tag} = 1;
480             }
481             }
482             # first get all of the texts with <>
483             my @layers = $drw->list_layers();
484             my $regex = qr/^<.*>$/;
485             my @addr;
486             foreach my $layer (@layers) {
487             push(@addr, $drw->addr_by_regex($layer, $regex));
488             }
489             # print scalar(@addr), " texts found\n";
490             my %tags_okay = map({$_ => 1} @tags);
491             foreach my $addr (@addr) {
492             my $tag = $drw->Get("string", $addr);
493             my ($type, $name, $opts) = parse_tag($tag);
494             # just ignore pass-through tags
495             $pass{"$type:$name"} and next;
496             # print "type: $type, name: $name\n";
497             $tags_okay{$type} or
498             croak("$type is not one of @tags\n");
499             if($type eq "function") {
500             # print "must call function $name\n";
501             $self->run_function($name, $addr);
502             next;
503             # XXX why would we need to create a tag item for functions?
504             }
505             $self->{tags}{$type}{$name} and
506             croak "multiple tags found for $type:$name\n";
507             my @pt = $drw->Get("pt", $addr);
508             $self->{tags}{$type}{$name} = {
509             pt => \@pt,
510             type => $type,
511             name => $name,
512             addr => $addr,
513             };
514             }
515             # this guy needs to see all of the geo tags
516             $self->geo_match();
517             } # end subroutine find_tags definition
518             ########################################################################
519              
520             =head2 geo_match
521              
522             Performs the rectangle-tag matching. Must be able to reduce each geo
523             tag to an innermost enclosing rectangle or dies with much whining.
524              
525             $bp->geo_match();
526              
527             =cut
528             sub geo_match {
529             my $self = shift;
530             $self->{tags} or die "geo_match called before find_tags?";
531             my $geo = $self->{tags}{geo};
532             $geo or return();
533             my @tags = keys(%$geo);
534             unless(@tags) {
535             warn("tags/geo defined, but null!(?)\n");
536             return();
537             }
538             my $drw = $self->{drw};
539             my @fit_addr = $drw->addr_by_type('fit', 'plines');
540             (@fit_addr == @tags) or croak("geo (",
541             scalar(@tags), ")/fit (", scalar(@fit_addr),
542             ") count mismatch\n");
543             my @recs = map({[$drw->Get("pts", $_)]} @fit_addr);
544             my @matches;
545             for(my $i = 0; $i < @recs; $i++) {
546             for(my $g = 0; $g < @tags; $g++) {
547             my $addr = $geo->{$tags[$g]}{addr};
548             my @pt = $drw->Get("pt", $addr);
549             ## print "check ", print_line($recs[$i]), " vs @pt\n";
550             if(iswithin($recs[$i], \@pt)) {
551             push(@{$matches[$i]}, $tags[$g]);
552             }
553             }
554             }
555             # go through matches in least-matched order (thus, the first to
556             # speak for a tag gets to keep it
557             my @order = sort(
558             {
559             scalar(@{$matches[$a]}) <=>
560             scalar(@{$matches[$b]})
561             } 0..$#matches);
562             my %map_rec;
563             foreach my $i (@order) {
564             my @found = @{$matches[$i]};
565             foreach my $name (@found) {
566             defined($map_rec{$name}) and next;
567             $map_rec{$name} = $i;
568             }
569             }
570             foreach my $name (@tags) {
571             defined($map_rec{$name}) or
572             die "geo tag $name has no rectangle!\n";
573             my $i = $map_rec{$name};
574             # print "rectangle $i connects to $name\n";
575             $geo->{$name}{rectangle} = {
576             addr => $fit_addr[$i],
577             pts => [
578             ($drw->getExtentsRec([$fit_addr[$i]]))[0,2]
579             ],
580             };
581             }
582            
583             } # end subroutine geo_match definition
584             ########################################################################
585              
586             =head2 run_function
587              
588             Runs the function $name (in a list context) and places it's results
589             (joined with spaces) into the string at $addr.
590              
591             $bp->run_function($name, $addr);
592              
593             =cut
594             sub run_function {
595             my $self = shift;
596             my ($name, $addr) = @_;
597             if($name =~ s#^(.*)/+##) {
598             my $lib = $1;
599             # print "using lib: $lib\n";
600             eval("use lib '$lib';");
601             $@ and croak("problem with lib '$lib'\n\t $@\n");
602             }
603             my $mod = 'main';
604             if($name =~ s/^(.*):://) {
605             $mod = $1;
606             eval("require $mod;");
607             $@ and croak("problem with module '$mod'\n\t: $@\n");
608             }
609             if($mod->can($name)) {
610             my @data = $mod->$name;
611             my $string = join(" ", @data);
612             # print "got data '@data' out of $mod->$name\n";
613             my $drw = $self->{drw};
614             $drw->Set({string => $string}, $addr);
615             }
616             else {
617             croak("$mod does not define a function named '$name'\n");
618             }
619             } # end subroutine run_function definition
620             ########################################################################
621              
622             =head2 load_drawing
623              
624             Loads a drawing from a filename or CAD::Drawing object and returns a
625             CAD::Drawing object.
626              
627             $drw = $bp->load_drawing($name => $filename);
628             # or:
629             $drw = $bp->load_drawing($name => $drawing_object);
630              
631             =cut
632             sub load_drawing {
633             my $self = shift;
634             my ($name, $source) = @_;
635             my $in;
636             if(ref($source)) {
637             # had better be a drw
638             $in = $source;
639             }
640             else {
641             $in = CAD::Drawing->new();
642             $in->load($source);
643             }
644             return($in);
645             } # end subroutine load_drawing definition
646             ########################################################################
647              
648             =head1 Functions
649              
650             Not object-oriented, and likely not exported.
651              
652             =head2 parse_tag
653              
654             Break a tag into type, name, and options. When (and if) options are
655             supported within the tags, they will be handled here.
656              
657             ($type, $name, $options) = parse_tag($tag);
658              
659             =cut
660             sub parse_tag {
661             my ($string) = @_;
662             my $tag = $string;
663             # print "tag: $tag\n";
664             ($tag =~ s/^
665             ($tag =~ s/>$//) or croak("string: '$string' is invalid\n");
666             my ($type, $name) = split(/:/, $tag, 2);
667             # XXX for options, we must parse $type
668             my $options = {};
669             return($type, $name, $options);
670             } # end subroutine parse_tag definition
671             ########################################################################
672             1;