File Coverage

blib/lib/CAD/Drawing/IO/Tk.pm
Criterion Covered Total %
statement 1 3 33.3
branch n/a
condition n/a
subroutine 1 1 100.0
pod n/a
total 2 4 50.0


line stmt bran cond sub pod time code
1             package CAD::Drawing::IO::Tk;
2             our $VERSION = '0.04';
3              
4 1     1   22448 use CAD::Drawing;
  0            
  0            
5             use CAD::Drawing::Defined;
6              
7             use CAD::Calc qw(dist2d);
8              
9             # with the new plug-in architecture, this seems odd to have a
10             # strictly-inherited module in the IO::* namespace (when this thing
11             # finally grows-up, maybe we use a GUI::* namespace?)
12              
13             our $is_inherited = 1;
14              
15              
16             use vars qw(
17             %dsp
18             $textsize
19             $text_base
20             );
21              
22             $text_base = 8;
23              
24             use warnings;
25             use strict;
26             use Carp;
27              
28             my %default = (
29             width => 800,
30             height => 600,
31             zoom => "fit",
32             );
33              
34             =pod
35              
36             =head1 NAME
37              
38             CAD::Drawing::IO::Tk - GUI I/O methods for CAD::Drawing
39              
40             =head1 NOTICE
41              
42             This module is considered extremely pre-ALPHA and its use is probably
43             deprecated by the time you read this.
44              
45             =head1 AUTHOR
46              
47             Eric L. Wilhelm
48              
49             http://scratchcomputing.com
50              
51             =head1 COPYRIGHT
52              
53             This module is copyright (C) 2004-2006 by Eric L. Wilhelm. Portions
54             copyright (C) 2003 by Eric L. Wilhelm and A. Zahner Co.
55              
56             =head1 LICENSE
57              
58             This module is distributed under the same terms as Perl. See the Perl
59             source package for details.
60              
61             You may use this software under one of the following licenses:
62              
63             (1) GNU General Public License
64             (found at http://www.gnu.org/copyleft/gpl.html)
65             (2) Artistic License
66             (found at http://www.perl.com/pub/language/misc/Artistic.html)
67              
68             =head1 NO WARRANTY
69              
70             This software is distributed with ABSOLUTELY NO WARRANTY. The author,
71             his former employer, and any other contributors will in no way be held
72             liable for any loss or damages resulting from its use.
73              
74             =head1 Modifications
75              
76             The source code of this module is made freely available and
77             distributable under the GPL or Artistic License. Modifications to and
78             use of this software must adhere to one of these licenses. Changes to
79             the code should be noted as such and this notification (as well as the
80             above copyright information) must remain intact on all copies of the
81             code.
82              
83             Additionally, while the author is actively developing this code,
84             notification of any intended changes or extensions would be most helpful
85             in avoiding repeated work for all parties involved. Please contact the
86             author with any such development plans.
87              
88             =head1 SEE ALSO
89              
90             CAD::Drawing::IO
91             Tk
92              
93             =cut
94              
95             =head1 Methods
96              
97             There is no constructor for this class, its methods are inherited via
98             CAD::Drawing::IO
99              
100             =head1 Thoughts
101              
102             Need to re-structure the entire deal to have its own object which
103             belongs to the drawing object (or does the drawing object belong to this
104             object?) Either way, we need to be able to build-up into interactive
105             commands (possibly using eval("\$drw->$command"); ?)
106              
107             Ultimately, the focus here will likely drift toward supporting perlcad
108             and enabling use of perlcad from within CAD::Drawing scripts. However,
109             the nature of lights-out scripting vs the nature of on-screen drafting
110             is quite different, so there will be some tricks involved. Once each
111             entity has its own class, the ability to install callbacks and the
112             resolution of notifications should get easier. But, there will still
113             be the issue that a debug popup does not know it will appear when the
114             entities are created, while a drafting viewport does (or does it?)
115              
116             Possibly, adding a list of tk-id's to each $obj as it is drawn would be
117             a good starting point, but this gets us into trouble with multiple
118             viewports.
119              
120             =cut
121              
122             =head2 show
123              
124             Creates a new window (no options are required.)
125              
126             $drw->show(%options);
127              
128             =over
129              
130             =item Available Options
131              
132             forkokay => bool -- Attempt to fork the new window
133             window => MainWindow -- Use the pre-existing Tk object
134             stl => Message -- Use pre-existing Message widget
135             size => [W,H] -- Specify window size in pixels
136             width => W -- alias to size
137             height => H -- ditto
138             center => [X,Y] -- Center the drawing at (X,Y)
139             scale => factor -- Zoom by factor (default to fit)
140             bgcolor => color -- defaults to "white"
141             hang => boolean -- if not, you just get the canvas widget
142             items => \@list -- sorry, not compatible with select_addr :(
143              
144             =back
145              
146             =cut
147             sub show {
148             my $self = shift;
149             my %options = @_;
150             # XXX cannot do "use" or we get silly _TK_EXIT_(0) from everywhere!
151             require Tk;
152             require Tk::WorldCanvas;
153             my $kidpid;
154             if($options{forkokay}) {
155             $SIG{CHILD} = 'IGNORE';
156             if($kidpid = fork()) {
157             return($kidpid);
158             }
159             defined($kidpid) or croak("cannot fork $!\n");
160             $options{forkokay} = 0;
161             }
162             my $mw = $options{window};
163             defined($mw) || ($mw = MainWindow->new());
164             unless($options{size}) {
165             foreach my $item ("width", "height") {
166             my $val = $options{$item};
167             $val || ($val = $default{$item});
168             push(@{$options{size}}, $val);
169             }
170             }
171             $options{bgcolor} || ($options{bgcolor} = "white");
172             # FIXME: should have an indication of viewport number?
173             $options{title} || ($options{title} = "Drawing");
174             $mw->title($options{title});
175             my ($w,$h) = @{$options{size}};
176             # print "requesting $w x $h\n";
177             my $cnv = $mw->WorldCanvas(
178             '-bg' => $options{bgcolor},
179             '-width' => $options{size}[0],
180             '-height' => $options{size}[1],
181             );
182             # XXX scrolling when you want to wheelzoom is icky. What's up with
183             # that? (Tk::Canvas is a mess, that's what!)
184             ## print "bound to ", $cnv->bind('<4>'), "\n";
185             $cnv->pack(-fill => 'both', -expand=>1);
186             # XXX break this out into pieces
187             my $stl;
188             my %stl_conf = (
189             -anchor => "sw",
190             -width => $w,
191             -justify=>"left",
192             );
193             my %stl_pack = (-fill => 'x', -expand=>0, -side => "bottom");
194            
195             unless($stl = $options{stl}) {
196             $stl = $mw->Message(%stl_conf);
197             $stl->pack(%stl_pack);
198             }
199             else {
200             $stl->configure(%stl_conf);
201             $stl->pack(%stl_pack);
202             }
203             # FIXME: cannot just have a simplistic command line, it has to be powerful
204             # my $cmd = $mw->Text(
205             # -height=> 2,
206             # -width => $w,
207             # );
208             # $cmd->pack(-fill => 'x', -expand=>0, -side => "bottom");
209             # XXX $self here is a drawing, maybe that's not what we want...
210             $self->tkbindings($mw, $cnv, $stl);
211             $options{items} || ($options{items} = $self->select_addr({all=>1}));
212             $self->Draw($cnv, %options);
213             $cnv->viewAll();
214             text_size_reset($cnv);
215             if(defined($kidpid) or $options{hang}) {
216             $mw->MainLoop;
217             }
218             else {
219             return($cnv);
220             }
221             } # end subroutine show definition
222             ########################################################################
223              
224             =head2 Draw
225              
226             Draws geometry on the Tk canvas $cnv. List of items to draw must be
227             specified via addresses stored in $options{items}.
228              
229             The newest fad (:e) is the $options{tag} argument, which uses
230             addr_to_tktag() to tag the item.
231              
232             $drw->Draw($cnv, %options);
233              
234             =cut
235             sub Draw {
236             my $self = shift;
237             my $cnv = shift;
238             my %options = @_;
239             my @list = @{$options{items}};
240             foreach my $item (@list) {
241             my $type = $item->{type};
242             # print "item: $type\n";
243             if($dsp{$type}) {
244             my @tk_ids = $dsp{$type}->($self, $cnv, $item);
245             if($options{tag}) {
246             foreach my $tk_id (@tk_ids) {
247             my $tagstring = $self->addr_to_tktag($item);
248             $cnv->itemconfigure($tk_id, -tags => $tagstring);
249             }
250             }
251             }
252             else {
253             carp "no function for $type\n";
254             }
255             }
256            
257             } # end subroutine Draw definition
258             ########################################################################
259              
260             =head2 tkbindings
261              
262             Setup the keybindings.
263              
264             $drw->tkbindings($mw, $cnv);
265              
266             =cut
267             sub tkbindings {
268             my $self = shift;
269             my ($mw, $cnv, $stl) = @_;
270             # FIXME: this should be much more robust
271              
272             # maybe a vim-style modal binding? or possibly a command-line based
273             # system.
274             # just bind ":" to switch to the command bindings and to go back
275             # to visual mode (and the end of every command must go to visual mode.)
276              
277             # this one basically means 'focusFollowsMouse', which is evil.
278             # $mw->bind('' => sub{ $cnv->Tk::focus});
279              
280             # $mw->bind('' => sub{$mw->destroy});
281             # $cnv->CanvasBind('' => sub{print "called\n";exit;});
282             $mw->bind('' => sub {$mw->destroy});
283              
284             # XXX move this...
285             # middle-button pan:
286             my @pan_start;
287             my $drag_current;
288             $cnv->CanvasBind(
289             '' => sub {
290             @pan_start = $cnv->eventLocation();
291             # print "starting pan at @pan_start\n";
292             });
293             # have to have this here to prevent spurious panning with double-clicks
294             $cnv->CanvasBind('' => sub {$drag_current = 1});
295             $cnv->CanvasBind(
296             '' => sub {
297             $drag_current || return();
298             my @pan_stop = $cnv->eventLocation();
299             my $scale = $cnv->pixelSize();
300             # print "\tdouble: $isdouble\n";
301             # print "\tdrag: $drag_current\n";
302             # print "scale is $scale\n";
303             # print "stopping pan at @pan_stop\n";
304             my @diff = map({$pan_start[$_] - $pan_stop[$_]} 0,1);
305             # my $panx = abs($diff[0])/$scale;
306             # my $pany = abs($diff[1])/$scale;
307             # print "pixels: ($panx,$pany)\n";
308             # my $dopan = ( $panx > 10) | ( $pany > 10);
309             # $dopan && print "panning by @diff\n";
310             # $dopan && $cnv->panWorld(@diff);
311             $cnv->panWorld(@diff);
312             $drag_current = 0;
313             });
314            
315             # OKAY, so we've got 4 zoom actions and we don't get text or images
316             # for free.
317              
318             # this takes away all of our fun of having sizable texts (hmm. I
319             # guess we could create this font from anywhere?)
320            
321             # XXX this is going to have some odd behaviour for now, but it isn't
322             # worth trying to make a word-processor widget behave like scalable
323             # text.
324             $textsize = $text_base;
325             $cnv->fontCreate(
326             'cad-drawing-font',
327             -family => 'lucidasans',
328             -size => $textsize,
329             );
330             text_size_reset($cnv);
331             # print "view is @coords\n";
332             # print "other configs:\n",
333             # join("\n", map({join(" ", @$_ )} $cnv->configure())), "\n";
334             # print "width is: ", $cnv->cget(-width), "\n";
335            
336             # mouse-wheel zooming:
337             $cnv->CanvasBind('' => sub{
338             $cnv->zoom(1.125);
339             text_size_reset($cnv);
340             # print "$textsize\n";
341             if(0) {
342             package Tk::WorldCanvas;
343             my $pdata = $cnv->privateData();
344             print "pdata: $pdata\n";
345             foreach my $key (keys(%$pdata)) {
346             print "$key: $pdata->{$key}\n";
347             }
348             print "size is now $pdata->{width} x $pdata->{height}\n";
349             }
350              
351             }
352             );
353             $cnv->CanvasBind('' => sub{
354             $cnv->zoom(1/1.125);
355             text_size_reset($cnv);
356             }
357             );
358             # zoom extents:
359             $cnv->CanvasBind('' => sub{
360             $cnv->viewAll();
361             text_size_reset($cnv);
362             }
363             );
364             # zoom window:
365             $mw->bind(
366             '' => sub {
367             $stl->configure(-text=>"Pick window corners");
368             windowzoom($cnv, $stl);
369             });
370             # measure:
371             $mw->bind(
372             '' => sub {
373             $stl->configure(-text=>"Pick ends");
374             free_dist($cnv, $stl);
375             });
376              
377              
378             } # end subroutine tkbindings definition
379             ########################################################################
380              
381             =head2 text_size_reset
382              
383             text_size_reset($cnv);
384              
385             =cut
386             sub text_size_reset {
387             my $cnv = shift;
388             my @c = $cnv->getView();
389             my $width = $c[2] - $c[0];
390             my $disp = $cnv->cget(-width);
391             # print "showing $width in $disp\n";
392             # print "scale is ", $disp / $width, "\n";
393             $textsize = $text_base * $disp / $width;
394             # print "textsize is $textsize\n";
395             # XXX this is really getting to be a pain (too-large text causes slow-down)
396             ($textsize > 100) && ($textsize = 100);
397             if($textsize >= 2) {
398             ## print "textsize: $textsize\n";
399             $cnv->fontConfigure('cad-drawing-font', -size => $textsize);
400             }
401             else {
402             $cnv->fontConfigure('cad-drawing-font', -size => 2);
403             }
404              
405              
406             } # end subroutine text_size_reset definition
407             ########################################################################
408              
409             =head2 free_dist
410              
411             free_dist();
412              
413             =cut
414             sub free_dist {
415             my $cnv = shift;
416             my $stl = shift;
417             # this is crappy
418             $cnv->CanvasBind(
419             '' => sub {
420             $cnv->rubberBand(0);
421             });
422             $cnv->CanvasBind(
423             '' => sub {
424             $cnv->rubberBand(1);
425             });
426             $cnv->CanvasBind(
427             '' => sub {
428             my @box = $cnv->rubberBand(2);
429             # print "box is @box\n";
430             my $dist = dist2d([@box[0,1]],[@box[2,3]]);
431             my $dx = $box[2] - $box[0];
432             my $dy = $box[1] - $box[3];
433             foreach my $item qw(
434            
435            
436            
437             ) {
438             # print "item: $item\n";
439             $cnv->CanvasBind($item => "");
440             }
441             $stl->configure(-text=>"$dist ($dx,$dy)");
442             warn("measure: $dist ($dx,$dy)\n");
443             });
444             } # end subroutine free_dist definition
445             ########################################################################
446              
447             =head2 windowzoom
448              
449             Creates temporary bindings to drawing a rubber-band box.
450              
451             windowzoom($cnv);
452              
453             =cut
454             sub windowzoom {
455             my $cnv = shift;
456             my $stl = shift;
457             $cnv->CanvasBind(
458             '' => sub {
459             $cnv->rubberBand(0);
460             });
461             $cnv->CanvasBind(
462             '' => sub {
463             $cnv->rubberBand(1);
464             });
465             $cnv->CanvasBind(
466             '' => sub {
467             my @box = $cnv->rubberBand(2);
468             #print "box is @box\n";
469             $cnv->viewArea(@box);
470             text_size_reset($cnv);
471             foreach my $item qw(
472            
473            
474            
475             ) {
476             # print "item: $item\n";
477             $cnv->CanvasBind($item => "");
478             }
479             $stl->configure(-text=>"");
480             });
481             } # end subroutine windowzoom definition
482             ########################################################################
483              
484              
485             =head2 tksetview
486              
487             No longer used
488              
489             $drw->tksetview($cnv, %options);
490              
491             =cut
492             sub tksetview {
493             my $self = shift;
494             my $cnv = shift;
495             my %options = @_;
496             my $width = $options{size}[0];
497             my $height = $options{size}[1];
498             my @ext = $self->OrthExtents($options{items});
499             print "got extents: ",
500             join(" by ", map({join(" to ", @$_)} @ext)), "\n";
501             my @cent = map({($_->[0] + $_->[1]) / 2} @ext);
502             $options{center} && (@cent = @{$options{center}});
503             print "center is @cent\n";
504             my $scale = $options{scale};
505             unless($scale) {
506             $scale = $self->scalebox($options{size}, \@ext);
507             # print "got scale: $scale\n";
508             }
509             $cnv->scale('all'=> 0,0 , $scale, $scale);
510             my $bbox = $options{bbox};
511             $_ *= $scale for @$bbox;
512             # print "bbox now: @$bbox\n";
513             $cnv->configure(-scrollregion=> $bbox);
514             # my $xv = $ext[0][0] * $scale / $bbox->[2];
515             my $xv = ($ext[0][0] * $scale - $bbox->[0]) /
516             ($bbox->[2] - $bbox->[0]);
517             ## my $xv = ($width / 2 - $bbox->[0]) /
518             ## ($bbox->[2] - $bbox->[0]);
519              
520             print "xview: $xv\n";
521             $cnv->xviewMoveto($xv);
522             my (undef(), $yv) = tkpoint([0,$ext[1][0]]);
523             print "ypt: $yv\n";
524             print "ext top: $ext[1][1] bottom: $ext[1][0]\n";
525             print "bbox (t&b): $bbox->[1] $bbox->[3]\n";
526             $yv = (-$ext[1][0] * $scale + $bbox->[3] - $height / 2) /
527             ($bbox->[3] - $bbox->[1]);
528             print "yview: $yv\n";
529             $cnv->yviewMoveto($yv);
530             } # end subroutine tksetview definition
531             ########################################################################
532              
533             =head2 scalebox
534              
535             Returns the scaling required to create a view which most closely
536             matches @ext to @size of canvas.
537              
538             $scale = $drw->scalebox(\@size, \@ext);
539              
540             =cut
541             sub scalebox {
542             my $self = shift;
543             my ($size, $ext) = @_;
544             my ($ew, $eh) = map({abs($_->[0] - $_->[1])} @$ext);
545             my $dx = $size->[0] / $ew;
546             my $dy = $size->[1] / $eh;
547             # print "factors: $dx $dy\n";
548             my $scale = [$dx => $dy] -> [$dy <= $dx];
549             return($scale);
550             } # end subroutine scalebox definition
551             ########################################################################
552              
553             =head2 dsp subroutine refs
554              
555             each of these should do everything necessary to draw the item on the
556             canvas (but they might like to have a few options available?) and then
557             return a list of the Tk id's of the created items. Caller will then
558             assign identical tags to each id which is returned by each per-entity
559             call.
560              
561             =cut
562              
563             %dsp = (
564             lines => sub {
565             my ($self, $cnv, $addr) = @_;
566             my $arrow = "none";
567             $CAD::Drawing::IO::Tk::arrow && ($arrow = "last");
568             my $obj = $self->getobj($addr);
569             my $line = $cnv->createLine(
570             map({tkpoint($_)}
571             @{$obj->{pts}},
572             ),
573             # '-dash' => "",
574             # '-activedash' => ",",
575             # '-activefill' => "#ff0000",
576             '-fill'=> $aci2hex[$obj->{color}],
577             '-arrow' => $arrow,
578             );
579             # print "line item: $line (ref: ", ref($line), ")\n";
580             # my @list = $cnv->itemconfigure($line);
581             # foreach my $deal (@list) {
582             # print "got deal: @$deal\n";
583             #}
584             return($line);
585             }, # end sub $dsp{lines}
586             plines => sub {
587             my ($self, $cnv, $addr) = @_;
588             my $arrow = "none";
589             $CAD::Drawing::IO::Tk::arrow && ($arrow = "last");
590             my $obj = $self->getobj($addr);
591             my $st = $obj->{closed} ? -1 : 0;
592             my @ids;
593             for(my $i = $st; $i < scalar(@{$obj->{pts}}) -1; $i++) {
594             my @pts = map({tkpoint($_)}
595             $obj->{pts}[$i], $obj->{pts}[$i+1],
596             );
597             # print "adding @pts ($i -> ", $i+1, ")\n";
598             my $pline = $cnv->createLine(
599             @pts,
600             '-fill' => $aci2hex[$obj->{color}],
601             '-arrow' => $arrow,
602             );
603             # print "pline item: $pline\n";
604             push(@ids, $pline);
605             }
606             return(@ids);
607             }, # end sub $dsp{plines}
608             arcs => sub {
609             my ($self, $cnv, $addr) = @_;
610             my $obj = $self->getobj($addr);
611             # print "keys: ", join(" ", keys(%$obj)), "\n";
612             my $rad = $obj->{rad};
613             my @pt = tkpoint($obj->{pt});
614             # stupid graphics packages:
615             my @rec = (
616             map({$_ - $rad} @pt),
617             map({$_ + $rad} @pt),
618             );
619             my @angs = @{$obj->{angs}};
620             # stupid graphics packages:
621             @angs = map({$_ * 180 / $pi} @angs);
622             $angs[1] = $angs[1] - $angs[0];
623             $angs[1] += 360;
624             while($angs[1] > 360) {
625             $angs[1] -= 360;
626             }
627             my $arc = $cnv->createArc(
628             @rec,
629             '-start' => $angs[0],
630             '-extent' => $angs[1],
631             '-outline' => $aci2hex[$obj->{color}],
632             '-style' => "arc",
633             );
634             return($arc);
635             }, # end sub $dsp{arcs}
636             circles => sub {
637             my ($self, $cnv, $addr) = @_;
638             my $obj = $self->getobj($addr);
639             my $rad = $obj->{rad};
640             my @pt = tkpoint($obj->{pt});
641             # stupid graphics packages:
642             my @rec = (
643             map({$_ - $rad} @pt),
644             map({$_ + $rad} @pt),
645             );
646             my $circ = $cnv->createOval(
647             @rec,
648             '-outline' => $aci2hex[$obj->{color}],
649             );
650             return($circ);
651             }, # end sub $dsp{circles}
652             texts => sub {
653             my ($self, $cnv, $addr) = @_;
654             my $obj = $self->getobj($addr);
655             my @pt = tkpoint($obj->{pt});
656             my $height = $obj->{height};
657             my $string = $obj->{string};
658             my @text;
659             # FIXME: if tk doesn't get its act together, this becomes kludge:
660             if($obj->{render}) {
661             die "this is broken";
662             }
663             else {
664             @text = $cnv->createText(
665             @pt,
666             -font => "cad-drawing-font", #
667             -anchor => "sw",
668             -text => $string,
669             -fill => $aci2hex[$obj->{color}],
670             );
671             }
672             return(@text);
673             }, # end sub $dsp{texts}
674            
675             ); # end %dsp coderef hash
676             ########################################################################
677              
678             =head2 tkpoint
679              
680             Returns only the first and second element of an array reference as a
681             list.
682              
683             @xy_point = tkpoint(\@pt);
684              
685             =cut
686             sub tkpoint {
687             return($_[0]->[0], $_[0]->[1]);
688             } # end subroutine tkpoint definition
689             ########################################################################
690              
691             =head2 addr_to_tktag
692              
693             Returns a stringified tag of form: ######
694              
695             my $tag = $drw->addr_to_tktag($addr);
696              
697             =cut
698             sub addr_to_tktag {
699             my $self = shift;
700             my $addr = shift;
701             return(join("###", $addr->{layer}, $addr->{type}, $addr->{id}));
702             } # end subroutine addr_to_tktag definition
703             ########################################################################
704              
705             =head2 tktag_to_addr
706              
707             Returns an anonymous hash reference which should serve as an address,
708             provided that $tag is a valid ###### tag (and that the
709             entity exists in the $drw object (check this yourself.)
710              
711             my $addr = $drw->tktag_to_addr($tag);
712              
713             =cut
714             sub tktag_to_addr {
715             my $self = shift;
716             my $tag = shift;
717             my @these = split(/###/, $tag);
718             (@these == 3) or croak("parsing tag failed! ($tag)\n");
719             my @order = qw(layer type id);
720             return({map({$order[$_] => $these[$_]} 0..2)});
721             } # end subroutine tktag_to_addr definition
722             ########################################################################
723              
724             1;