File Coverage

blib/lib/Math/PlanePath/AlternateTerdragon.pm
Criterion Covered Total %
statement 176 214 82.2
branch 48 72 66.6
condition 24 26 92.3
subroutine 28 39 71.7
pod 16 16 100.0
total 292 367 79.5


line stmt bran cond sub pod time code
1             # Copyright 2018 Kevin Ryde
2              
3             # This file is part of Math-PlanePath.
4             #
5             # Math-PlanePath is free software; you can redistribute it and/or modify
6             # it under the terms of the GNU General Public License as published by the
7             # Free Software Foundation; either version 3, or (at your option) any later
8             # version.
9             #
10             # Math-PlanePath is distributed in the hope that it will be useful, but
11             # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
12             # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
13             # for more details.
14             #
15             # You should have received a copy of the GNU General Public License along
16             # with Math-PlanePath. If not, see .
17              
18              
19             package Math::PlanePath::AlternateTerdragon;
20 1     1   1299 use 5.004;
  1         4  
21 1     1   5 use strict;
  1         1  
  1         22  
22 1     1   5 use List::Util 'first';
  1         1  
  1         84  
23 1     1   6 use List::Util 'min'; # 'max'
  1         1  
  1         53  
24             *max = \&Math::PlanePath::_max;
25              
26 1     1   667 use Math::PlanePath;
  1         3  
  1         45  
27             *_divrem_mutate = \&Math::PlanePath::_divrem_mutate;
28              
29             use Math::PlanePath::Base::Generic
30 1         41 'is_infinite',
31             'round_nearest',
32 1     1   6 'xy_is_even';
  1         1  
33             use Math::PlanePath::Base::Digits
34 1         57 'digit_split_lowtohigh',
35             'digit_join_lowtohigh',
36 1     1   477 'round_up_pow';
  1         2  
37              
38 1     1   7 use vars '$VERSION', '@ISA';
  1         2  
  1         48  
39             $VERSION = 127;
40             @ISA = ('Math::PlanePath');
41              
42 1     1   611 use Math::PlanePath::TerdragonMidpoint;
  1         3  
  1         28  
43              
44             # uncomment this to run the ### lines
45             # use Smart::Comments;
46              
47              
48 1     1   5 use constant n_start => 0;
  1         2  
  1         57  
49 1         168 use constant parameter_info_array =>
50             [ { name => 'arms',
51             share_key => 'arms_6',
52             display => 'Arms',
53             type => 'integer',
54             minimum => 1,
55             maximum => 6,
56             default => 1,
57             width => 1,
58             description => 'Arms',
59 1     1   5 } ];
  1         2  
60              
61             sub x_negative {
62 2     2 1 98 my ($self) = @_;
63 2         11 return ($self->{'arms'} >= 2);
64             }
65             {
66             my @x_negative_at_n = (undef, undef, 5, 5, 6, 7, 8);
67             sub x_negative_at_n {
68 0     0 1 0 my ($self) = @_;
69 0         0 return $x_negative_at_n[$self->{'arms'}];
70             }
71             }
72             {
73             my @y_negative_at_n = (undef, 6, 12, 18, 11, 9, 10);
74             sub y_negative_at_n {
75 0     0 1 0 my ($self) = @_;
76 0         0 return $y_negative_at_n[$self->{'arms'}];
77             }
78             }
79             sub dx_minimum {
80 0     0 1 0 my ($self) = @_;
81 0 0       0 return ($self->{'arms'} == 1 ? -1 : -2);
82             }
83 1     1   6 use constant dx_maximum => 2;
  1         2  
  1         51  
84 1     1   6 use constant dy_minimum => -1;
  1         2  
  1         34  
85 1     1   5 use constant dy_maximum => 1;
  1         2  
  1         213  
