File Coverage

blib/lib/Gtk2/Hexgrid.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 Gtk2::Hexgrid;
2              
3             our $VERSION = '0.06';
4              
5 5     5   73949 use warnings;
  5         12  
  5         153  
6 5     5   28 use strict;
  5         8  
  5         151  
7 5     5   24 use Carp;
  5         9  
  5         372  
8 5     5   15847 use Gtk2;
  0            
  0            
9             use Cairo;
10             use POSIX qw(ceil floor);
11             use Gtk2::Hexgrid::Tile;
12             use base 'Gtk2::EventBox';
13              
14             sub new{
15             my $class = shift;
16             my ($w,$h, $lineSize, $border, $evenRowsFirst, $evenRowsLast, $r,$g,$b) = @_;
17            
18             ($r,$g,$b) = (0,.4,0) unless defined($r) and defined($g) and defined($b);
19            
20             my $self= new Gtk2::EventBox->new;
21             $self->{images} = {}; #cache for sprites
22             $self->{w} = $w;
23             $self->{h} = $h;
24             $self->{linesize} = $lineSize;
25             $self->{border} = $border;
26             $self->{evenFirst} = $evenRowsFirst != 0; #these may need to be 1 or 0
27             $self->{evenLast} = $evenRowsLast != 0;
28             $self->{gameBoard} = Gtk2::DrawingArea->new;
29             my @dimensions = _calc_board_dimensions($w, $h, $lineSize,
30             $border, $evenRowsFirst, $evenRowsLast);
31             $self->{gameBoard}->size (@dimensions);
32             $self->add($self->{gameBoard});
33            
34             $self->{gameBoard}->signal_connect ("expose_event" => \&_expose_event, $self);
35             $self->signal_connect ("button_press_event" => \&_button_press_cb, $self);
36             #init tiles
37             for my $row(0..$h){
38             my @thisRow;
39             for my $col(0..$w){
40             next unless tile_exists($self, $col, $row);
41             my $tile= new Gtk2::Hexgrid::Tile($self, $col, $row, $r,$g,$b);
42             push @thisRow, $tile; #what data should this be?
43             }
44             push @{$self->{tiles}}, \@thisRow;
45             }
46            
47             bless $self, $class;
48             return $self;
49             }
50              
51             sub redraw_board{
52             my $self = shift;
53             $self->{gameBoard}->queue_draw;
54             }
55              
56             sub get_cairo_context{
57             my $self = shift;
58             my $drawable= $self->{gameBoard}->window;
59             return Gtk2::Gdk::Cairo::Context->create ($drawable);
60             }
61              
62             #(in pixels)
63             sub _calc_board_dimensions{
64             my ($w,$h, $ls, $border, $evenFirst, $evenLast) = @_;
65             my $pixelsW = $ls*3*($w-1) + $ls*2;
66             $pixelsW += $ls*1.5 unless $evenFirst;
67             $pixelsW += $ls*1.5 unless $evenLast;
68             my $pixelsH = $ls*(sqrt(3)/2)*($h+1);
69             $pixelsW += $border*2;
70             $pixelsH += $border*2;
71             return ($pixelsW, $pixelsH);
72             }
73              
74             # direction: -1 is n, 0 ne, 1 se, 2 s, 3 sw, 4 nw, .....
75             # direction wraps around.
76             sub next_tile_by_direction{
77             my ($self, $col,$row, $dir) = @_;
78             croak 'usage: $hexgrid->next_tile_by_direction($col, $row, $direction)'
79             unless (ref($self) && defined($col) && defined($row) && defined($dir));
80             $dir %= 6;
81             return $self->get_tile($col, $row+2) if $dir==2;
82             return $self->get_tile($col, $row-2) if $dir==5;
83             my $otherCol = ($row&1 ^ $self->{evenFirst}) ? $col-1 : $col+1;
84             if ($otherCol > $col){
85             return $self->get_tile($col+1, $row-1) if $dir==0;
86             return $self->get_tile($col+1, $row+1) if $dir==1;
87             return $self->get_tile($col, $row+1) if $dir==3;
88             return $self->get_tile($col, $row-1) if $dir==4;
89             croak "why did I die";
90             }
91             return $self->get_tile($col, $row-1) if $dir==0;
92             return $self->get_tile($col, $row+1) if $dir==1;
93             return $self->get_tile($col-1, $row+1) if $dir==3;
94             return $self->get_tile($col-1, $row-1) if $dir==4;
95             croak "you've killed me!";
96             }
97              
98             sub next_col_row_by_direction{
99             my ($self, $col,$row, $dir) = @_;
100             croak 'usage: $hexgrid->next_col_row_by_direction($col, $row, $direction)'
101             unless (ref($self) && defined($col) && defined($row) && defined($dir));
102             $dir %= 6;
103             return ($col, $row+2) if $dir==2;
104             return ($col, $row-2) if $dir==5;
105             my $otherCol = ($row&1 ^ $self->{evenFirst}) ? $col-1 : $col+1;
106             if ($otherCol > $col){
107             return ($col+1, $row-1) if $dir==0;
108             return ($col+1, $row+1) if $dir==1;
109             return ($col, $row+1) if $dir==3;
110             return ($col, $row-1) if $dir==4;
111             croak "why did I die";
112             }
113             return ($col, $row-1) if $dir==0;
114             return ($col, $row+1) if $dir==1;
115             return ($col-1, $row+1) if $dir==3;
116             return ($col-1, $row-1) if $dir==4;
117             croak "you've killed me!";
118             }
119              
120             sub get_adjacent_tile_coordinates{
121             my ($self, $col,$row) = @_;
122             croak 'usage: $hexgrid->get_adjacent_tile_coordinates($col, $row)'
123             unless (ref($self) && defined($col) && defined($row));
124             my @tiles;
125             push @tiles, [$col, $row-2];
126             push @tiles, [$col, $row+2];
127             push @tiles, [$col, $row-1];
128             push @tiles, [$col, $row+1];
129             my $otherCol = ($row&1 ^ $self->{evenFirst}) ? $col-1 : $col+1;
130             push @tiles, [$otherCol, $row-1];
131             push @tiles, [$otherCol, $row+1];
132             return @tiles;
133             }
134              
135             sub get_adjacent_tiles{
136             my ($self, $col,$row) = @_;
137             croak 'usage: $hexgrid->get_adjacent_tiles($col, $row)'
138             unless (ref($self) && defined($col) && defined($row));
139             my @co = $self->get_adjacent_tile_coordinates($col,$row);
140             my @tiles;
141             for my $c (@co){
142             my $tile = $self->get_tile($c->[0], $c->[1]);
143             push @tiles, $tile if $tile;
144             }
145             return @tiles;
146             }
147              
148             sub tiles_adjacent{
149             my ($self, $col1,$row1,$col2,$row2) = @_;
150             croak 'usage: $hexgrid->tiles_adjacent($col1,$row1,$col2,$row2)'
151             unless (ref($self) && defined($row1) && defined($row2) && defined($col1) && defined($col2));
152             my @tiles = $self->get_adjacent_tile_coordinates($col1,$row1);
153             for my $T (@tiles){
154             if ($T->[0]==$col2 && $T->[1]==$row2){
155             return 1
156             }
157             }
158             return 0
159             }
160              
161             #imagine 6 spokes extending outward at the corners, and looping back around
162             #dealing with coordinates rather than tiles, as it could run into undefined space and back
163             sub get_ring{
164             my ($self, $col, $row, $radius) = @_;
165             return $self->get_tile($col, $row) if $radius==0;
166             my @corners = map{[$col, $row]} (0..5);
167             my @tiles_co;
168             #for my $ring (1..$radius){
169             for my $dir(0..5){
170             for (1..$radius){
171             my @co = $self->next_col_row_by_direction(@{$corners[$dir]}[0,1], $dir);
172             $corners[$dir] = \@co;
173             }
174             my @tmp = @{$corners[$dir]};
175             for (1..$radius){
176             @tmp = $self->next_col_row_by_direction(@tmp, $dir+2);
177             push @tiles_co, [@tmp];
178             }
179             }
180             my @tiles = grep {defined $_} map {$self->get_tile(@$_)} @tiles_co;
181             #map {print STDERR join (',',@$_), "\n"} @tiles_co;
182             return @tiles;
183             }
184             sub get_tiles_in_range{
185             my ($self, $col, $row, $range) = @_;
186             my @tiles;
187             for my $radius (0..$range){
188             push @tiles, $self->get_ring($col, $row, $radius);
189             }
190             return @tiles;
191             }
192              
193             sub get_tile_center{
194             my ($self, $col, $row) = @_;
195             croak 'usage: $hexgrid->get_tile_center($col,$row)'
196             unless (ref($self) && defined($col) && defined($row));
197             my $ls = $self->{linesize};
198             my $evenFirst = $self->{evenFirst};
199             my $evenLast = $self->{evenLast};
200             #center of tile at upper left corner
201             my $x0 = $ls;
202             my $y0 = $ls * sqrt(3)/2;
203             my $oddRow = $row&1;
204             if(($oddRow and $evenFirst) or not ($oddRow or $evenFirst)){
205             $x0 += $ls*1.5;
206             }
207             $x0 += $ls*$col*3;
208             $y0 += $ls*$row*sqrt(3)/2;
209             $x0 += $self->{border};
210             $y0 += $self->{border};
211             return ($x0,$y0);
212             }
213              
214             sub _distBetweenRows{
215             my $ls=shift;
216             ($ls *sqrt(3)/2)
217             }
218             sub _distBetweenCols{ #more like horizontal distance between diagonal lines
219             my $ls=shift;
220             ($ls *1.5)
221             }
222              
223             sub _dist{
224             sqrt(($_[0]-$_[2])**2 + ($_[1]-$_[3])**2)
225             }
226              
227             sub get_col_row_from_XY{
228             my ($self, $x, $y) = @_;
229             croak 'usage: $hexgrid->get_col_row_from_XY($x,$y)'
230             unless (ref($self) && defined($x) && defined($y));
231             my $ls = $self->{linesize};
232             my ($bestDist,$bestCol,$bestRow) = ($ls*50,-8,-8); #values to be replaced
233             my ($startRow,$startCol) = (-2,-1);
234             my $endRow = $self->{h} + 1;
235             my $endCol = $self->{w};
236             for my $row ($startRow..$endRow){
237             for my $col ($startCol..$endCol){
238             my ($centerX,$centerY) = $self->get_tile_center($col,$row);
239             my $dist = _dist($centerX,$centerY, $x, $y);
240             if ($dist < $bestDist){
241             ($bestDist,$bestCol,$bestRow) = ($dist, $col,$row);
242             }
243             }
244             }
245             return ($bestCol,$bestRow)
246             }
247              
248             sub get_tile_from_XY{
249             my ($self, $x, $y) = @_;
250             croak 'usage: $hexgrid->get_tile_from_XY($x,$y)'
251             unless (ref($self) && defined($x) && defined($y));
252             my ($col,$row) = $self->get_col_row_from_XY($x,$y);
253             my $tile = $self->get_tile($col,$row);
254             return $tile;
255             }
256             #this func translates mouseclicks to another coordinate system.
257             #consider the area beside each diagonal line on the grid to be a chunk.
258             #this figures out what chunk x and y belong to, and then what side of the diag it is on
259             # fix this if you need to get nonexistant tile coordinates on a potentially infinite plane
260             sub _get_col_row_from_XY_fast_broken{
261             my ($self, $x, $y) = @_;
262             my $ls = $self->{linesize};
263             my ($c0x, $c0y) = $self->get_tile_center(0,0);
264             my $relativeY = ($y - $c0y); #y dist from tile 0,0
265             my $relativeX = ($x - $c0x); #x dist from tile 0,0
266             unless ($self->{evenFirst}){ #rounded corner--origin is 1 chunk to left
267             $relativeX += distBetweenCols($ls);
268             }
269             # the row could be either $vert or $vert+1
270             # column could be either $horiz/2 or ($horiz+1)/2
271             # use pythagorian to find out the truth
272             my $vert = floor ($relativeY / distBetweenRows($ls));
273             my $horiz = ($relativeX / distBetweenCols($ls));
274             my ($x1,$x2,$y1,$y2);
275             ($x1, $x2) = (floor($horiz/2) , floor(($horiz+1)/2));
276             if($self->{evenFirst} != ($vert&2)){ #right tile lower than left
277             ($y1,$y2) = ($vert, $vert+1);
278             }
279             else{ #right tile is higher
280             ($y1,$y2) = ($vert+1, $vert);
281             }
282             my @center1 = $self->get_tile_center($x1,$y1);
283             my @center2 = $self->get_tile_center($x2,$y2);
284             my $dist1 = dist(@center1, $x, $y);
285             my $dist2 = dist(@center2, $x, $y);
286             if ($dist1<$dist2){
287             return ($x1,$y1)
288             }
289             return ($x2,$y2)
290             }
291              
292             sub tile_exists{
293             my ($self, $col,$row) = @_;
294             croak 'usage: $hexgrid->tile_exists($col,$row)'
295             unless (ref($self) && defined($col) && defined($row));
296             my $evenFirst = $self->{evenFirst};
297             my $evenLast = $self->{evenLast};
298             return 0 if $row <0;
299             return 0 if $col <0;
300             return 0 if $row >= $self->{h}; # obvious case
301             my $oddRow = $row&1;
302             unless ($oddRow){ #even rows are always of size $hexgrid->{w}
303             return 0 if $col >= $self->{w};
304             return 1;
305             }#only odd rows left
306             if ($evenFirst != $evenLast){ #odd rows = even rows
307             return 0 if $col >= $self->{w};
308             }
309             elsif ($evenFirst == 0){ ##odd rows = even rows+1
310             return 0 if $col >= $self->{w} +1
311             }
312             elsif ($evenFirst == 1){ ##odd rows = even rows-1
313             return 0 if $col >= $self->{w} -1
314             }
315             return 1;
316             }
317              
318             sub get_all_tiles{
319             my $self = shift;
320             croak 'usage: $hexgrid->get_all_tiles;'
321             unless ref($self);
322             my ($w,$h) = @{$self}{'w','h'};
323             my @tiles;
324             for my $row (0..$h-1){
325             for my $col (0..$w){
326             if ($self->tile_exists($col,$row)){
327             my $tile = $self->get_tile($col,$row);
328             push (@tiles, $tile) if $tile;
329             }
330             }
331             }
332             return @tiles;
333             }
334              
335             sub get_tile{
336             my ($self, $col,$row) = @_;
337             croak 'usage: $hexgrid->get_tile($col, $row)'
338             unless (ref($self) && defined($col) && defined($row));
339             return undef unless $self->tile_exists($col,$row);
340             my $tile = $self->{tiles}->[$row][$col];
341             return $tile;
342             }
343              
344             #return the total number of tiles
345             sub num_tiles{
346             my $self = shift;
347             return $self->{numTiles} if $self->{numTiles};
348             $self->{numTiles} = scalar $self->get_all_tiles;
349             return $self->{numTiles};
350             }
351              
352             #corners of the grid can be 1 or 2 tiles
353             sub nw_corner{
354             my $self = shift;
355             return $self->get_tile(0,0) if $self->{evenFirst};
356             return ($self->get_tile(0,0), $self->get_tile(0,1));
357             }
358             sub ne_corner{
359             my $self = shift;
360             my @tiles = $self->get_tile($self->{w}-1,0);
361             unless($self->{evenLast}){
362             push @tiles, $self->get_tile ($self->{w}-$self->{evenFirst}, 1);
363             }
364             return @tiles;
365             }
366             sub sw_corner{
367             my $self = shift;
368             my @tiles = $self->get_tile (0, $self->{h}-1);
369             if ($self->{evenFirst} ^ ($self->{h}%2)){
370             push @tiles, $self->get_tile(0,$self->{h}-2)
371             }
372             return @tiles
373             }
374             #if this needs debugging, try looking at the coordinates on the example.
375             sub se_corner{
376             my $self = shift;
377             my @tiles; # = $self->get_tile (0, $self->{h}-1);
378             #return undef;
379             if ($self->{evenLast}){
380             push @tiles, $self->get_tile ($self->{w}-1, $self->{h}-1);
381             unless ($self->{h}%2){
382             push @tiles, $self->get_tile ($self->{w}-1, $self->{h}-2);
383             }
384             }
385             else{ #odd last
386             if ($self->{h}%2){
387             push @tiles, $self->get_tile ($self->{w}-1, $self->{h}-1);
388             push @tiles, $self->get_tile ($self->{w}, $self->{h}-2);
389             }
390             else{
391             push @tiles, $self->get_tile ($self->{w}, $self->{h}-1);
392             }
393             }
394             return @tiles
395             }
396              
397             sub tile_w{
398             return shift->{linesize}*2
399             }
400             sub tile_h{
401             return shift->{linesize}*sqrt(3)
402             }
403              
404             sub load_image{
405             my ($self, $imagename, $filename, $scale_to_tile) = @_;
406             croak 'usage: $hexgrid->load_image($imagename, $filename, $scale_to_tile)'
407             unless (ref($self) && defined($imagename) && defined($filename));
408             croak "file $filename not found" unless -e $filename;
409            
410             return if $self->{images}->{$imagename};
411             my $surface = Cairo::ImageSurface->create_from_png ($filename);
412             if($scale_to_tile){
413             my $format = $surface->get_format;
414             my $cur_w= $surface->get_width;
415             my $cur_h= $surface->get_height;
416             my $to_w = $self->tile_w;
417             my $to_h = $self->tile_h;
418             my $scaledSurf = Cairo::ImageSurface->create ($format, $to_w, $to_h);
419             my $cr = Cairo::Context->create ($scaledSurf);
420             $cr->scale($to_w/$cur_w, $to_h/$cur_h);
421             $cr->set_source_surface ($surface, 0, 0);
422             $cr->paint;
423             $surface = $scaledSurf;
424             }
425             $self->{images}->{$imagename} = $surface;
426             }
427              
428             sub get_image{
429             my ($self, $name) = @_;
430             croak 'usage: $hexgrid->get_image($imagename)'
431             unless (ref($self) && defined($name));
432             return $self->{images}->{$name}
433             }
434              
435             sub _draw_sprite{
436             my ($self, $cr, $sprite) = @_;
437             my $type = $sprite->type;
438             my $tile = $sprite->tile;
439             my ($col, $row) = $tile->colrow;
440             unless($self->tile_exists($col, $row)){
441             carp "tile at $col $row doesn't exist" and return;
442             }
443             my ($x, $y) = $self->get_tile_center($col, $row);
444             # warn $type;
445             if($type eq 'text'){
446             my $text = $sprite->text;
447             my $fontSize = $sprite->size;
448             $cr->select_font_face ('sans', 'normal', 'normal');
449             $cr->set_font_size ($fontSize);
450             $cr->set_source_rgb (0, .0, .0);
451             my $extents = $cr->text_extents($text);
452             my ($w, $h) = @{$extents}{qw/width height/};
453             $x -= $w/2;
454             $y += $h/2;
455             $cr->move_to($x, $y);
456             $cr->show_text($text);
457             }
458             elsif($type eq 'image'){
459             my $imagename = $sprite->imageName;
460             my $image = $self->get_image($imagename);
461             $cr->set_source_rgb (.5,.5,.5);
462             my $w = $image->get_width;
463             my $h = $image->get_height;
464             $x -= $w/2;
465             $y -= $h/2;
466             #$cr->move_to($x, $y);
467             $cr->set_source_surface ($image, int $x, int $y);
468             $cr->paint;
469             }
470             }
471              
472             sub draw_tile{
473             my ($self, $cr, $col,$row, $r,$g,$b) = @_;
474             croak 'usage: $hexgrid->draw_tile($cr, $cr(optional), $col, $row, $r,$g,$b(optional))'
475             unless (ref($self) && defined($col) && defined($row));
476             return 0 unless $self->tile_exists($col,$row);
477            
478             my $tile = $self->get_tile($col, $row);
479             unless (defined($r) and defined($g) and defined($b)){
480             ($r,$g,$b) = @{$tile}{'r','g','b'};
481             }
482             $cr = $self->get_cairo_context unless ($cr);
483             my $ls=$self->{linesize};
484             my ($topX, $topY) = ($ls/2, -$ls * sqrt(3)/2); #upper right corner
485             my ($sideX, $sideY) = ($ls, 0); #right-side corner
486             #draw lines around tile center
487             my ($cx, $cy) = get_tile_center($self, $col,$row);
488             $cr->move_to($cx+$topX, $cy+$topY); #start at top-right
489             $cr->line_to($cx+$sideX, $cy+$sideY);
490             $cr->line_to($cx+$topX, $cy-$topY);
491             $cr->line_to($cx-$topX, $cy-$topY);
492             $cr->line_to($cx-$sideX, $cy-$sideY);
493             $cr->line_to($cx-$topX, $cy+$topY);
494             my $path= $cr->copy_path;
495             $cr->set_source_rgb($r, $g, $b);
496             $cr->fill;
497            
498             #draw sprites
499             $cr->append_path($path);
500             $cr->clip;
501             my $sprites = $tile->sprites;
502             $self->_draw_sprite($cr, $_) for (@$sprites);
503             $cr->reset_clip;
504            
505             #stroke hex border
506             $cr->append_path($path);
507             $cr->close_path;
508             $cr->set_source_rgb (0, .0, .0);
509             $cr->set_line_width (2);
510             $cr->stroke;
511             }
512             sub draw_tile_ref{
513             my ($self, $tile, $r,$g,$b) = @_;
514             croak 'usage: $hexgrid->draw_tile_ref($tile)' ." OR\n".
515             'usage: $hexgrid->draw_tile_ref($tile, $r,$g,$b)'
516             unless (ref($self) && ref($tile));
517             $self->draw_tile (undef, $tile->colrow, $r,$g,$b)
518             }
519              
520             sub _expose_event{
521             my ($widget, $eventexpose, $hexgrid) = @_;
522             my $cr = get_cairo_context($hexgrid);
523            
524             my @area= $eventexpose->area->values;
525             my $tiles = $hexgrid->{tiles};
526             for my $rownum (0..$#$tiles){
527             my $row= $tiles->[$rownum];
528             for my $colnum(0..@$row){
529             my $tile = $row->[$colnum];
530             if($hexgrid->tile_exists($colnum,$rownum)){
531             draw_tile($hexgrid, $cr, $colnum,$rownum);
532             }
533             }
534             }
535             }
536              
537             sub on_click{
538             my ($self, $func) = @_;
539             croak 'usage: $hexgrid->on_click(\&func)'
540             unless (ref($self) && ref($func) eq 'CODE');
541             $self->{onClick} = $func;
542             }
543              
544             sub _button_press_cb{
545             # $widget is an eventbox
546             my ($widget, $event, $hexgrid) = @_;
547             my ($x, $y)= $event->coords;
548             my ($col, $row)= get_col_row_from_XY ($hexgrid, $x, $y);
549             if ($hexgrid->{onClick}){
550             $hexgrid->{onClick}->($col,$row, $x, $y) ;
551             }
552             }
553             q ! positively!;
554             __END__