File Coverage

blib/lib/Math/PlanePath/AlternateTerdragon.pm
Criterion Covered Total %
statement 177 214 82.7
branch 49 72 68.0
condition 25 26 96.1
subroutine 28 39 71.7
pod 16 16 100.0
total 295 367 80.3


line stmt bran cond sub pod time code
1             # Copyright 2018, 2019 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   1206 use 5.004;
  1         3  
21 1     1   5 use strict;
  1         1  
  1         20  
22 1     1   4 use List::Util 'first';
  1         2  
  1         85  
23 1     1   6 use List::Util 'min'; # 'max'
  1         2  
  1         54  
24             *max = \&Math::PlanePath::_max;
25              
26 1     1   629 use Math::PlanePath;
  1         2  
  1         41  
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         54 'digit_split_lowtohigh',
35             'digit_join_lowtohigh',
36 1     1   464 'round_up_pow';
  1         2  
37              
38 1     1   6 use vars '$VERSION', '@ISA';
  1         1  
  1         47  
39             $VERSION = 128;
40             @ISA = ('Math::PlanePath');
41              
42 1     1   518 use Math::PlanePath::TerdragonMidpoint;
  1         2  
  1         29  
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         70  
49 1         186 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 99 my ($self) = @_;
63 2         10 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         40  
84 1     1   5 use constant dy_minimum => -1;
  1         1  
  1         45  
85 1     1   5 use constant dy_maximum => 1;
  1         2  
  1         189  
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         1  
  1         50  
112 1     1   5 use constant dsumxy_minimum => -2; # diagonals
  1         2  
  1         35  
113 1     1   4 use constant dsumxy_maximum => 2;
  1         2  
  1         46  
114 1     1   6 use constant ddiffxy_minimum => -2;
  1         8  
  1         50  
115 1     1   6 use constant ddiffxy_maximum => 2;
  1         2  
  1         87  
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   6 use constant turn_any_straight => 0; # never straight
  1         1  
  1         1232  
