File Coverage

blib/lib/Math/PlanePath/SierpinskiCurve.pm
Criterion Covered Total %
statement 159 236 67.3
branch 56 102 54.9
condition 27 35 77.1
subroutine 23 38 60.5
pod 18 18 100.0
total 283 429 65.9


line stmt bran cond sub pod time code
1             # Copyright 2011, 2012, 2013, 2014, 2015, 2016, 2017, 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             # Sierpinski, "Sur Une Nouvelle Courbe Qui Remplit Toute Une Aire Plaine",
20             # Bull. Acad. Sci. Cracovie Series A, 1912, pages 462-478.
21              
22              
23             package Math::PlanePath::SierpinskiCurve;
24 2     2   9489 use 5.004;
  2         16  
25 2     2   11 use strict;
  2         4  
  2         70  
26 2     2   11 use List::Util 'sum','first';
  2         4  
  2         273  
27             #use List::Util 'min','max';
28             *min = \&Math::PlanePath::_min;
29             *max = \&Math::PlanePath::_max;
30              
31 2     2   14 use vars '$VERSION', '@ISA';
  2         4  
  2         134  
32             $VERSION = 129;
33 2     2   744 use Math::PlanePath;
  2         5  
  2         131  
34             @ISA = ('Math::PlanePath');
35             *_divrem_mutate = \&Math::PlanePath::_divrem_mutate;
36              
37             use Math::PlanePath::Base::Generic
38 2         109 'is_infinite',
39 2     2   14 'round_nearest';
  2         4  
40             use Math::PlanePath::Base::Digits
41 2         138 'round_up_pow',
42             'round_down_pow',
43 2     2   494 'digit_split_lowtohigh';
  2         5  
44              
45             # uncomment this to run the ### lines
46             # use Smart::Comments;
47              
48              
49 2     2   15 use constant n_start => 0;
  2         3  
  2         379  
50              
51             sub x_negative {
52 8     8 1 31 my ($self) = @_;
53 8         27 return ($self->{'arms'} >= 3);
54             }
55             sub y_negative {
56 8     8 1 16 my ($self) = @_;
57 8         28 return ($self->{'arms'} >= 5);
58             }
59              
60 2         448 use constant parameter_info_array =>
61             [
62             { name => 'arms',
63             share_key => 'arms_8',
64             display => 'Arms',
65             type => 'integer',
66             minimum => 1,
67             maximum => 8,
68             default => 1,
69             width => 1,
70             description => 'Arms',
71             },
72              
73             { name => 'straight_spacing',
74             display => 'Straight Spacing',
75             type => 'integer',
76             minimum => 1,
77             default => 1,
78             width => 1,
79             description => 'Spacing of the straight line points.',
80             },
81             { name => 'diagonal_spacing',
82             display => 'Diagonal Spacing',
83             type => 'integer',
84             minimum => 1,
85             default => 1,
86             width => 1,
87             description => 'Spacing of the diagonal points.',
88             },
89 2     2   15 ];
  2         3  
90              
91             # Ntop = (4^level)/2 - 1
92             # Xtop = 3*2^(level-1) - 1
93             # fill = Ntop / (Xtop*(Xtop-1)/2)
94             # -> 2 * ((4^level)/2 - 1) / (3*2^(level-1) - 1)^2
95             # -> 2 * ((4^level)/2) / (3*2^(level-1))^2
96             # = 4^level / (9*4^(level-1)
97             # = 4/9 = 0.444
98              
99             sub x_negative_at_n {
100 0     0 1 0 my ($self) = @_;
101 0 0       0 return $self->arms_count >= 3 ? 2 : undef;
102             }
103             sub y_negative_at_n {
104 0     0 1 0 my ($self) = @_;
105 0 0       0 return $self->arms_count >= 5 ? 4 : undef;
106             }
107              
108             {
109             # Note: shared by Math::PlanePath::SierpinskiCurveStair
110             my @x_minimum = (undef,
111             1, # 1 arm
112             0, # 2 arms
113             ); # more than 2 arm, X goes negative
114             sub x_minimum {
115 0     0 1 0 my ($self) = @_;
116 0         0 return $x_minimum[$self->arms_count];
117             }
118             }
119             {
120             # Note: shared by Math::PlanePath::SierpinskiCurveStair
121             my @sumxy_minimum = (undef,
122             1, # 1 arm, octant and X>=1 so X+Y>=1
123             1, # 2 arms, X>=1 or Y>=1 so X+Y>=1
124             0, # 3 arms, Y>=1 and X>=Y, so X+Y>=0
125             ); # more than 3 arm, Sum goes negative so undef
126             sub sumxy_minimum {
127 0     0 1 0 my ($self) = @_;
128 0         0 return $sumxy_minimum[$self->arms_count];
129             }
130             }
131 2     2   16 use constant sumabsxy_minimum => 1;
  2         3  
  2         222  
