File Coverage

blib/lib/Math/PlanePath/AlternateTerdragon.pm
Criterion Covered Total %
statement 175 214 81.7
branch 48 72 66.6
condition 25 26 96.1
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, 2019, 2020 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   1497 use 5.004;
  1         4  
21 1     1   6 use strict;
  1         2  
  1         24  
22 1     1   5 use List::Util 'first';
  1         2  
  1         95  
23 1     1   7 use List::Util 'min'; # 'max'
  1         2  
  1         63  
24             *max = \&Math::PlanePath::_max;
25              
26 1     1   788 use Math::PlanePath;
  1         3  
  1         47  
27             *_divrem_mutate = \&Math::PlanePath::_divrem_mutate;
28              
29             use Math::PlanePath::Base::Generic
30 1         48 'is_infinite',
31             'round_nearest',
32 1     1   6 'xy_is_even';
  1         2  
33             use Math::PlanePath::Base::Digits
34 1         63 'digit_split_lowtohigh',
35             'digit_join_lowtohigh',
36 1     1   582 'round_up_pow';
  1         2  
37              
38 1     1   7 use vars '$VERSION', '@ISA';
  1         2  
  1         55  
39             $VERSION = 129;
40             @ISA = ('Math::PlanePath');
41              
42 1     1   714 use Math::PlanePath::TerdragonMidpoint;
  1         3  
  1         34  
43              
44             # uncomment this to run the ### lines
45             # use Smart::Comments;
46              
47              
48 1     1   8 use constant n_start => 0;
  1         1  
  1         70  
49 1         231 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   6 } ];
  1         2  
