File Coverage

blib/lib/Language/Logo.pm
Criterion Covered Total %
statement 67 648 10.3
branch 13 276 4.7
condition 12 149 8.0
subroutine 11 61 18.0
pod n/a
total 103 1134 9.0


line stmt bran cond sub pod time code
1             #
2             # Language::Logo.pm
3             #
4             # An implementation of the Logo programming language which allows
5             # multiple clients to connect simultaneously.
6             #
7             # Written January 2007, by John C. Norton
8             # Presented at Boston Perlmongers on January 16th, 2007
9             # Last update -- 1/30/2007 22:12
10             #
11              
12              
13             # Package header
14             package Logo;
15             our $VERSION = '1.000'; # Current version
16              
17              
18             # Strict
19 2     2   62666 use strict;
  2         6  
  2         84  
20 2     2   12 use warnings;
  2         2  
  2         58  
21              
22              
23             # Libraries
24 2     2   365756 use Data::Dumper;
  2         34098  
  2         162  
25 2     2   3756 use IO::Select;
  2         4768  
  2         118  
26 2     2   1786 use IO::Socket;
  2         76080  
  2         12  
27 2     2   3482 use Sys::Hostname;
  2         4066  
  2         190  
28              
29              
30             #################
31             ### Variables ###
32             #################
33 2     2   16 use constant PI => (4 * atan2(1, 1));
  2         2  
  2         21082  