132              
133             # Note: shared by Math::PlanePath::SierpinskiCurveStair
134             # Math::PlanePath::AlternatePaper
135             # Math::PlanePath::AlternatePaperMidpoint
136             sub diffxy_minimum {
137 0     0 1 0 my ($self) = @_;
138 0 0       0 return ($self->arms_count == 1
139             ? 1 # octant Y<=X-1 so X-Y>=1
140             : undef); # more than 1 arm, DiffXY goes negative
141             }
142 2     2   25 use constant absdiffxy_minimum => 1; # X=Y never occurs
  2         5  
  2         126  
143 2     2   23 use constant rsquared_minimum => 1; # minimum X=1,Y=0
  2         4  
  2         824  
144              
145             sub dx_minimum {
146 0     0 1 0 my ($self) = @_;
147             return - max($self->{'straight_spacing'},
148 0         0 $self->{'diagonal_spacing'});
149             }
150             *dy_minimum = \&dx_minimum;
151              
152             sub dx_maximum {
153 0     0 1 0 my ($self) = @_;
154             return max($self->{'straight_spacing'},
155 0         0 $self->{'diagonal_spacing'});
156             }
157             *dy_maximum = \&dx_maximum;
158              
159             sub _UNDOCUMENTED__dxdy_list {
160 0     0   0 my ($self) = @_;
161 0         0 my $s = $self->{'straight_spacing'};
162 0         0 my $d = $self->{'diagonal_spacing'};
163 0 0       0 return ($s,0, # E eight scaled
    0          
    0          
    0          
    0          
    0          
    0          
164             ($d ? ( $d, $d) : ()), # NE except s=0
165             ($s ? ( 0, $s) : ()), # N or d=0 skips
166             ($d ? (-$d, $d) : ()), # NW
167             ($s ? (-$s, 0) : ()), # W
168             ($d ? (-$d,-$d) : ()), # SW
169             ($s ? ( 0,-$s) : ()), # S
170             ($d ? ( $d,-$d) : ())); # SE
171              
172             }
173             {
174             my @_UNDOCUMENTED__dxdy_list_at_n = (undef,
175             21, 20, 27, 36,
176             29, 12, 12, 13);
177             sub _UNDOCUMENTED__dxdy_list_at_n {
178 0     0   0 my ($self) = @_;
179 0         0 return $_UNDOCUMENTED__dxdy_list_at_n[$self->{'arms'}];
180             }
181             }
182             sub dsumxy_minimum {
183 0     0 1 0 my ($self) = @_;
184             return - max($self->{'straight_spacing'},
185 0         0 2*$self->{'diagonal_spacing'});
186             }
187             sub dsumxy_maximum {
188 0     0 1 0 my ($self) = @_;
189             return max($self->{'straight_spacing'},
190 0         0 2*$self->{'diagonal_spacing'});
191             }
192             *ddiffxy_minimum = \&dsumxy_minimum;
193             *ddiffxy_maximum = \&dsumxy_maximum;
194              
195 2     2   14 use constant dir_maximum_dxdy => (1,-1); # South-East
  2         4  
  2         118  
196 2     2   13 use constant turn_any_straight => 0; # never straight
  2         4  
  2         3581  