128              
129              
130             #------------------------------------------------------------------------------
131              
132             sub new {
133 20     20 1 1789 my $self = shift->SUPER::new(@_);
134 20   100     92 $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 1925 my ($self, $n) = @_;
143             ### AlternateTerdragon n_to_xy(): $n
144              
145 28 50       57 if ($n < 0) { return; }
  0         0  
146 28 50       58 if (is_infinite($n)) { return ($n, $n); }
  0         0  
147              
148 28         41 my $zero = ($n * 0); # inherit bignum 0
149 28         32 my $i; # X
150 28         39 my $j = $zero; # +60
151 28         30 my $k = $zero; # +120
152 28         39 my $pow = $zero + 1; # inherit bignum 1
153              
154             # initial rotation from arm number
155 28         28 my $rot;
156             {
157 28         30 my $int = int($n);
  28         39  
158 28         41 $i = $n - $int; # frac, inherit possible BigFloat
159 28         29 $n = $int; # BigFloat int() gives BigInt, use that
160 28         62 $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         34 my $even = 1;
167 28         56 my @n = digit_split_lowtohigh($n,3);
168 28         56 while (@n) {
169 77         89 my $digit = shift @n;
170             ### at: "$i, $j, $k even digit $digit"
171 77 100       123 if ($digit == 1) {
    100          
172 32         55 ($i,$j,$k) = ($pow-$j, -$k, $i); # rotate +120 and add
173             } elsif ($digit == 2) {
174 27         29 $j += $pow; # add rotated +60
175             }
176              
177 77 100       137 last unless @n;
178 65         78 $digit = shift @n;
179 65 100       105 if ($digit == 1) {
    100          
180 20         40 ($i,$j,$k) = ($pow+$k, $pow-$i, -$j); # rotate -120 and add
181             } elsif ($digit == 2) {
182 24         25 $i += $pow; # add * b
183 24         28 $k -= $pow;
184             }
185 65         119 $pow *= 3;
186             }
187              
188             ### final: "$i, $j, $k"
189             ### is: (2*$i + $j - $k).", ".($j+$k)
190              
191             ### $rot
192 28 100       55 if ($rot >= 3) {
193 1         3 ($i,$j,$k) = (-$i,-$j,-$k);
194 1         2 $rot -= 3;
195             }
196 28 100       55 if ($rot == 1) { ($i,$j,$k) = (-$k,$i,$j); } # rotate +60
  7 100       16  
197 3         5 elsif ($rot == 2) { ($i,$j,$k) = (-$j,-$k, $i); } # rotate +128
198              
199 28         84 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 71 return scalar((shift->xy_to_n_list(@_))[0]);
214             }
215             sub xy_to_n_list {
216 35     35 1 481 my ($self, $x,$y) = @_;
217             ### AlternateTerdragon xy_to_n_list(): "$x, $y"
218              
219 35         67 $x = round_nearest($x);
220 35         57 $y = round_nearest($y);
221             {
222             # nothing at an odd point, and trap overflows in $x+$y dividing out b
223 35         43 my $sum = abs($x) + abs($y);
  35         45  
224 35 50       63 if (is_infinite($sum)) { return $sum; } # infinity
  0         0  
225 35 50       75 if ($sum % 2) { return; }
  0         0  
226             }
227              
228 35 100 66     71 if ($x==0 && $y==0) {
229 9         28 return 0 .. $self->{'arms'}-1;
230             }
231              
232 26         61 my $arms_count = $self->arms_count;
233 26         31 my $zero = ($x * 0 * $y); # inherit bignum 0
234              
235 26         38 my @n_list;
236 26         47 foreach my $d (0,1,2) {
237 78         117 my ($ndigits,$arm) = _xy_d_to_ndigits_and_arm($x,$y,$d);
238 78 100       116 next if $arm >= $arms_count;
239 75 100       137 if ($arm & 1) {
240             ### flip ...
241 22         30 @$ndigits = map {2-$_} @$ndigits;
  81         109  
242             }
243 75         141 push @n_list,
244             digit_join_lowtohigh($ndigits, 3, $zero) * $arms_count + $arm;
245             }
246              
247             ### unsorted n_list: @n_list
248 26         75 return sort {$a<=>$b} @n_list;
  66         189  
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 78     78   105 my ($x,$y, $d) = @_;
258             ### _xy_d_to_ndigits_and_arm(): "$x,$y d=$d"
259 78         94 my @ndigits;
260             my $arm;
261 78         85 for (;;) {
262             ### at: "$x,$y d=$d"
263 958 100 100     852 if ($x==0 && $y==0) { $arm = 2*$d; last; }
  53         59  
  53         64  
264 905 100 100     837 if ($d==2 && $x==1 && $y==1) { $arm = 1; last; }
  15   100     33  
  15         18  
265 890 100 100     804 if ($d==0 && $x==-2 && $y==0) { $arm = 3; last; }
  3   100     5  
  3         3  
266 887 100 100     815 if ($d==1 && $x==1 && $y==-1) { $arm = 5; last; }
  7   100     11  
  7         9  
267 880         501 my $a = $x % 3; # z mod b = -x mod 3
268 880 100       582 if ($a) { $a = 3-$a; }
  325         345  
269 880         560 push @ndigits, $a;
270              
271 880 100       627 if ($a==1) { $d = ($d-1) % 3; }
  183         211  
272             ### a: $a
273             ### new d: $d
274              
275 880         542 $x -= $digit_to_x[$d]->[$a];
276 880         509 $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 880         3552 ($x,$y) = (($x+$y)/2, # divide b = w6+1
285             ($y-$x/3)/2);
286              
287 880         483 $y = -$y;
288 880         490 $d = (-$d) % 3;
289             }
290 78 100       120 if (scalar(@ndigits) & 1) { $arm = (6-$arm) % 6; }
  38         44  
291             ### $arm
292             ### @ndigits
293 78         141 return (\@ndigits, $arm);
294             }
295              
296             # not exact
297             sub rect_to_n_range {
298 20     20 1 1415 my ($self, $x1,$y1, $x2,$y2) = @_;
299             ### AlternateTerdragon rect_to_n_range(): "$x1,$y1 $x2,$y2"
300 20         46 my $xmax = int(max(abs($x1),abs($x2)));
301 20         40 my $ymax = int(max(abs($y1),abs($y2)));
302             return (0,
303             ($xmax*$xmax + 3*$ymax*$ymax + 1)
304             * 2
305 20         56 * $self->{'arms'});
306             }
307              
308             my @digit_to_nextturn = (2,-2);
309             sub n_to_dxdy {
310 4     4 1 405 my ($self, $n) = @_;
311             ### AlternateTerdragon n_to_dxdy(): $n
312              
313 4 50       8 if ($n < 0) {
314 0         0 return; # first direction at N=0
315             }
316 4 50       10 if (is_infinite($n)) {
317 0         0 return ($n,$n);
318             }
319              
320 4         8 my $int = int($n); # integer part
321 4         7 $n -= $int; # fraction part
322              
323             # initial direction from arm
324 4         9 my $dir6 = _divrem_mutate ($int, $self->{'arms'});
325              
326 4         10 my @ndigits = digit_split_lowtohigh($int,3);
327 4         8 foreach my $i (0 .. $#ndigits) {
328 2 100       6 if ($ndigits[$i] == 1) {
329 1 50       4 $dir6 += 2*($i&1 ? -1 : 1); # count 1s for total turn
330             }
331             }
332 4         6 $dir6 %= 6;
333 4         4 my $dx = $dir6_to_dx[$dir6];
334 4         8 my $dy = $dir6_to_dy[$dir6];
335              
336 4 100       6 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         5 my $above = scalar(@ndigits);
341 3         6 foreach my $i (0 .. $#ndigits) {
342 2 100       3 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         4 $dx += $n*($dir6_to_dx[$dir6] - $dx);
354 3         7 $dy += $n*($dir6_to_dy[$dir6] - $dy);
355             }
356 4         10 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__