86              
87             sub sumxy_minimum {
88 0     0 1 0 my ($self) = @_;
89             # arm 0 and arm 1 are always above X+Y=0 opposite diagonal, which is +120 deg
90 0 0       0 return ($self->{'arms'} <= 2 ? 0 : undef);
91             }
92             sub diffxy_minimum {
93 0     0 1 0 my ($self) = @_;
94             # arm 0 remains below the X-Y leading diagonal, being +60 deg
95 0 0       0 return ($self->{'arms'} <= 1 ? 0 : undef);
96             }
97              
98             sub _UNDOCUMENTED__dxdy_list {
99 0     0   0 my ($self) = @_;
100 0 0       0 return ($self->{'arms'} == 1
101             ? Math::PlanePath::_UNDOCUMENTED__dxdy_list_three()
102             : Math::PlanePath::_UNDOCUMENTED__dxdy_list_six());
103             }
104             {
105             my @_UNDOCUMENTED__dxdy_list_at_n = (undef, 3, 7, 10, 7, 8, 5);
106             sub _UNDOCUMENTED__dxdy_list_at_n {
107 0     0   0 my ($self) = @_;
108 0         0 return $_UNDOCUMENTED__dxdy_list_at_n[$self->{'arms'}];
109             }
110             }
111 1     1   6 use constant absdx_minimum => 1;
  1         2  
  1         39  
112 1     1   5 use constant dsumxy_minimum => -2; # diagonals
  1         2  
  1         42  
113 1     1   6 use constant dsumxy_maximum => 2;
  1         1  
  1         55  
114 1     1   6 use constant ddiffxy_minimum => -2;
  1         2  
  1         36  
115 1     1   5 use constant ddiffxy_maximum => 2;
  1         9  
  1         89  
116              
117             # arms=1 curve goes at 0,120,240 degrees
118             # arms=2 second +60 to 60,180,300 degrees
119             # so when arms==1 dir maximum is 240 degrees
120             sub dir_maximum_dxdy {
121 0     0 1 0 my ($self) = @_;
122 0 0       0 return ($self->{'arms'} == 1
123             ? (-1,-1) # 0,2,4 only South-West
124             : ( 1,-1)); # rotated to 1,3,5 too South-East
125             }
126              
127 1     1   5 use constant turn_any_straight => 0; # never straight
  1         2  
  1         1290  