197              
198              
199             #------------------------------------------------------------------------------
200              
201             sub new {
202 256     256 1 9807 my $self = shift->SUPER::new(@_);
203              
204 256   100     1277 $self->{'arms'} = max(1, min(8, $self->{'arms'} || 1));
205 256   100     927 $self->{'straight_spacing'} ||= 1;
206 256   100     942 $self->{'diagonal_spacing'} ||= 1;
207 256         589 return $self;
208             }
209              
210             sub n_to_xy {
211 7496     7496 1 107281 my ($self, $n) = @_;
212             ### SierpinskiCurve n_to_xy(): $n
213              
214 7496 50       14425 if ($n < 0) {
215 0         0 return;
216             }
217 7496 50       14405 if (is_infinite($n)) {
218 0         0 return ($n,$n);
219             }
220              
221 7496         13411 my $int = int($n); # BigFloat int() gives BigInt, use that
222 7496         10615 $n -= $int; # preserve possible BigFloat
223             ### $int
224             ### $n
225              
226 7496         16575 my $arm = _divrem_mutate ($int, $self->{'arms'});
227              
228 7496         12219 my $s = $self->{'straight_spacing'};
229 7496         10662 my $d = $self->{'diagonal_spacing'};
230 7496         11086 my $base = 2*$d+$s;
231 7496         10574 my $x = my $y = ($int * 0); # inherit big 0
232 7496         10340 my $len = $x + $base; # inherit big
233              
234 7496         14945 foreach my $digit (digit_split_lowtohigh($int,4)) {
235             ### at: "$x,$y digit=$digit"
236              
237 13559 100       26857 if ($digit == 0) {
    100          
    100          
238 1741         2824 $x = $n*$d + $x;
239 1741         2420 $y = $n*$d + $y;
240 1741         2422 $n = 0;
241              
242             } elsif ($digit == 1) {
243 5757         11332 ($x,$y) = ($n*$s - $y + $len-$d-$s, # rotate +90
244             $x + $d);
245 5757         7642 $n = 0;
246              
247             } elsif ($digit == 2) {
248             # rotate -90
249 3652         8578 ($x,$y) = ($n*$d + $y + $len-$d,
250             -$n*$d - $x + $len-$d-$s);
251 3652         5065 $n = 0;
252              
253             } else { # digit==3
254 2409         3357 $x += $len;
255             }
256 13559         19895 $len *= 2;
257             }
258              
259             # n=0 or n=33..33
260 7496         11864 $x = $n*$d + $x;
261 7496         10129 $y = $n*$d + $y;
262              
263 7496         10126 $x += 1;
264 7496 100       13983 if ($arm & 1) {
265 3221         5752 ($x,$y) = ($y,$x); # mirror 45
266             }
267 7496 100       13709 if ($arm & 2) {
268 2773         5083 ($x,$y) = (-1-$y,$x); # rotate +90
269             }
270 7496 100       13838 if ($arm & 4) {
271 1808         2400 $x = -1-$x; # rotate 180
272 1808         2356 $y = -1-$y;
273             }
274              
275             # use POSIX 'floor';
276             # $x += floor($x/3);
277             # $y += floor($y/3);
278              
279             # $x += floor(($x-1)/3) + floor(($x-2)/3);
280             # $y += floor(($y-1)/3) + floor(($y-2)/3);
281              
282              
283             ### final: "$x,$y"
284 7496         18005 return ($x,$y);
285             }
286              
287             my @digit_to_dir = (0, -2, 2, 0);
288             my @dir8_to_dx = (1, 1, 0,-1, -1, -1, 0, 1);
289             my @dir8_to_dy = (0, 1, 1, 1, 0, -1, -1,-1);
290             my @digit_to_nextturn = (-1, # after digit=0
291             2, # digit=1
292             -1); # digit=2
293             sub n_to_dxdy {
294 0     0 1 0 my ($self, $n) = @_;
295             ### n_to_dxdy(): $n
296              
297 0 0       0 if ($n < 0) {
298 0         0 return; # first direction at N=0
299             }
300              
301 0         0 my $int = int($n);
302 0         0 $n -= $int;
303              
304 0         0 my $arm = _divrem_mutate($int,$self->{'arms'});
305 0         0 my $lowbit = _divrem_mutate($int,2);
306             ### $lowbit
307             ### $int
308              
309 0 0       0 if (is_infinite($int)) {
310 0         0 return ($int,$int);
311             }
312 0         0 my @ndigits = digit_split_lowtohigh($int,4);
313             ### @ndigits
314              
315 0         0 my $dir8 = sum(0, map {$digit_to_dir[$_]} @ndigits);
  0         0  
316 0 0       0 if ($arm & 1) {
317 0         0 $dir8 = - $dir8; # mirrored on second,fourth,etc arm
318             }
319 0         0 $dir8 += ($arm|1); # NE,NW,SW, or SE
320              
321 0         0 my $turn;
322 0 0 0     0 if ($n || $lowbit) {
323             # next turn
324              
325             # lowest non-3 digit, or zero if all 3s (implicit 0 above high digit)
326 0     0   0 $turn = $digit_to_nextturn[ first {$_!=3} @ndigits, 0 ];
  0         0  
327 0 0       0 if ($arm & 1) {
328 0         0 $turn = - $turn; # mirrored on second,fourth,etc arm
329             }
330             }
331              
332 0 0       0 if ($lowbit) {
333 0         0 $dir8 += $turn;
334             }
335              
336 0         0 my $s = $self->{'straight_spacing'};
337 0         0 my $d = $self->{'diagonal_spacing'};
338              
339 0         0 $dir8 &= 7;
340 0 0       0 my $spacing = ($dir8 & 1 ? $d : $s);
341 0         0 my $dx = $spacing * $dir8_to_dx[$dir8];
342 0         0 my $dy = $spacing * $dir8_to_dy[$dir8];
343              
344 0 0       0 if ($n) {
345 0         0 $dir8 += $turn;
346 0         0 $dir8 &= 7;
347 0 0       0 $spacing = ($dir8 & 1 ? $d : $s);
348 0         0 $dx += $n*($spacing * $dir8_to_dx[$dir8]
349             - $dx);
350 0         0 $dy += $n*($spacing * $dir8_to_dy[$dir8]
351             - $dy);
352             }
353              
354 0         0 return ($dx, $dy);
355             }
356              
357             # 2| . 3 .
358             # 1| 1 . 2
359             # 0| . 0 .
360             # +------
361             # 0 1 2
362             #
363             # 4| . . . 3 . # diagonal_spacing == 3
364             # 3| . . . . 2 4 # mod=2*3+1=7
365             # 2| . . . . . . .
366             # 1| 1 . . . . . . .
367             # 0| . 0 . . . . . . 6
368             # +------------------
369             # 0 1 2 3 4 5 6 7 8
370             #
371             sub _NOTWORKING__xy_is_visited {
372 0     0   0 my ($self, $x, $y) = @_;
373 0         0 $x = round_nearest($x);
374 0         0 $y = round_nearest($y);
375 0         0 my $mod = 2*$self->{'diagonal_spacing'} + $self->{'straight_spacing'};
376 0   0     0 return (_rect_within_arms($x,$y, $x,$y, $self->{'arms'})
377             && ((($x%$mod)+($y%$mod)) & 1));
378             }
379              
380             # x1 * x2 *
381             # +-----*-+y2*
382             # | *| *
383             # | * *
384             # | |* *
385             # | | **
386             # +-------+y1*
387             # ----------------
388             #
389             # arms=5 x1,y2 after X=Y-1 line, so x1 > y2-1, x1 >= y2
390             # ************
391             # x1 * x2
392             # +---*----+y2
393             # | * |
394             # | * |
395             # |* |
396             # * |
397             # *+--------+y1
398             # *
399             #
400             # arms=7 x1,y1 after X=-2-Y line, so x1 > -2-y1
401             # ************
402             # ** +------+
403             # * *| |
404             # * * |
405             # * |* |
406             # * | * |
407             # *y1+--*---+
408             # * x1 *
409             #
410             # _rect_within_arms() returns true if rectangle x1,y1,x2,y2 has some part
411             # within the extent of the $arms set of octants.
412             #
413             sub _rect_within_arms {
414 363     363   646 my ($x1,$y1, $x2,$y2, $arms) = @_;
415 363 100 66     1413 return ($arms <= 4
      66        
416             ? ($y2 >= 0 # y2 top edge must be positive
417             && ($arms <= 2
418             ? ($arms == 1 ? $x2 > $y1 # arms==1 bottom right
419             : $x2 >= 0) # arms==2 right edge
420             : ($arms == 4 # arms==4 anything
421             || $x2 >= -$y2))) # arms==3 top right
422              
423             # arms >= 5
424             : ($y2 >= 0 # y2 top edge positive is good, otherwise check
425             || ($arms <= 6
426             ? ($arms == 5 ? $x1 < $y2 # arms==5 top left
427             : $x1 < 0) # arms==6 left edge
428             : ($arms == 8 # arms==8 anything
429             || $x1 <= -2-$y1)))); # arms==7 bottom left
430             }
431              
432             sub xy_to_n {
433 57896     57896 1 268145 my ($self, $x, $y) = @_;
434             ### SierpinskiCurve xy_to_n(): "$x, $y"
435              
436 57896         105540 $x = round_nearest($x);
437 57896         108673 $y = round_nearest($y);
438              
439 57896         82902 my $arm = 0;
440 57896 100       99129 if ($y < 0) {
441 27203         35799 $arm = 4;
442 27203         37606 $x = -1-$x; # rotate -180
443 27203         35071 $y = -1-$y;
444             }
445 57896 100       96045 if ($x < 0) {
446 28816         38183 $arm += 2;
447 28816         47921 ($x,$y) = ($y, -1-$x); # rotate -90
448             }
449 57896 100       99931 if ($y > $x) { # second octant
450 25613         33116 $arm++;
451 25613         42324 ($x,$y) = ($y,$x); # mirror 45
452             }
453              
454 57896         85415 my $arms = $self->{'arms'};
455 57896 100       97082 if ($arm >= $arms) {
456 24000         44688 return undef;
457             }
458              
459 33896         44899 $x -= 1;
460 33896 100 100     95805 if ($x < 0 || $x < $y) {
461 4200         8567 return undef;
462             }
463             ### x adjust to zero: "$x,$y"
464             ### assert: $x >= 0
465             ### assert: $y >= 0
466              
467 29696         42780 my $s = $self->{'straight_spacing'};
468 29696         39340 my $d = $self->{'diagonal_spacing'};
469 29696         42049 my $base = (2*$d+$s);
470 29696   100     82393 my ($len,$level) = round_down_pow (($x+$y)/$base || 1, 2);
471             ### $level
472             ### $len
473 29696 50       59123 if (is_infinite($level)) {
474 0         0 return $level;
475             }
476              
477             # Xtop = 3*2^(level-1)-1
478             #
479 29696         51954 $len *= 2*$base;
480             ### initial len: $len
481              
482 29696         39828 my $n = 0;
483 29696         50249 foreach (0 .. $level) {
484 35817         49420 $n *= 4;
485             ### at: "loop=$_ len=$len x=$x,y=$y n=$n"
486             ### assert: $x >= 0
487             ### assert: $y >= 0
488              
489 35817         51361 my $len_sub_d = $len - $d;
490 35817 100       58478 if ($x < $len_sub_d) {
491             ### digit 0 or 1...
492 33055 100       57266 if ($x+$y+$s < $len) {
493             ### digit 0 ...
494             } else {
495             ### digit 1 ...
496 6602         11654 ($x,$y) = ($y-$d, $len-$s-$d-$x); # shift then rotate -90
497 6602         9222 $n += 1;
498             }
499             } else {
500 2762         3787 $x -= $len_sub_d;
501             ### digit 2 or 3 to: "x=$x y=$y"
502 2762 100       4402 if ($x < $y) { # before diagonal
503             ### digit 2...
504 1880         3535 ($x,$y) = ($len-$d-$s-$y, $x); # shift y-len then rotate +90
505 1880         2624 $n += 2;
506             } else {
507             #### digit 3...
508 882         1142 $x -= $d;
509 882         1181 $n += 3;
510             }
511 2762 100       4838 if ($x < 0) {
512 376         857 return undef;
513             }
514             }
515 35441         59011 $len /= 2;
516             }
517              
518             ### end at: "x=$x,y=$y n=$n"
519             ### assert: $x >= 0
520             ### assert: $y >= 0
521              
522 29320         39587 $n *= 4;
523 29320 100 100     128801 if ($y == 0 && $x == 0) {
    100 100        
    100 100        
    100 100        
524             ### final digit 0 ...
525             } elsif ($x == $d && $y == $d) {
526             ### final digit 1 ...
527 1765         2505 $n += 1;
528             } elsif ($x == $d+$s && $y == $d) {
529             ### final digit 2 ...
530 1608         2271 $n += 2;
531             } elsif ($x == $base && $y == 0) {
532             ### final digit 3 ...
533 1278         1996 $n += 3;
534             } else {
535 22874         51104 return undef;
536             }
537              
538 6446         14043 return $n*$arms + $arm;
539             }
540              
541             # not exact
542             sub rect_to_n_range {
543 363     363 1 5538 my ($self, $x1,$y1, $x2,$y2) = @_;
544             ### SierpinskiCurve rect_to_n_range(): "$x1,$y1 $x2,$y2"
545              
546 363         748 $x1 = round_nearest ($x1);
547 363         762 $x2 = round_nearest ($x2);
548 363         674 $y1 = round_nearest ($y1);
549 363         677 $y2 = round_nearest ($y2);
550 363 50       672 ($x1,$x2) = ($x2,$x1) if $x1 > $x2;
551 363 100       617 ($y1,$y2) = ($y2,$y1) if $y1 > $y2;
552              
553 363         577 my $arms = $self->{'arms'};
554 363 100       637 unless (_rect_within_arms($x1,$y1, $x2,$y2, $arms)) {
555             ### rect outside octants, for arms: $arms
556 9         24 return (1,0);
557             }
558              
559 354         641 my $max = ($x2 + $y2);
560 354 100       673 if ($arms >= 3) {
561 307         709 _apply_max ($max, -1-$x1 + $y2);
562              
563 307 100       528 if ($arms >= 5) {
564 237         506 _apply_max ($max, -1-$x1 - $y1-1);
565              
566 237 100       445 if ($arms >= 7) {
567 135         219 _apply_max ($max, $x2 - $y1-1);
568             }
569             }
570             }
571              
572             # base=2d+s
573             # level begins at
574             # base*(2^level-1)-s = X+Y ... maybe
575             # base*2^level = X+base
576             # 2^level = (X+base)/base
577             # level = log2((X+base)/base)
578             # then
579             # Nlevel = 4^level-1
580              
581 354         584 my $base = 2 * $self->{'diagonal_spacing'} + $self->{'straight_spacing'};
582 354         970 my ($power) = round_down_pow (int(($max+$base-2)/$base),
583             2);
584 354         855 return (0, 4*$power*$power * $arms - 1);
585             }
586              
587             sub _apply_max {
588             ### _apply_max(): "$_[0] cf $_[1]"
589 679 100   679   1192 unless ($_[0] > $_[1]) {
590 273         376 $_[0] = $_[1];
591             }
592             }
593              
594             #------------------------------------------------------------------------------
595              
596             sub level_to_n_range {
597 9     9 1 620 my ($self, $level) = @_;
598 9         30 return (0, 4**$level * $self->{'arms'} - 1);
599             }
600             sub n_to_level {
601 0     0 1   my ($self, $n) = @_;
602 0 0         if ($n < 0) { return undef; }
  0            
603 0 0         if (is_infinite($n)) { return $n; }
  0            
604 0           $n = round_nearest($n);
605 0           _divrem_mutate ($n, $self->{'arms'});
606 0           my ($pow, $exp) = round_up_pow ($n+1, 4);
607 0           return $exp;
608             }
609              
610             #------------------------------------------------------------------------------
611             1;
612             __END__