34              
35             # User-defined
36             my $iam = "Language::Logo"; # Module identifier
37             my $d_title = "$iam version $VERSION";
38              
39             my $max_connect = 16; # Maximum client connections
40             my $retry_timeout = 10; # Client connection timeout after N seconds
41              
42             # Defaults
43             my $d_port = "8220"; # Default socket port
44             my $d_update = 10; # Default gui update rate
45             my $d_bg = "black"; # Default canvas background color
46             my $d_width = 512; # Default canvas width
47             my $d_height = 512; # Default canvas height
48             my $d_color = 'white'; # Default pen/turtle color
49             my $d_psize = '1'; # default pen size (thickness)
50             my $d_txdim = '6'; # Default turtle x-dimension
51             my $d_tydim = '9'; # Default turtle y-dimension
52              
53             my @switches = qw( verbose name title bg width height update host port );
54             my %switches = map { $_ => 1 } @switches;
55              
56             # Global (server-specific) variables
57             my $pserver_vars = [qw( nticks verbose count total )];
58              
59             # Client-specific top-level variables (with initial values)
60             my $pclient_vars = {
61             'debug' => 0,
62             'step' => 0,
63             };
64              
65             # Turtle state info passed back from server to client
66             my $pinfo = [qw( x y angle pen color size show wrap )];
67              
68             # Command aliases and descriptions
69             my $palias = {
70             'fd' => [ 'forward', 'Moves forward the given number of pixels' ],
71             'bk' => [ 'backward', 'Moves backward the given number of pixels' ],
72             'rt' => [ 'right', 'Rotates clockwise the given angle' ],
73             'lt' => [ 'left', 'Rotates counter-clockwise the given angle' ],
74             'sh' => [ 'seth', 'Sets the turtle heading to the given angle' ],
75             'pu' => [ 'penup', 'Stops drawing' ],
76             'pd' => [ 'pendown', 'Starts drawing' ],
77             'ps' => [ 'pensize', 'Specifies the line width to draw with' ],
78             'co' => [ 'color', 'Specifies the color to draw with' ],
79             'cs' => [ 'clear', 'Clears the screen' ],
80             'hm' => [ 'home', 'Homes the turtle to the starting position' ],
81             'sx' => [ 'setx', 'Sets the x-coordinate' ],
82             'sy' => [ 'sety', 'Sets the y-coordinate' ],
83             'xy' => [ 'setxy', 'Sets the x and y coordinates' ],
84             'ht' => [ 'hideturtle', 'Makes the turtle invisible' ],
85             'st' => [ 'showturtle', 'Makes the turtle visible' ],
86             'w' => [ 'width', 'Specifies the width of the screen (global)' ],
87             'h' => [ 'height', 'Specifies the height of the screen (global)' ],
88             'bg' => [ 'background', 'Sets the screen background color (global)' ],
89             'ud' => [ 'update', 'Changes the Tk update interval (global)' ],
90             'wr' => [ 'wrap', 'Sets wrap (0=normal, 1=torus, 2=reflective)' ],
91             };
92              
93              
94             my $pmethods = {
95             'forward' => 'move_turtle',
96             'backward' => 'move_turtle',
97             'right' => 'turn_turtle',
98             'left' => 'turn_turtle',
99             'seth' => 'turn_turtle',
100             'penup' => 'change_pen_state',
101             'pendown' => 'change_pen_state',
102             'pensize' => 'change_pen_size',
103             'color' => 'change_color',
104             'clear' => 'modify_canvas',
105             'width' => 'modify_canvas',
106             'height' => 'modify_canvas',
107             'background' => 'modify_canvas',
108             'home' => 'reset_turtle',
109             'setx' => 'move_turtle',
110             'sety' => 'move_turtle',
111             'setxy' => 'move_turtle',
112             'hideturtle' => 'show_turtle',
113             'showturtle' => 'show_turtle',
114             'update' => 'change_update',
115             'wrap' => 'set_wrap_value',
116             };
117              
118              
119             ###################
120             ### Subroutines ###
121             ###################
122              
123             #===================
124             #=== Client code ===
125             #===================
126             sub new {
127 2     2   64 my ($class, @args) = @_;
128 2 50       10 (ref $class) and $class = ref $class;
129              
130             # Create blessed reference
131 2         4 my $self = { };
132 2         8 bless $self, $class;
133              
134             # Parse optional arguments
135 2         8 while (@args) {
136 8         12 my $arg = shift @args;
137 8 50 33     62 if ($arg =~ /^sig(.+)$/) {
    50          
138             # Trap specified signals
139 0         0 my $sig = uc $1;
140 0         0 $SIG{$sig} = shift @args;
141             } elsif (defined($switches{$arg}) and @args > 0) {
142             # Assign all valid parameters
143 8         44 $self->{$arg} = shift @args;
144             }
145             }
146              
147             # Startup a new server locally if 'host' was not defined.
148 2 50       14 if (!defined($self->{'host'})) {
149 2         12 $self->fork_server();
150             }
151              
152             # Connect to the server
153 1         70 $self->connect_to_server();
154              
155             # Return the object
156 0         0 return $self;
157             }
158              
159              
160             sub disconnect {
161 0     0   0 my ($self, $msg) = @_;
162 0 0 0     0 if ($msg || 0) {
163 0         0 print "$msg";
164 0         0 ;
165             }
166 0         0 my $sock = $self->{'socket'};
167 0 0 0     0 if ($sock || 0) {
168 0         0 close($sock);
169             }
170             }
171              
172              
173             sub connect_to_server {
174 1     1   21 my ($self) = @_;
175              
176             # Return if socket is already connected
177 1         10 my $sock = $self->{'socket'};
178 1 50 50     60 ($sock || 0) and return $sock;
179              
180             # If hostname is ':', use local host
181 1   50     149 my $host = $self->{'host'} || ':';
182 1 50       43 ($host eq ':') and $host = hostname();
183              
184 1   33     30 my $port = $self->{'port'} || $d_port;
185 1         34 my %params = (
186             'PeerAddr' => $host,
187             'PeerPort' => $port,
188             'Proto' => 'tcp',
189             'ReuseAddr' => 0,
190             );
191              
192             # Keep retrying until $retry_timeout is exceeded
193 1         6 my $start = time;
194 1         8 while (1) {
195 60 50       2181 ($sock = new IO::Socket::INET(%params)) and last; # Success!
196 60 100       8976940 if (time - $start > $retry_timeout) {
197 1         1626 die "$iam: Failed client socket connection\n";
198             }
199 59         5922337 select(undef, undef, undef, 0.1);
200             }
201              
202             # Save socket
203 0         0 $self->{'socket'} = $sock;
204 0   0     0 my $name = $self->{'name'} || "";
205 0         0 print $sock ":$name\n";
206 0         0 chomp(my $ans = <$sock>);
207 0 0       0 if ($ans !~ /^(\d+):(.+)$/) {
208 0         0 die "$iam: expected 'id:name', got '$ans'\n";
209             }
210 0         0 my ($id, $newname) = ($1, $2);
211 0         0 $self->{'id'} = $id;
212 0         0 $self->{'name'} = $newname;
213 0         0 $self->{'host'} = $host;
214 0         0 return $sock;
215             }
216              
217              
218             sub host {
219 0     0   0 my ($self) = @_;
220 0         0 return $self->{'host'};
221             }
222              
223              
224             sub interact {
225 0     0   0 my ($self) = @_;
226              
227 0         0 print "Type '?' for help\n";
228 0         0 while (1) {
229 0         0 print "$iam> ";
230 0         0 my $cmd = ;
231 0 0       0 defined($cmd) or return;
232 0         0 chomp $cmd;
233 0         0 $cmd =~ s/^\s*(.*)\s*$/$1/; # Trim whitespace
234 0 0       0 next if ($cmd eq "");
235              
236 0 0 0     0 if ($cmd eq 'quit' or $cmd eq 'bye' or $cmd eq 'exit') {
      0        
237             # Exit interactive mode
238 0         0 return 0;
239             }
240              
241 0 0       0 if ($cmd eq "?") {
242 0         0 $self->interactive_help();
243             } else {
244 0         0 $self->interactive_command($cmd);
245             }
246             }
247             }
248              
249              
250             sub interactive_help {
251 0     0   0 printf " Command Abbr Description\n";
252 0         0 print "-" x 79, "\n";
253 0         0 my @keys = keys %$palias;
254 0         0 my @sort = sort { $palias->{$a}->[0] cmp $palias->{$b}->[0] } @keys;
  0         0  
255 0         0 foreach my $alias (@sort) {
256 0         0 my $pcmd = $palias->{$alias};
257 0         0 my ($full, $desc) = @$pcmd;
258 0         0 printf " %10.10s %3.3s %s\n", $full, $alias, $desc;
259             }
260             }
261              
262              
263             sub interactive_command {
264 0     0   0 my ($self, $cmd) = @_;
265              
266             # Send a Logo command
267 0         0 my $preply = $self->command($cmd);
268 0         0 my $err = $preply->{'error'};
269 0 0       0 if (defined($err)) {
270 0         0 print "ERROR: $err\n";
271             } else {
272 0         0 my $text = "";
273 0         0 foreach my $param (@$pinfo) {
274 0         0 my $val = $preply->{$param};
275 0 0       0 $text and $text .= ",";
276 0         0 $text .= "$param=$val";
277             }
278 0         0 print "[$text]\n";
279             }
280             }
281              
282              
283             sub query {
284 0     0   0 my ($self, @params) = @_;
285 0         0 my $sock = $self->connect_to_server();
286 0         0 my $preply = $self->client_send($sock, "?");
287 0 0       0 defined($preply->{'error'}) and return $preply;
288 0         0 my @values = ( );
289 0         0 foreach my $param (@params) {
290 0 0       0 if (!defined($preply->{$param})) {
291 0         0 $preply->{'error'} = "Server parameter '$param' undefined";
292 0         0 return $preply;
293             }
294 0         0 my $value = $preply->{$param};
295 0         0 push @values, $value;
296             }
297 0 0       0 return wantarray? (@values): $values[0];
298             }
299              
300              
301             sub command {
302 0     0   0 my ($self, $cmdstr) = @_;
303 0         0 my $sock = $self->connect_to_server();
304 0 0       0 $sock or return 0;
305 0         0 my @commands = split(';', $cmdstr);
306 0         0 my $preply = { };
307 0         0 foreach my $cmd (@commands) {
308 0         0 $cmd =~ s/^\s*//; # Trim leading whitespace
309 0         0 $cmd =~ s/\s*$//; # Trim trailing whitespace
310 0         0 $preply = $self->client_send($sock, "=$cmd");
311 0 0       0 defined($preply->{'error'}) and return $preply;
312             }
313 0         0 return $preply;
314             }
315              
316              
317             sub cmd {
318 0     0   0 my $self = shift;
319 0         0 return $self->command(@_);
320             }
321              
322              
323             sub client_send {
324 0     0   0 my ($self, $sock, $text) = @_;
325 0         0 print $sock $text, "\n";
326 0         0 my $answer = <$sock>;
327 0 0       0 $answer or die "$iam: server socket went away\n";
328 0         0 chomp $answer;
329 0         0 my $preply = { };
330 0 0       0 if ($answer =~ s/^!//) {
331 0         0 $preply->{'error'} = $answer;
332 0         0 return $preply;
333             }
334 0         0 $answer =~ s/^(.)//;
335 0         0 my @params = split(',', $answer);
336 0         0 foreach my $param (@params) {
337 0         0 my ($param, $val) = ($param =~ /^(.*)=(.*)$/);
338 0         0 $preply->{$param} = $val;
339             }
340 0         0 return $preply;
341             }
342              
343              
344             #===================
345             #=== Server code ===
346             #===================
347             sub fork_server {
348 2     2   6 my ($self) = @_;
349              
350 2   50     28 my $verbose = $self->{'verbose'} || 0;
351 2   33     18 my $title = $self->{'title'} || $d_title;
352 2   33     12 my $w = $self->{'width'} || $d_width;
353 2   33     8 my $h = $self->{'height'} || $d_height;
354 2   33     16 my $bg = $self->{'bg'} || $d_bg;
355 2   33     10 my $update = $self->{'update'} || $d_update;
356 2   33     20 my $host = $self->{'host'} || hostname();
357 2   33     44 my $port = $self->{'port'} || $d_port;
358            
359 2         5365 my $fork = fork();
360 2 50       146 defined($fork) or die "$iam: failed to fork server\n";
361 2 100       160 $fork and return;
362 1         60 Logo->server_init($verbose, $title, $w, $h, $bg, $update, $host, $port);
363             }
364              
365              
366             sub server_init {
367 1     1   26 my ($class, $verbose, $title, $w, $h, $bg, $update, $host, $port) = @_;
368              
369             # Create a blessed object
370 1         94 my $self = {
371             'nticks' => 0, # Tracks number of GUI updates
372             'verbose' => $verbose, # Verbose flag
373             'count' => 0, # Current number of connections
374             'total' => 0, # Total number of connections
375             'clients' => { }, # The client hash
376             'names' => { }, # The clients by name
377             };
378 1         36 bless $self, $class;
379              
380             # Open a socket connection at the desired port
381 1         128 my %params = (
382             'LocalHost' => $host,
383             'LocalPort' => $port,
384             'Proto' => 'tcp',
385             'Listen' => $max_connect,
386             'ReuseAddr' => 0,
387             );
388              
389             # Create socket object
390 1         52 my $sock = new IO::Socket::INET(%params);
391 1 50       1600 if (!$sock) {
392             # Port is already in use -- client will connect to it instead
393 0 0       0 $verbose and print "[Port $port already in use]\n";
394 0         0 exit;
395             }
396 1         5 $self->{'socket'} = $sock;
397              
398             # Create select set for reading
399 1         57 $self->{'select'} = new IO::Select($sock);
400              
401             # Create the GUI
402 1         1337 require Tk;
403 0 0         $verbose and print "[Logo server v$VERSION on '$host']\n";
404 0           my $mw = Tk::MainWindow->new(-title => $title);
405 0           $self->{'mw'} = $mw;
406              
407             # Allow easy dismissal of the GUI
408 0     0     $mw->bind("" => sub { $self->server_exit });
  0            
409              
410             # Create a new canvas
411 0           $self->clear_screen($w, $h, $bg);
412              
413             # Manage the GUI
414 0           $self->{'repid'} = $self->set_update($update);
415 0           Tk::MainLoop();
416             }
417              
418              
419             sub server_exit {
420 0     0     my ($self) = @_;
421 0           my $mw = $self->{'mw'};
422              
423 0           my $sel = $self->{'select'};
424 0           my $sock = $self->{'socket'};
425 0           my $pclients = $self->{'clients'};
426 0           my $pnames = $self->{'names'};
427              
428 0           close $sock;
429              
430 0           foreach my $name (keys %$pnames) {
431 0           my $pclient = $pnames->{$name};
432 0           my $fh = $pclient->{'fh'};
433 0           $self->server_remove_client($pclients, $sel, $fh);
434             }
435              
436             # Shouldn't ever get here, since when the last client exited,
437             # the server should have already gone away. But just in case ...
438             #
439 0           $mw->destroy();
440 0           exit;
441             }
442              
443              
444             sub set_update {
445 0     0     my ($self, $update) = @_;
446 0 0         ($update < 1) and $update = 1;
447 0 0         ($update > 1000) and $update = 1000;
448 0           $self->{'update'} = $update;
449 0           my $mw = $self->{'mw'};
450 0     0     my $id = $mw->repeat($update => sub { $self->server_loop() });
  0            
451 0           return $id;
452             }
453              
454              
455             sub server_loop {
456 0     0     my ($self) = @_;
457              
458             # Increment tick count
459 0           ++$self->{'nticks'};
460              
461             # Get data from the object
462 0           my $sel = $self->{'select'};
463 0           my $sock = $self->{'socket'};
464 0           my $pclients = $self->{'clients'};
465              
466             # Handle each pending socket
467 0           my @readable = $sel->can_read(0);
468 0           foreach my $rh (@readable) {
469 0 0         if ($rh == $sock) {
470             # The main socket means a new incoming connection.
471 0           $self->server_add_client($rh, $pclients);
472             } else {
473             # Service the socket
474 0           my $text = <$rh>;
475              
476 0 0         if (defined($text)) {
477             # Process command
478 0           chomp $text;
479 0           my $pc = $pclients->{$rh};
480 0 0         if ($text eq '?') {
    0          
481 0           $self->server_query($pc);
482             } elsif ($text =~ s/^=//) {
483 0           $self->server_command($pc, $text);
484             }
485             } else {
486             # Socket was closed -- remove the client
487 0           $self->server_remove_client($pclients, $sel, $rh);
488             }
489             }
490             }
491             }
492              
493              
494             sub server_add_client {
495 0     0     my ($self, $rh, $pclients) = @_;
496              
497             # Accept the client connect and add the new socket
498 0           my $sel = $self->{'select'};
499 0           my $ns = $rh->accept();
500 0           $sel->add($ns);
501              
502 0           my $verbose = $self->{'verbose'};
503 0           my $peer = getpeername($ns);
504 0           my ($port, $iaddr) = unpack_sockaddr_in($peer);
505 0           my $remote = inet_ntoa($iaddr);
506              
507             # Get the client handshake, and send back its unique ID
508 0           chomp(my $text = <$ns>);
509 0 0         ($text =~ /^:(.*)$/) or die "Bad header, expected ':[name]', got '$text'";
510 0   0       my $name = $1 || "";
511              
512 0           my $id = $self->{'total'} + 1;
513 0   0       $name ||= "CLIENT$id";
514 0           print $ns "$id:$name\n";
515              
516 0           my $pc = $pclients->{$ns} = {
517             'id' => $id,
518             'fh' => $ns,
519             'name' => $name,
520             'remote' => $remote,
521             };
522              
523             # Assign defaults to client-specific variables
524 0           map { $pc->{$_} = $pclient_vars->{$_} } (keys %$pclient_vars);
  0            
525              
526             # Create the 'turtle' object
527 0           $self->create_turtle($pc);
528              
529             # Increment the number of connections and the total connection count
530 0           ++$self->{'count'};
531 0           ++$self->{'total'};
532              
533             # Add the client's name
534 0 0         $verbose and print "[Added socket $id => '$name']\n";
535 0           $self->{'names'}->{$name} = $pclients->{$ns};
536             }
537              
538              
539             sub server_remove_client {
540 0     0     my ($self, $pclients, $sel, $fh) = @_;;
541 0           my $verbose = $self->{'verbose'};
542 0           my $pc = $pclients->{$fh};
543 0           my $name = $pc->{'name'};
544 0           my $id = $pc->{'id'};
545 0           $sel->remove($fh);
546 0           close($fh);
547 0           delete $pclients->{$fh};
548              
549             # Remove the client's name
550 0           my $pnames = $self->{'names'};
551 0           delete $pnames->{$name};
552              
553             # Remove the client's turtle
554 0           my $cv = $self->{'canvas'};
555 0           my $ptids = $pc->{'turtle'}->{'tids'};
556 0 0 0       ($ptids || 0) and map { $cv->delete($_) } @$ptids;
  0            
557              
558             # Decrement the global client count
559 0           --$self->{'count'};
560 0 0         $verbose and print "[Closed socket $id '$name']\n";
561              
562             # Exit the server if this is the last connection
563 0 0 0       if (0 == $self->{'count'} and $self->{'total'} > 0) {
564 0 0         $verbose and print "[Final client closed -- exiting]\n";
565 0           $self->{'mw'}->destroy();
566 0           exit;
567             }
568             }
569              
570              
571             sub server_query {
572 0     0     my ($self, $pc) = @_;
573 0           my $text = "";
574 0           foreach my $param (@$pserver_vars) {
575 0 0         $text and $text .= ",";
576 0           my $val = $self->{$param};
577 0           $text .= "$param=$val";
578             }
579 0           my $fh = $pc->{'fh'};
580 0           printf $fh "?$text\n";
581             }
582              
583              
584             sub server_command {
585 0     0     my ($self, $pc, $cmdstr) = @_;
586 0           my $id = $pc->{'id'};
587 0           $pc->{'lastcmd'} = $cmdstr;
588              
589 0           my $debug = $pc->{'debug'};
590 0 0         $debug and print "Command<$id>: '$cmdstr'\n";
591              
592 0           my @args = split(/\s+/, $cmdstr);
593 0           my $cmd = shift @args;
594              
595             # Allow "noop" command to just query current client parameters
596 0 0         if ($cmdstr eq 'noop') {
597 0           return $self->server_reply($pc);
598             }
599              
600             # Resolve any command alias
601 0           while (defined($palias->{$cmd})) {
602 0           my $pcmd = $palias->{$cmd};
603 0           my $newcmd = $pcmd->[0];
604 0           $cmd = $newcmd;
605             }
606 0           unshift @args, $cmd;
607              
608             # Execute one command if single-stepping is on
609 0 0         if ($pc->{'step'}) {
610 0           my $go = $self->server_single_step($pc, $cmd, [ @args ]);
611 0 0         $go or return $self->server_reply($pc);
612             }
613              
614             # Client variables
615 0 0         if (defined($pclient_vars->{$cmd})) {
616 0           return $self->server_set_variable($pc, @args);
617             }
618              
619             # Find command in dispatch table
620 0           my $method = $pmethods->{$cmd};
621 0 0         defined($method) and return $self->$method($pc, @args);
622              
623             # Return acknowledgment
624 0           $self->server_error($pc, "Unknown command '$cmd'");
625             }
626              
627              
628             sub server_set_variable {
629 0     0     my ($self, $pc, $param, $val) = @_;
630 0   0       $pc->{$param} = $val || 0;
631 0 0         $pc->{'debug'} and print "Variable '$param' set to '$val'\n";
632 0           $self->server_reply($pc);
633             }
634              
635              
636             sub server_single_step {
637 0     0     my ($self, $pc, $cmd, $pargs) = @_;
638 0           my $cmdstr = join(" ", @$pargs);
639 0           print "Step> [$cmdstr] Execute {y|n|c}? [y]";
640 0           chomp(my $ans = );
641 0 0         ($ans =~ /^[cC]/) and $pc->{'step'} = 0;
642 0 0         return ($ans =~ /^[nN]/)? 0: 1;
643             }
644              
645              
646             sub server_reply {
647 0     0     my ($self, $pc) = @_;
648 0           my $fh = $pc->{'fh'};
649 0           my $turtle = $pc->{'turtle'};
650 0           my $text = "";
651 0           foreach my $param (@$pinfo) {
652 0           my $val = $turtle->{$param};
653 0 0         $text and $text .= ",";
654 0           $text .= "$param=$val";
655             }
656 0           printf $fh "=$text\n";
657             }
658              
659              
660             sub server_error {
661 0     0     my ($self, $pc, $msg) = @_;
662 0           my $fh = $pc->{'fh'};
663 0   0       $msg ||= "";
664 0           print $fh "!$msg\n";
665             }
666              
667              
668             sub create_turtle {
669 0     0     my ($self, $pc, $from) = @_;
670              
671 0           my $turtle = {
672             'pen' => 0, # Pen state: 0 = 'up', 1 = 'down'
673             'color' => $d_color, # Pen color (also turtle color)
674             'size' => $d_psize, # Pen size (thickness)
675             'xdim' => $d_txdim, # Turtle x-dimension
676             'ydim' => $d_tydim, # Turtle y-dimension
677             'dist' => 0, # Last distance traveled (used as default)
678             'show' => 1, # Turtle starts out visible
679             'wrap' => 0, # Normal wrap (= no wrap)
680             };
681              
682             # Use old turtle as a reference
683 0 0 0       if ($from || 0) {
684 0           map { $turtle->{$_} = $from->{$_} } (keys %$from);
  0            
685             }
686              
687 0           $self->home_turtle($pc, $turtle);
688 0           $self->draw_turtle($pc, $turtle);
689             }
690              
691              
692             sub home_turtle {
693 0     0     my ($self, $pc, $turtle) = @_;
694 0           my $cv = $self->{'canvas'};
695 0           my $width = $cv->cget(-width);
696 0           my $height = $cv->cget(-height);
697              
698 0           my $x = int($width / 2);
699 0           my $y = int($height / 2);
700              
701 0           $turtle->{'x'} = $x;
702 0           $turtle->{'y'} = $y;
703 0           $turtle->{'angle'} = 0;
704             }
705              
706              
707             sub reset_turtle {
708 0     0     my ($self, $pc, $cmd) = @_;
709 0           my $turtle = $pc->{'turtle'};
710 0           $self->home_turtle($pc, $turtle);
711 0           $self->draw_turtle($pc, $turtle);
712 0           $self->server_reply($pc);
713             }
714              
715              
716             sub draw_turtle {
717 0     0     my ($self, $pc, $turtle) = @_;
718              
719             # Erase old turtle if one exists
720 0           my $cv = $self->{'canvas'};
721 0           my $ptids = $pc->{'turtle'}->{'tids'};
722 0 0 0       if ($ptids || 0) {
723 0           map { $cv->delete($_) } @$ptids;
  0            
724 0           $pc->{'turtle'}->{'tids'} = 0;
725             }
726              
727             # Create turtle parameters
728 0           my $cvbg = $cv->cget(-bg);
729 0           my $x = $turtle->{'x'};
730 0           my $y = $turtle->{'y'};
731 0           my $angle = $turtle->{'angle'};
732 0           my $color = $turtle->{'color'};
733 0           my $show = $turtle->{'show'};
734 0           my $xdim = $turtle->{'xdim'};
735 0           my $ydim = $turtle->{'ydim'};
736              
737 0 0         if ($turtle->{'show'}) {
738             # Assign points, rotate them, and plot the turtle
739 0           my $ppts = [ $x, $y, $x-$xdim, $y, $x, $y-2*$ydim, $x+$xdim, $y ];
740 0           $ppts = $self->rotate($x, $y, $angle, $ppts);
741 0           my @args = (-fill => $cvbg, -outline => $color);
742 0           my $tid = $cv->createPolygon(@$ppts, @args);
743 0           $turtle->{'tids'} = [ $tid ];
744 0           $pc->{'turtle'} = $turtle;
745              
746             # If the pen is down, draw a circle around the current point
747 0           $ppts = [ ];
748 0 0         if ($turtle->{'pen'}) {
749 0           $ppts = [ $x-3, $y-3, $x+3, $y+3 ];
750 0           $tid = $cv->createOval(@$ppts, -outline => $color);
751 0           push @{$turtle->{'tids'}}, $tid;
  0            
752             }
753             }
754              
755             # Save the turtle to this client's data
756 0           $pc->{'turtle'} = $turtle;
757             }
758              
759              
760             sub change_update {
761 0     0     my ($self, $pc, $cmd, $update) = @_;
762 0           my $repid = $self->{'repid'};
763 0 0 0       ($repid || 0) and $repid->cancel();
764 0           $self->{'repid'} = $self->set_update($update);
765 0           $self->server_reply($pc);
766             }
767              
768              
769             sub set_wrap_value {
770 0     0     my ($self, $pc, $cmd, $wrap) = @_;
771 0 0         defined($wrap) or return $self->syntax_error($pc);
772 0           $wrap = int($wrap);
773 0 0 0       if ($wrap < 0 || $wrap > 2) {
774 0           return $self->server_error($pc, "Invalid wrap value '$wrap'");
775             }
776 0           my $turtle = $pc->{'turtle'};
777 0           $turtle->{'wrap'} = $wrap;
778 0           $self->server_reply($pc);
779             }
780              
781              
782             sub modify_canvas {
783 0     0     my ($self, $pc, $cmd, $val) = @_;
784              
785 0           my $cv = $self->{'canvas'};
786 0 0         ($cmd eq 'clear') and $self->clear_screen();
787 0 0 0       ($cmd eq 'width') and eval {$cv->configure('-wi', $val || $d_width)};
  0            
788 0 0 0       ($cmd eq 'height') and eval {$cv->configure('-he', $val || $d_height)};
  0            
789 0 0 0       ($cmd eq 'background') and eval {$cv->configure('-bg', $val || $d_bg)};
  0            
790              
791 0           my $pnames = $self->{'names'};
792 0           foreach my $name (keys %$pnames) {
793 0           my $pclient = $pnames->{$name};
794 0           my $turtle = $pclient->{'turtle'};
795 0 0 0       if ($cmd eq 'w' or $cmd eq 'h') {
    0          
796             # Have to recreate the turtle
797 0           $self->create_turtle($pclient);
798             } elsif ($cmd eq 'bg') {
799             # Have to redraw the turtle
800 0           $self->draw_turtle($pclient, $turtle);
801             }
802             }
803              
804 0           $self->server_reply($pc);
805             }
806              
807              
808             sub clear_screen {
809 0     0     my ($self, $width, $height, $bg) = @_;
810              
811             # Clear any old canvas
812 0           my $oldcv = $self->{'canvas'};
813 0 0 0       if ($oldcv || 0) {
814 0   0       $width ||= $oldcv->cget(-width);
815 0   0       $height ||= $oldcv->cget(-height);
816 0   0       $bg ||= $oldcv->cget(-bg);
817 0           $oldcv->packForget();
818             }
819            
820             # Create a new canvas
821 0   0       $width ||= $d_width;
822 0   0       $height ||= $d_height;
823 0   0       $bg ||= $d_bg;
824 0           my $mw = $self->{'mw'};
825 0           my @opts = (-bg => $bg, -width => $width, -height => $height);
826 0           my $cv = $mw->Canvas(@opts);
827 0           $cv->pack(-expand => 1, -fill => 'both');
828 0           $self->{'canvas'} = $cv;
829              
830             # For each client, draw its turtle
831 0   0       my $pclients = $self->{'clients'} || { };
832 0           foreach my $pc (values %$pclients) {
833 0           my $turtle = $pc->{'turtle'};
834 0           $self->create_turtle($pc, $turtle);
835             }
836             }
837              
838              
839             sub rotate {
840 0     0     my ($self, $x, $y, $angle, $ppoints) = @_;
841 0           for (my $i = 0; $i < @$ppoints; $i += 2) {
842 0           $ppoints->[$i] -= $x;
843 0           $ppoints->[$i+1] -= $y;
844             }
845 0           my $ppolar = $self->rect_to_polar($ppoints);
846 0           for (my $i = 1; $i <= @$ppolar; $i += 2) {
847 0           $ppolar->[$i] = ($ppolar->[$i] + $angle) % 360;
848             }
849 0           $ppoints = $self->polar_to_rect($ppolar);
850 0           for (my $i = 0; $i < @$ppoints; $i += 2) {
851 0           $ppoints->[$i] += $x;
852 0           $ppoints->[$i+1] += $y;
853             }
854 0           return $ppoints;
855             }
856              
857              
858             sub calculate_endpoint {
859 0     0     my ($self, $x, $y, $angle, $dist) = @_;
860 0           my $prect = $self->polar_to_rect([ $dist, $angle ]);
861 0           my ($x1, $y1) = @$prect;
862 0           $x1 += $x;
863 0           $y1 += $y;
864 0           return ($x1, $y1);
865             }
866              
867              
868             sub rect_to_polar {
869 0     0     my ($self, $ppoints) = @_;
870 0           my $ppolar = ( );
871 0           while (@$ppoints > 1) {
872 0           my $x = shift @$ppoints;
873 0           my $y = shift @$ppoints;
874 0           my $r = sqrt($x ** 2 + $y ** 2);
875 0           my $t = $self->rad_to_deg(atan2($y, $x));
876 0           push @$ppolar, $r, $t;
877             }
878 0           return $ppolar;
879             }
880              
881              
882             sub polar_to_rect {
883 0     0     my ($self, $ppoints) = @_;
884 0           my $prect = [ ];
885 0           while (@$ppoints > 1) {
886 0           my $r = shift @$ppoints;
887 0           my $t = $self->deg_to_rad(shift @$ppoints);
888 0           my $x = $r * cos($t);
889 0           my $y = $r * sin($t);
890 0           push @$prect, $x, $y;
891             }
892 0           return $prect;
893             }
894              
895              
896             sub deg_to_rad {
897 0     0     my ($self, $degrees) = @_;
898 0           my $radians = $degrees * PI / 180;
899 0 0         ($radians < 0) and $radians += 6.283185307;
900 0           return $radians;
901             }
902              
903              
904             sub rad_to_deg {
905 0     0     my ($self, $radians) = @_;
906 0           my $degrees = $radians * 180 / PI;
907 0 0         ($degrees < 0) and $degrees += 360;
908 0           return $degrees;
909             }
910              
911              
912             sub show_turtle {
913 0     0     my ($self, $pc, $cmd) = @_;
914 0 0         my $b_show = ($cmd eq 'st')? 1: 0;
915 0           my $turtle = $pc->{'turtle'};
916 0           $turtle->{'show'} = $b_show;
917 0           $self->draw_turtle($pc, $turtle);
918 0           $self->server_reply($pc);
919             }
920              
921              
922             sub change_color {
923 0     0     my ($self, $pc, $cmd, $color) = @_;
924 0 0         defined($color) or return $self->syntax_error($pc);
925              
926             # Allow a random color
927 0 0 0       if (($color || "") eq 'random') {
928 0           $color = sprintf "#%02x%02x%02x", rand 256, rand 256, rand 256;
929             }
930              
931 0           my $turtle = $pc->{'turtle'};
932 0           $turtle->{'color'} = $color;
933 0           $self->draw_turtle($pc, $turtle);
934 0           $self->server_reply($pc);
935             }
936              
937              
938             sub change_pen_state {
939 0     0     my ($self, $pc, $cmd) = @_;
940 0 0         my $state = ($cmd eq 'pendown')? 1: 0;
941 0           my $turtle = $pc->{'turtle'};
942 0           $turtle->{'pen'} = $state;
943 0           $self->draw_turtle($pc, $turtle);
944 0           $self->server_reply($pc);
945             }
946              
947              
948             sub change_pen_size {
949 0     0     my ($self, $pc, $cmd, $size, @args) = @_;
950 0           my $turtle = $pc->{'turtle'};
951              
952             # Allow a random pen size
953 0 0 0       if (($size || "") eq "random") {
954 0           my $min = $args[0];
955 0           my $max = $args[1];
956 0 0         defined($min) or return $self->syntax_error($pc);
957 0 0         defined($max) or return $self->syntax_error($pc);
958 0           $size = $min + rand($max - $min);
959             }
960              
961 0   0       $size ||= $d_psize;
962 0           $turtle->{'size'} = $size;
963 0           $self->server_reply($pc);
964             }
965              
966              
967             sub syntax_error {
968 0     0     my ($self, $pc) = @_;
969 0           my $cmd = $pc->{'lastcmd'};
970 0           $self->server_error($pc, "Syntax error in '$cmd'");
971             }
972              
973              
974             sub turn_turtle {
975 0     0     my ($self, $pc, $cmd, $newang, $arg0, $arg1) = @_;
976              
977 0           my $turtle = $pc->{'turtle'};
978 0           my $angle = $turtle->{'angle'};
979              
980             # Allow a random angle of turn
981 0 0 0       if (($newang || "") eq 'random') {
982 0 0         defined($arg0) or return $self->syntax_error($pc);
983 0 0         defined($arg1) or return $self->syntax_error($pc);
984 0           $newang = $arg0 + rand($arg1 - $arg0);
985             }
986              
987             # Make angles default to right angles
988 0 0         defined($newang) or $newang = 90;
989              
990             # Assign the angle
991 0 0         ($cmd eq 'left') and $angle = $angle - $newang;
992 0 0         ($cmd eq 'right') and $angle = $angle + $newang;
993 0 0         ($cmd eq 'seth') and $angle = $newang;
994              
995             # Normalize the angle
996 0           while ($angle < 0) { $angle += 360 }
  0            
997 0           while ($angle > 360) { $angle -= 360 }
  0            
998              
999 0           $turtle->{'angle'} = $angle;
1000 0           $self->draw_turtle($pc, $turtle);
1001 0           $self->server_reply($pc);
1002             }
1003              
1004              
1005             sub move_turtle {
1006 0     0     my ($self, $pc, $cmd, $dist, $arg0, $arg1) = @_;
1007 0           my $turtle = $pc->{'turtle'};
1008 0           my $angle = $turtle->{'angle'};
1009 0           my $wrap = $turtle->{'wrap'};
1010              
1011             # Allow a random distance
1012 0 0 0       if (($dist || "") eq 'random') {
1013 0 0         defined($arg0) or return $self->syntax_error($pc);
1014 0 0         defined($arg1) or return $self->syntax_error($pc);
1015 0           $dist = $arg0 + rand($arg1 - $arg0);
1016             }
1017              
1018 0   0       $dist ||= $turtle->{'dist'};
1019 0 0         (0 == $dist) and return $self->syntax_error($pc);
1020 0           $turtle->{'dist'} = $dist;
1021 0 0         ($cmd eq 'forward') and $angle = ($angle + 270) % 360;
1022 0 0         ($cmd eq 'backward') and $angle = ($angle + 90) % 360;
1023 0           my ($x0, $y0) = ($turtle->{'x'}, $turtle->{'y'});
1024 0           my ($x1, $y1);
1025 0 0 0       if ($cmd eq 'setx' or $cmd eq 'sety' or $cmd eq 'setxy') {
      0        
1026 0 0         if ($cmd eq 'setxy') {
1027 0 0         defined($dist) or return $self->syntax_error($pc);
1028 0 0         defined($arg0) or return $self->syntax_error($pc);
1029 0           ($x1, $y1) = ($dist, $arg0);
1030             } else {
1031 0 0         defined($dist) or return $self->syntax_error($pc);
1032 0           ($x1, $y1) = ($x0, $y0);
1033 0 0         ($x1, $y1) = ($cmd eq 'setx')? ($dist, $y0): ($x0, $dist);
1034             }
1035             } else {
1036 0           ($x1, $y1) = $self->calculate_endpoint($x0, $y0, $angle, $dist);
1037             }
1038              
1039 0           my @args = ($pc, $x0, $y0, $x1, $y1);
1040 0 0         return $self->move_turtle_reflect(@args) if (2 == $wrap);
1041 0 0         return $self->move_turtle_torus(@args) if (1 == $wrap);
1042 0           return $self->move_turtle_normal(@args); # Assume wrap == 0
1043             }
1044              
1045              
1046             sub move_turtle_normal {
1047 0     0     my ($self, $pc, $x0, $y0, $x1, $y1) = @_;
1048 0           my $turtle = $pc->{'turtle'};
1049 0           my $pen = $turtle->{'pen'};
1050 0           my $size = $turtle->{'size'};
1051 0           my $color = $turtle->{'color'};
1052              
1053 0           $self->line($pen, $x0, $y0, $x1, $y1, $color, $size);
1054 0           $self->move($pc, $x1, $y1);
1055 0           $self->server_reply($pc);
1056             }
1057              
1058              
1059             sub move_turtle_torus {
1060 0     0     my ($self, $pc, $x0, $y0, $x1, $y1) = @_;
1061 0           my $turtle = $pc->{'turtle'};
1062 0           my $pen = $turtle->{'pen'};
1063 0           my $size = $turtle->{'size'};
1064 0           my $color = $turtle->{'color'};
1065              
1066             # Calculate (dx, dy), which don't change for torus behavior
1067 0           my ($dx, $dy) = ($x1 - $x0, $y1 - $y0);
1068              
1069 0           while (!$self->contained($x1, $y1)) {
1070 0           my $height = $self->{'height'};
1071 0           my $width = $self->{'width'};
1072 0 0         if (abs($dx) < 0.0000001) {
    0          
1073             # Vertical line
1074 0 0         my $yb = ($y1 < $y0)? 0: $height;
1075 0           $self->line($pen, $x0, $y0, $x0, $yb, $color, $size);
1076 0 0         ($y0, $y1) = $yb? (0, $y1-$height): ($height, $y1+$height);
1077 0           $self->move($pc, $x0, $y0);
1078             } elsif (abs($dy) < 0.0000001) {
1079             # Horizontal line
1080 0 0         my $xb = ($x1 < $x0)? 0: $width;
1081 0           $self->line($pen, $x0, $y0, $xb, $y0, $color, $size);
1082 0 0         ($x0, $x1) = $xb? (0, $x1-$width): ($width, $x1+$width);
1083 0           $self->move($pc, $x0, $y0);
1084             } else {
1085             # Diagonal line
1086 0           my $m = $dy / $dx;
1087 0           my $b = $y1 - ($m * $x1);
1088 0 0         my $xb = ($y1 > $y0)? (($height - $b) / $m): (-$b / $m);
1089 0 0         my $yb = ($x1 > $x0)? (($m * $width) + $b): $b;
1090 0           my ($xn, $yn) = ($xb, $yb);
1091 0 0 0       my $crossx = ($xb > 0 and $xb < $width)? 1: 0;
1092 0 0 0       my $crossy = ($yb > 0 and $yb < $height)? 1: 0;
1093 0 0 0       if ($crossx and !$crossy) {
    0 0        
1094             # Line intercepts x-axis
1095 0 0         $yb = ($y1 > $y0)? $height: 0;
1096 0           $yn = $height - $yb;
1097 0 0         $y1 = ($y1 > $y0)? $y1 - $height: $y1 + $height;
1098             } elsif ($crossy and !$crossx) {
1099             # Line intercepts y-axis
1100 0 0         $xb = ($x1 > $x0)? $width: 0;
1101 0           $xn = $width - $xb;
1102 0 0         $x1 = ($x1 > $x0)? $x1 - $width: $x1 + $width;
1103             } else {
1104             # Line intercepts both axes
1105 0 0         $xb = ($x1 > $x0)? $width: 0;
1106 0 0         $yb = ($y1 > $y0)? $height: 0;
1107 0           ($xn, $yn) = ($width - $xb, $height - $yb);
1108 0 0         $x1 = ($x1 > $x0)? $x1 - $width: $x1 + $width;
1109 0 0         $y1 = ($y1 > $y0)? $y1 - $height: $y1 + $height;
1110             }
1111              
1112 0           $self->line($pen, $x0, $y0, $xb, $yb, $color, $size);
1113 0           ($x0, $y0) = ($xn, $yn);
1114 0           $self->move($pc, $x0, $y0);
1115             }
1116             }
1117              
1118             # Back within canvas
1119 0           return $self->move_turtle_normal($pc, $x0, $y0, $x1, $y1);
1120             }
1121              
1122              
1123             sub move_turtle_reflect {
1124 0     0     my ($self, $pc, $x0, $y0, $x1, $y1) = @_;
1125 0           my $turtle = $pc->{'turtle'};
1126 0           my $angle = $turtle->{'angle'};
1127 0           my $pen = $turtle->{'pen'};
1128 0           my $size = $turtle->{'size'};
1129 0           my $color = $turtle->{'color'};
1130              
1131 0           while (!$self->contained($x1, $y1)) {
1132             # Calculate (dx, dy), which change for reflection behavior
1133 0           my ($dx, $dy) = ($x1 - $x0, $y1 - $y0);
1134              
1135 0           my $height = $self->{'height'};
1136 0           my $width = $self->{'width'};
1137 0 0         if (abs($dx) < 0.0000001) {
    0          
1138             # Vertical line
1139 0 0         my $yb = ($y1 < $y0)? 0: $height;
1140 0           $self->line($pen, $x0, $y0, $x0, $yb, $color, $size);
1141 0           $y0 = $yb;
1142 0 0         $y1 = ($y1 < $y0)? (- $y1): (2 * $height) - $y1;
1143 0           $self->move($pc, $x0, $y0);
1144 0           $angle = $self->adjust_angle($pc, 180 - $angle);
1145             } elsif (abs($dy) < 0.0000001) {
1146             # Horizontal line
1147 0 0         my $xb = ($x1 < $x0)? 0: $width;
1148 0           $self->line($pen, $x0, $y0, $xb, $y0, $color, $size);
1149 0           $x0 = $xb;
1150 0 0         $x1 = ($x1 < $x0)? (- $x1): (2 * $width) - $x1;
1151 0           $self->move($pc, $x0, $y0);
1152 0           $angle = $self->adjust_angle($pc, 360 - $angle);
1153             } else {
1154             # Diagonal line
1155 0           my $m = $dy / $dx;
1156 0           my $b = $y1 - ($m * $x1);
1157 0 0         my $xb = ($y1 > $y0)? (($height - $b) / $m): (-$b / $m);
1158 0 0         my $yb = ($x1 > $x0)? (($m * $width) + $b): $b;
1159 0 0 0       my $crossx = ($xb > 0 and $xb < $width)? 1: 0;
1160 0 0 0       my $crossy = ($yb > 0 and $yb < $height)? 1: 0;
1161 0 0 0       if ($crossx and !$crossy) {
    0 0        
1162             # Line intercepts x-axis
1163 0 0         $yb = ($y1 > $y0)? $height: 0;
1164 0 0         $y1 = ($y1 > $y0)? (2 * $height - $y1): (- $y1);
1165             } elsif ($crossy and !$crossx) {
1166             # Line intercepts y-axis
1167 0 0         $xb = ($x1 > $x0)? $width: 0;
1168 0 0         $x1 = ($x1 > $x0)? (2 * $width - $x1): (- $x1);
1169             } else {
1170             # Line intercepts both axes
1171 0 0         $xb = ($x1 > $x0)? $width: 0;
1172 0 0         $yb = ($y1 > $y0)? $height: 0;
1173 0 0         $x1 = ($x1 > $x0)? (2 * $width - $x1): (- $x1);
1174 0 0         $y1 = ($y1 > $y0)? (2 * $height - $y1): (- $y1);
1175             }
1176              
1177 0           $self->line($pen, $x0, $y0, $xb, $yb, $color, $size);
1178 0           ($x0, $y0) = ($xb, $yb);
1179 0           $self->move($pc, $x0, $y0);
1180 0           $angle = $self->adjust_angle($pc, 180 - $angle);
1181             }
1182             }
1183              
1184             # Back within canvas
1185 0           return $self->move_turtle_normal($pc, $x0, $y0, $x1, $y1);
1186             }
1187              
1188              
1189             sub adjust_angle {
1190 0     0     my ($self, $pc, $newang) = @_;
1191 0           my $turtle = $pc->{'turtle'};
1192 0           while ($newang >= 360) {
1193 0           $newang -= 360;
1194             }
1195 0           while ($newang < 0) {
1196 0           $newang += 360;
1197             }
1198 0           $turtle->{'angle'} = $newang;
1199 0           $self->draw_turtle($pc, $turtle);
1200 0           return $newang;
1201             }
1202              
1203              
1204             sub line {
1205 0     0     my ($self, $pen, $x0, $y0, $x1, $y1, $color, $size) = @_;
1206              
1207             # Pen is up; no need to draw
1208 0 0         return unless $pen;
1209              
1210             # Get canvas and draw line
1211 0           my $cv = $self->{'canvas'};
1212 0           my @points = ($x0, $y0, $x1, $y1, -fill => $color, -width => $size);
1213 0           $cv->createLine(@points);
1214             }
1215              
1216              
1217             sub move {
1218 0     0     my ($self, $pc, $x, $y) = @_;
1219              
1220             # Set new turtle coordinates and redraw turtle
1221 0           my $turtle = $pc->{'turtle'};
1222 0           $turtle->{'x'} = $x;
1223 0           $turtle->{'y'} = $y;
1224 0           $self->draw_turtle($pc, $turtle);
1225             }
1226              
1227              
1228             sub contained {
1229 0     0     my ($self, $x1, $y1) = @_;
1230              
1231 0           my $cv = $self->{'canvas'};
1232 0           my $width = $cv->cget(-width);
1233 0           my $height = $cv->cget(-height);
1234              
1235 0           $self->{'width'} = $width;
1236 0           $self->{'height'} = $height;
1237              
1238 0 0 0       return ($x1 < 0 or $x1 > $width or $y1 < 0 or $y1 > $height)? 0: 1;
1239             }
1240              
1241              
1242             1;
1243              
1244             __END__