60              
61             sub x_negative {
62 2     2 1 117 my ($self) = @_;
63 2         12 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   7 use constant dx_maximum => 2;
  1         2  
  1         49  
84 1     1   5 use constant dy_minimum => -1;
  1         6  
  1         55  
85 1     1   7 use constant dy_maximum => 1;
  1         1  
  1         242  
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   7 use constant absdx_minimum => 1;
  1         2  
  1         82  
112 1     1   7 use constant dsumxy_minimum => -2; # diagonals
  1         1  
  1         45  
113 1     1   5 use constant dsumxy_maximum => 2;
  1         2  
  1         62  
114 1     1   7 use constant ddiffxy_minimum => -2;
  1         13  
  1         61  
115 1     1   7 use constant ddiffxy_maximum => 2;
  1         1  
  1         104  
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         2  
  1         1505  
128              
129              
130             #------------------------------------------------------------------------------
131              
132             sub new {
133 20     20 1 2167 my $self = shift->SUPER::new(@_);
134 20   100     107 $self->{'arms'} = max(1, min(6, $self->{'arms'} || 1));
135 20         41 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 2359 my ($self, $n) = @_;
143             ### AlternateTerdragon n_to_xy(): $n
144              
145 28 50       70 if ($n < 0) { return; }
  0         0  
146 28 50       80 if (is_infinite($n)) { return ($n, $n); }
  0         0  
147              
148 28         53 my $zero = ($n * 0); # inherit bignum 0
149 28         42 my $i; # X
150 28         38 my $j = $zero; # +60
151 28         38 my $k = $zero; # +120
152 28         41 my $pow = $zero + 1; # inherit bignum 1
153              
154             # initial rotation from arm number
155 28         37 my $rot;
156             {
157 28         45 my $int = int($n);
  28         41  
158 28         41 $i = $n - $int; # frac, inherit possible BigFloat
159 28         38 $n = $int; # BigFloat int() gives BigInt, use that
160 28         73 $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         47 my $even = 1;
167 28         73 my @n = digit_split_lowtohigh($n,3);
168 28         61 while (@n) {
169 77         120 my $digit = shift @n;
170             ### at: "$i, $j, $k even digit $digit"
171 77 100       149 if ($digit == 1) {
    100          
172 25         48 ($i,$j,$k) = ($pow-$j, -$k, $i); # rotate +120 and add
173             } elsif ($digit == 2) {
174 25         34 $j += $pow; # add rotated +60
175             }
176              
177 77 100       142 last unless @n;
178 59         93 $digit = shift @n;
179 59 100       111 if ($digit == 1) {
    100          
180 24         56 ($i,$j,$k) = ($pow+$k, $pow-$i, -$j); # rotate -120 and add
181             } elsif ($digit == 2) {
182 19         30 $i += $pow; # add * b
183 19         23 $k -= $pow;
184             }
185 59         108 $pow *= 3;
186             }
187              
188             ### final: "$i, $j, $k"
189             ### is: (2*$i + $j - $k).", ".($j+$k)
190              
191             ### $rot
192 28 50       57 if ($rot >= 3) {
193 0         0 ($i,$j,$k) = (-$i,-$j,-$k);
194 0         0 $rot -= 3;
195             }
196 28 100       64 if ($rot == 1) { ($i,$j,$k) = (-$k,$i,$j); } # rotate +60
  6 100       14  
197 3         7 elsif ($rot == 2) { ($i,$j,$k) = (-$j,-$k, $i); } # rotate +128
198              
199 28         102 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 87 return scalar((shift->xy_to_n_list(@_))[0]);
214             }
215             sub xy_to_n_list {
216 35     35 1 614 my ($self, $x,$y) = @_;
217             ### AlternateTerdragon xy_to_n_list(): "$x, $y"
218              
219 35         90 $x = round_nearest($x);
220 35         69 $y = round_nearest($y);
221             {
222             # nothing at an odd point, and trap overflows in $x+$y dividing out b
223 35         55 my $sum = abs($x) + abs($y);
  35         55  
224 35 50       68 if (is_infinite($sum)) { return $sum; } # infinity
  0         0  
225 35 50       96 if ($sum % 2) { return; }
  0         0  
226             }
227              
228 35 100 66     93 if ($x==0 && $y==0) {
229 11         39 return 0 .. $self->{'arms'}-1;
230             }
231              
232 24         62 my $arms_count = $self->arms_count;
233 24         41 my $zero = ($x * 0 * $y); # inherit bignum 0
234              
235 24         40 my @n_list;
236 24         45 foreach my $d (0,1,2) {
237 72         142 my ($ndigits,$arm) = _xy_d_to_ndigits_and_arm($x,$y,$d);
238 72 100       188 next if $arm >= $arms_count;
239 68 100       119 if ($arm & 1) {
240             ### flip ...
241 18         33 @$ndigits = map {2-$_} @$ndigits;
  68         112  
242             }
243 68         174 push @n_list,
244             digit_join_lowtohigh($ndigits, 3, $zero) * $arms_count + $arm;
245             }
246              
247             ### unsorted n_list: @n_list
248 24         91 return sort {$a<=>$b} @n_list;
  58         212  
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 72     72   120 my ($x,$y, $d) = @_;
258             ### _xy_d_to_ndigits_and_arm(): "$x,$y d=$d"
259 72         103 my @ndigits;
260             my $arm;
261 72         132 for (;;) {
262             ### at: "$x,$y d=$d"
263 912 100 100     975 if ($x==0 && $y==0) { $arm = 2*$d; last; }
  51         74  
  51         74  
264 861 100 100     936 if ($d==2 && $x==1 && $y==1) { $arm = 1; last; }
  10   100     13  
  10         16  
265 851 100 100     943 if ($d==0 && $x==-2 && $y==0) { $arm = 3; last; }
  3   100     5  
  3         5  
266 848 100 100     921 if ($d==1 && $x==1 && $y==-1) { $arm = 5; last; }
  8   100     9  
  8         15  
267 840         584 my $a = $x % 3; # z mod b = -x mod 3
268 840 100       670 if ($a) { $a = 3-$a; }
  296         398  
269 840         679 push @ndigits, $a;
270              
271 840 100       754 if ($a==1) { $d = ($d-1) % 3; }
  173         239  
272             ### a: $a
273             ### new d: $d
274              
275 840         573 $x -= $digit_to_x[$d]->[$a];
276 840         581 $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 840         4121 ($x,$y) = (($x+$y)/2, # divide b = w6+1
285             ($y-$x/3)/2);
286              
287 840         556 $y = -$y;
288 840         572 $d = (-$d) % 3;
289             }
290 72 100       152 if (scalar(@ndigits) & 1) { $arm = (6-$arm) % 6; }
  46         110  
291             ### $arm
292             ### @ndigits
293 72         185 return (\@ndigits, $arm);
294             }
295              
296             # not exact
297             sub rect_to_n_range {
298 20     20 1 1654 my ($self, $x1,$y1, $x2,$y2) = @_;
299             ### AlternateTerdragon rect_to_n_range(): "$x1,$y1 $x2,$y2"
300 20         59 my $xmax = int(max(abs($x1),abs($x2)));
301 20         45 my $ymax = int(max(abs($y1),abs($y2)));
302             return (0,
303             ($xmax*$xmax + 3*$ymax*$ymax + 1)
304             * 2
305 20         66 * $self->{'arms'});
306             }
307              
308             my @digit_to_nextturn = (2,-2);
309             sub n_to_dxdy {
310 4     4 1 484 my ($self, $n) = @_;
311             ### AlternateTerdragon n_to_dxdy(): $n
312              
313 4 50       10 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         12 my $dir6 = _divrem_mutate ($int, $self->{'arms'});
325              
326 4         10 my @ndigits = digit_split_lowtohigh($int,3);
327 4         11 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         7 $dir6 %= 6;
333 4         7 my $dx = $dir6_to_dx[$dir6];
334 4         6 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         7 $dx += $n*($dir6_to_dx[$dir6] - $dx);
354 3         6 $dy += $n*($dir6_to_dy[$dir6] - $dy);
355             }
356 4         14 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__