128              
129              
130             #------------------------------------------------------------------------------
131              
132             sub new {
133 20     20 1 1896 my $self = shift->SUPER::new(@_);
134 20   100     104 $self->{'arms'} = max(1, min(6, $self->{'arms'} || 1));
135 20         37 return $self;
136             }
137              
138             my @dir6_to_dx = (2, 1,-1,-2, -1, 1);
139             my @dir6_to_dy = (0, 1, 1, 0, -1,-1);
140              
141             sub n_to_xy {
142 28     28 1 1785 my ($self, $n) = @_;
143             ### AlternateTerdragon n_to_xy(): $n
144              
145 28 50       64 if ($n < 0) { return; }
  0         0  
146 28 50       70 if (is_infinite($n)) { return ($n, $n); }
  0         0  
147              
148 28         43 my $zero = ($n * 0); # inherit bignum 0
149 28         34 my $i; # X
150 28         37 my $j = $zero; # +60
151 28         34 my $k = $zero; # +120
152 28         36 my $pow = $zero + 1; # inherit bignum 1
153              
154             # initial rotation from arm number
155 28         29 my $rot;
156             {
157 28         31 my $int = int($n);
  28         43  
158 28         32 $i = $n - $int; # frac, inherit possible BigFloat
159 28         31 $n = $int; # BigFloat int() gives BigInt, use that
160 28         60 $rot = _divrem_mutate ($n, $self->{'arms'});
161             }
162              
163             # even si = pow, sj = 0, sk = 0
164             # odd si = pow, sj = 0, sk = -pow
165              
166 28         36 my $even = 1;
167 28         63 my @n = digit_split_lowtohigh($n,3);
168 28         55 while (@n) {
169 74         84 my $digit = shift @n;
170             ### at: "$i, $j, $k even digit $digit"
171 74 100       125 if ($digit == 1) {
    100          
172 26         49 ($i,$j,$k) = ($pow-$j, -$k, $i); # rotate +120 and add
173             } elsif ($digit == 2) {
174 26         32 $j += $pow; # add rotated +60
175             }
176              
177 74 100       115 last unless @n;
178 61         69 $digit = shift @n;
179 61 100       103 if ($digit == 1) {
    100          
180 27         45 ($i,$j,$k) = ($pow+$k, $pow-$i, -$j); # rotate -120 and add
181             } elsif ($digit == 2) {
182 16         21 $i += $pow; # add * b
183 16         22 $k -= $pow;
184             }
185 61         89 $pow *= 3;
186             }
187              
188             ### final: "$i, $j, $k"
189             ### is: (2*$i + $j - $k).", ".($j+$k)
190              
191             ### $rot
192 28 100       50 if ($rot >= 3) {
193 2         6 ($i,$j,$k) = (-$i,-$j,-$k);
194 2         3 $rot -= 3;
195             }
196 28 100       56 if ($rot == 1) { ($i,$j,$k) = (-$k,$i,$j); } # rotate +60
  8 50       17  
197 0         0 elsif ($rot == 2) { ($i,$j,$k) = (-$j,-$k, $i); } # rotate +128
198              
199 28         86 return (2*$i + $j - $k, $j+$k);
200             }
201              
202             # all even points when arms==6
203             sub xy_is_visited {
204 0     0 1 0 my ($self, $x, $y) = @_;
205 0 0       0 if ($self->{'arms'} == 6) {
206 0         0 return xy_is_even($self,$x,$y);
207             } else {
208 0         0 return defined($self->xy_to_n($x,$y));
209             }
210             }
211              
212             sub xy_to_n {
213 20     20 1 78 return scalar((shift->xy_to_n_list(@_))[0]);
214             }
215             sub xy_to_n_list {
216 35     35 1 8893 my ($self, $x,$y) = @_;
217             ### AlternateTerdragon xy_to_n_list(): "$x, $y"
218              
219 35         78 $x = round_nearest($x);
220 35         63 $y = round_nearest($y);
221             {
222             # nothing at an odd point, and trap overflows in $x+$y dividing out b
223 35         41 my $sum = abs($x) + abs($y);
  35         50  
224 35 50       61 if (is_infinite($sum)) { return $sum; } # infinity
  0         0  
225 35 50       79 if ($sum % 2) { return; }
  0         0  
226             }
227              
228 35 100 66     77 if ($x==0 && $y==0) {
229 10         28 return 0 .. $self->{'arms'}-1;
230             }
231              
232 25         59 my $arms_count = $self->arms_count;
233 25         34 my $zero = ($x * 0 * $y); # inherit bignum 0
234              
235 25         31 my @n_list;
236 25         41 foreach my $d (0,1,2) {
237 75         117 my ($ndigits,$arm) = _xy_d_to_ndigits_and_arm($x,$y,$d);
238 75 100       121 next if $arm >= $arms_count;
239 72 100       109 if ($arm & 1) {
240             ### flip ...
241 28         50 @$ndigits = map {2-$_} @$ndigits;
  130         176  
242             }
243 72         151 push @n_list,
244             digit_join_lowtohigh($ndigits, 3, $zero) * $arms_count + $arm;
245             }
246              
247             ### unsorted n_list: @n_list
248 25         90 return sort {$a<=>$b} @n_list;
  56         124  
249             }
250              
251             my @digit_to_x = ([0,2,1], [0,-1,-2], [0,-1, 1]);
252             my @digit_to_y = ([0,0,1], [0, 1, 0], [0,-1,-1]);
253              
254             # $d = 0,1,2 for segment leaving $x,$y at direction $d*120 degrees.
255             # For odd arms the digits are 0<->2 reversals.
256             sub _xy_d_to_ndigits_and_arm {
257 75     75   115 my ($x,$y, $d) = @_;
258             ### _xy_d_to_ndigits_and_arm(): "$x,$y d=$d"
259 75         84 my @ndigits;
260             my $arm;
261 75         79 for (;;) {
262             ### at: "$x,$y d=$d"
263 907 100 100     811 if ($x==0 && $y==0) { $arm = 2*$d; last; }
  44         58  
  44         54  
264 863 100 100     811 if ($d==2 && $x==1 && $y==1) { $arm = 1; last; }
  16   100     24  
  16         22  
265 847 100 100     767 if ($d==0 && $x==-2 && $y==0) { $arm = 3; last; }
  9   66     122  
  9         15  
266 838 100 100     793 if ($d==1 && $x==1 && $y==-1) { $arm = 5; last; }
  6   100     7  
  6         8  
267 832         480 my $a = $x % 3; # z mod b = -x mod 3
268 832 100       553 if ($a) { $a = 3-$a; }
  298         385  
269 832         686 push @ndigits, $a;
270              
271 832 100       591 if ($a==1) { $d = ($d-1) % 3; }
  162         187  
272             ### a: $a
273             ### new d: $d
274              
275 832         968 $x -= $digit_to_x[$d]->[$a];
276 832         511 $y -= $digit_to_y[$d]->[$a];
277             ### subtract: "$digit_to_x[$d]->[$a],$digit_to_y[$d]->[$a] to $x,$y"
278              
279             ### assert: ($x+$y) % 2 == 0
280             ### assert: $x % 3 == 0
281             ### assert: ($y-$x/3) % 2 == 0
282             ### assert: (3*$y-$x) % 6 == 0
283              
284 832         3501 ($x,$y) = (($x+$y)/2, # divide b = w6+1
285             ($y-$x/3)/2);
286              
287 832         470 $y = -$y;
288 832         510 $d = (-$d) % 3;
289             }
290 75 100       163 if (scalar(@ndigits) & 1) { $arm = (6-$arm) % 6; }
  32         47  
291             ### $arm
292             ### @ndigits
293 75         184 return (\@ndigits, $arm);
294             }
295              
296             # not exact
297             sub rect_to_n_range {
298 20     20 1 1274 my ($self, $x1,$y1, $x2,$y2) = @_;
299             ### AlternateTerdragon rect_to_n_range(): "$x1,$y1 $x2,$y2"
300 20         139 my $xmax = int(max(abs($x1),abs($x2)));
301 20         36 my $ymax = int(max(abs($y1),abs($y2)));
302             return (0,
303             ($xmax*$xmax + 3*$ymax*$ymax + 1)
304             * 2
305 20         54 * $self->{'arms'});
306             }
307              
308             my @digit_to_nextturn = (2,-2);
309             sub n_to_dxdy {
310 4     4 1 362 my ($self, $n) = @_;
311             ### AlternateTerdragon n_to_dxdy(): $n
312              
313 4 50       11 if ($n < 0) {
314 0         0 return; # first direction at N=0
315             }
316 4 50       9 if (is_infinite($n)) {
317 0         0 return ($n,$n);
318             }
319              
320 4         10 my $int = int($n); # integer part
321 4         11 $n -= $int; # fraction part
322              
323             # initial direction from arm
324 4         13 my $dir6 = _divrem_mutate ($int, $self->{'arms'});
325              
326 4         9 my @ndigits = digit_split_lowtohigh($int,3);
327 4         9 foreach my $i (0 .. $#ndigits) {
328 2 100       19 if ($ndigits[$i] == 1) {
329 1 50       6 $dir6 += 2*($i&1 ? -1 : 1); # count 1s for total turn
330             }
331             }
332 4         7 $dir6 %= 6;
333 4         7 my $dx = $dir6_to_dx[$dir6];
334 4         5 my $dy = $dir6_to_dy[$dir6];
335              
336 4 100       9 if ($n) {
337             ### fraction part: $n
338              
339             # find lowest non-2 digit, or zero if all 2s or no digits at all
340 3         6 my $above = scalar(@ndigits);
341 3         6 foreach my $i (0 .. $#ndigits) {
342 2 100       5 if ($ndigits[$i] != 2) {
343             ### lowest non-2: "at i=$i digit=$ndigits[$i]"
344 1         2 $above = $ndigits[$i] ^ $i;
345 1         2 last;
346             }
347             }
348              
349 3         7 $dir6 = ($dir6 + $digit_to_nextturn[$above & 1]) % 6;
350             ### $above
351             ### $dir6
352              
353 3         5 $dx += $n*($dir6_to_dx[$dir6] - $dx);
354 3         6 $dy += $n*($dir6_to_dy[$dir6] - $dy);
355             }
356 4         12 return ($dx, $dy);
357             }
358              
359              
360             #-----------------------------------------------------------------------------
361             # eg. arms=5 0 .. 5*3^k step by 5s
362             # 1 .. 5*3^k+1 step by 5s
363             # 4 .. 5*3^k+4 step by 5s
364             #
365             sub level_to_n_range {
366 0     0 1   my ($self, $level) = @_;
367 0           return (0, (3**$level + 1) * $self->{'arms'} - 1);
368             }
369             sub n_to_level {
370 0     0 1   my ($self, $n) = @_;
371 0 0         if ($n < 0) { return undef; }
  0            
372 0 0         if (is_infinite($n)) { return $n; }
  0            
373 0           $n = round_nearest($n);
374 0           _divrem_mutate ($n, $self->{'arms'});
375 0           my ($pow, $exp) = round_up_pow ($n, 3);
376 0           return $exp;
377             }
378              
379             1;
380             __END__