——# Copyright 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020 Kevin Ryde
# This file is part of Math-PlanePath.
#
# Math-PlanePath is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by the
# Free Software Foundation; either version 3, or (at your option) any later
# version.
#
# Math-PlanePath is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
# for more details.
#
# You should have received a copy of the GNU General Public License along
# with Math-PlanePath. If not, see <http://www.gnu.org/licenses/>.
# Kanga "Number Mosaics" rotated to
#
# ...-16---15
# \
# 6----5 14
# / \ \
# 7 1 4 13
# / / / /
# 8 2----3 12
# \ /
# 9---10---11
#
#
# Could go pointy end with same loop/step, or point to the right
#
# 13--12--11
# / |
# 14 4---3 10
# / / | |
# 15 5 1---2 9
# \ \ |
# 16 6---7---8
# \ |
# 17--18--19--20
#
package
Math::PlanePath::HexSpiral;
use
5.004;
use
strict;
#use List::Util 'max';
*max
= \
&Math::PlanePath::_max
;
$VERSION
= 129;
use
Math::PlanePath;
*_sqrtint
= \
&Math::PlanePath::_sqrtint
;
@ISA
= (
'Math::PlanePath'
);
'round_nearest'
,
'xy_is_even'
;
# uncomment this to run the ### lines
#use Devel::Comments '###';
*parameter_info_array
= \
&Math::PlanePath::SquareSpiral::parameter_info_array
;
# 2w+3 --- 3w/2+3 -- w+4
# / \
# 2w+4 0 -------- w+3 *
# \ /
# 2w+5 ----------------- 3w+7 w=2; 1+3*w+7=14
# ^
# X=0
sub
x_negative_at_n {
my
(
$self
) =
@_
;
return
$self
->n_start + (
$self
->{
'wider'
} ? 0 : 3);
}
sub
y_negative_at_n {
my
(
$self
) =
@_
;
return
$self
->n_start + 2
*$self
->{
'wider'
} + 5;
}
sub
_UNDOCUMENTED__dxdy_list_at_n {
my
(
$self
) =
@_
;
return
$self
->n_start + 3
*$self
->{
'wider'
} + 7;
}
sub
rsquared_minimum {
my
(
$self
) =
@_
;
return
(
$self
->{
'wider'
} % 2
? 1
# odd "wider" minimum X=1,Y=0
: 0);
# even "wider" includes X=0,Y=0
}
*sumabsxy_minimum
= \
&rsquared_minimum
;
*_UNDOCUMENTED__dxdy_list
= \
&Math::PlanePath::_UNDOCUMENTED__dxdy_list_six
;
*absdiffxy_minimum
= \
&rsquared_minimum
;
sub
_UNDOCUMENTED__turn_any_left_at_n {
my
(
$self
) =
@_
;
return
$self
->n_start +
$self
->{
'wider'
} + 1;
}
#------------------------------------------------------------------------------
sub
new {
my
$self
=
shift
->SUPER::new (
@_
);
# parameters
$self
->{
'wider'
} ||= 0;
# default
if
(!
defined
$self
->{
'n_start'
}) {
$self
->{
'n_start'
} =
$self
->default_n_start;
}
return
$self
;
}
# wider==0
# diagonal down and to the left
# d = [ 0, 1, 2, 3 ]
# N = [ 1, 6, 17, 34 ]
# N = (3*$d**2 + 2*$d + 1)
# d = -1/3 + sqrt(1/3 * $n + -2/9)
# = (-1 + sqrt(3*$n - 2)) / 3
#
# wider==1
# diagonal down and to the left
# d = [ 0, 1, 2, 3 ]
# N = [ 1, 8, 21, 40 ]
# N = (3*$d**2 + 4*$d + 1)
# d = -2/3 + sqrt(1/3 * $n + 1/9)
# = (-2 + sqrt(3*$n + 1)) / 3
#
# wider==2
# diagonal down and to the left
# d = [ 0, 1, 2, 3, 4 ]
# N = [ 1, 10, 25, 46, 73 ]
# N = (3*$d**2 + 6*$d + 1)
# d = -1 + sqrt(1/3 * $n + 2/3)
# = (-3 + sqrt(3*$n + 6)) / 3
#
# N = 3*$d*$d + (2+2*$w)*$d + 1
# = (3*$d + 2 + 2*$w)*$d + 1
# d = (-1-w + sqrt(3*$n + ($w+2)*$w - 2)) / 3
# = (sqrt(3*$n + ($w+2)*$w - 2) -1-w) / 3
sub
n_to_xy {
my
(
$self
,
$n
) =
@_
;
#### n_to_xy: "$n wider=$self->{'wider'}"
$n
=
$n
-
$self
->{
'n_start'
};
# N=0 basis
if
(
$n
< 0) {
return
; }
my
$w
=
$self
->{
'wider'
};
my
$d
=
int
((_sqrtint(3
*$n
+ (
$w
+2)
*$w
+ 1) - 1 -
$w
) / 3);
#### d frac: ((_sqrtint(3*$n + ($w+2)*$w + 1) - 1 - $w) / 3)
#### $d
$n
+= 1;
# N=1 basis
$n
-= (3
*$d
+ 2 + 2
*$w
)
*$d
+ 1;
#### remainder: $n
$d
=
$d
+ 1;
# no warnings if $d==inf
if
(
$n
<=
$d
+
$w
) {
#### bottom horizontal
$d
= -
$d
+ 1;
return
(2
*$n
+
$d
-
$w
,
$d
);
}
$n
-=
$d
+
$w
;
if
(
$n
<=
$d
-1) {
#### right lower diagonal, being 1 shorter: $n
return
(
$n
+
$d
+ 1 +
$w
,
$n
-
$d
+ 1);
}
$n
-=
$d
-1;
if
(
$n
<=
$d
) {
#### right upper diagonal: $n
return
(-
$n
+ 2
*$d
+
$w
,
$n
);
}
$n
-=
$d
;
if
(
$n
<=
$d
+
$w
) {
#### top horizontal
return
(-2
*$n
+
$d
+
$w
,
$d
);
}
$n
-=
$d
+
$w
;
if
(
$n
<=
$d
) {
#### left upper diagonal
return
(-
$n
-
$d
-
$w
,
-
$n
+
$d
);
}
#### left lower diagonal
$n
-=
$d
;
return
(
$n
- 2
*$d
-
$w
,
-
$n
);
}
sub
xy_is_visited {
my
(
$self
,
$x
,
$y
) =
@_
;
return
xy_is_even(
$self
,
$x
+
$self
->{
'wider'
},
$y
);
}
sub
xy_to_n {
my
(
$self
,
$x
,
$y
) =
@_
;
### xy_to_n(): "$x, $y"
$x
= round_nearest (
$x
);
$y
= round_nearest (
$y
);
my
$w
=
$self
->{
'wider'
};
if
((
$x
^
$y
^
$w
) & 1) {
return
undef
;
# nothing on odd squares
}
my
$ay
=
abs
(
$y
);
my
$ax
=
abs
(
$x
) -
$w
;
if
(
$ax
>
$ay
) {
my
$d
= (
$ax
+
$ay
)/2;
# x+y is even
if
(
$x
> 0) {
### right ends
### $d
return
((3
*$d
- 2 + 2
*$w
)
*$d
-
$w
# horizontal to the right
+
$y
# offset up or down
+
$self
->{
'n_start'
});
}
else
{
### left ends
return
((3
*$d
+ 1 + 2
*$w
)
*$d
# horizontal to the left
-
$y
# offset up or down
+
$self
->{
'n_start'
});
}
}
else
{
my
$d
=
$ay
;
if
(
$y
> 0) {
### top horizontal
### $d
return
((3
*$d
+ 2
*$w
)
*$d
# diagonal up to the left
+ (-
$d
-
$x
-
$w
) / 2
# negative offset rightwards
+
$self
->{
'n_start'
});
}
else
{
### bottom horizontal, and centre horizontal
### $d
### offset: $d
return
((3
*$d
+ 2 + 2
*$w
)
*$d
# diagonal down to the left
+ (
$x
+
$w
+
$d
)/2
# offset rightwards
+
$self
->{
'n_start'
});
}
}
}
# not exact
sub
rect_to_n_range {
my
(
$self
,
$x1
,
$y1
,
$x2
,
$y2
) =
@_
;
### HexSpiral rect_to_n_range(): $x1,$y1, $x2,$y2
my
$w
=
$self
->{
'wider'
};
# symmetric in +/-y, and biggest y is biggest n
my
$y
= max (
abs
(
$y1
),
abs
(
$y2
));
# symmetric in +/-x, and biggest x
my
$x
= max (
abs
(
$x1
),
abs
(
$x2
));
if
(
$x
>=
$w
) {
$x
-=
$w
;
}
# in the middle horizontal path parts y determines the loop number
# in the end parts diagonal distance, 2 apart
my
$d
= (
$y
>=
$x
?
$y
# middle
: (
$x
+
$y
+ 1)/2);
# ends
$d
=
int
(
$d
) + 1;
# diagonal downwards bottom left being the end of a revolution
# s=0
# s=1 n=7
# s=2 n=19
# s=3 n=37
# s=4 n=61
# n = 3*$d*$d + 3*$d + 1
#
# ### gives: "sum $d is " . (3*$d*$d + 3*$d + 1)
# ENHANCE-ME: find actual minimum if rect doesn't cover 0,0
return
(
$self
->{
'n_start'
},
(3
*$d
+ 3 + 2
*$w
)
*$d
+
$self
->{
'n_start'
});
}
1;
__END__
=for stopwords PlanePath Ryde Math-PlanePath ie OEIS
=head1 NAME
Math::PlanePath::HexSpiral -- integer points around a hexagonal spiral
=head1 SYNOPSIS
use Math::PlanePath::HexSpiral;
my $path = Math::PlanePath::HexSpiral->new;
my ($x, $y) = $path->n_to_xy (123);
=head1 DESCRIPTION
This path makes a hexagonal spiral, with points spread out horizontally to
fit on a square grid.
28 -- 27 -- 26 -- 25 3
/ \
29 13 -- 12 -- 11 24 2
/ / \ \
30 14 4 --- 3 10 23 1
/ / / \ \ \
31 15 5 1 --- 2 9 22 <- Y=0
\ \ \ / /
32 16 6 --- 7 --- 8 21 -1
\ \ /
33 17 -- 18 -- 19 -- 20 -2
\
34 -- 35 ... -3
^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^
-6 -5 -4 -3 -2 -1 X=0 1 2 3 4 5 6
Each horizontal gap is 2, so for instance n=1 is at X=0,Y=0 then n=2 is at
X=2,Y=0. The diagonals are just 1 across, so n=3 is at X=1,Y=1. Each
alternate row is offset from the one above or below. The result is a
triangular lattice per L<Math::PlanePath/Triangular Lattice>.
The octagonal numbers 8,21,40,65, etc 3*k^2-2*k fall on a horizontal
straight line at Y=-1. In general straight lines are 3*k^2 + b*k + c.
A plain 3*k^2 goes diagonally up to the left, then b is a 1/6 turn
anti-clockwise, or clockwise if negative. So b=1 goes horizontally to the
left, b=2 diagonally down to the left, b=3 diagonally down to the right,
etc.
=head2 Wider
An optional C<wider> parameter makes the path wider, stretched along the top
and bottom horizontals. For example
$path = Math::PlanePath::HexSpiral->new (wider => 2);
gives
... 36----35 3
\
21----20----19----18----17 34 2
/ \ \
22 8---- 7---- 6---- 5 16 33 1
/ / \ \ \
23 9 1---- 2---- 3---- 4 15 32 <- Y=0
\ \ / /
24 10----11----12----13----14 31 -1
\ /
25----26----27----28---29----30 -2
^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^
-7 -6 -5 -4 -3 -2 -1 X=0 1 2 3 4 5 6 7
The centre horizontal from N=1 is extended by C<wider> many extra places,
then the path loops around that shape. The starting point N=1 is shifted to
the left by wider many places to keep the spiral centred on the origin
X=0,Y=0. Each horizontal gap is still 2.
Each loop is still 6 longer than the previous, since the widening is
basically a constant amount added into each loop.
=head2 N Start
The default is to number points starting N=1 as shown above. An optional
C<n_start> can give a different start with the same shape etc. For example
to start at 0,
=cut
# math-image --path=HexSpiral,n_start=0 --all --output=numbers --size=70x9
=pod
n_start => 0
27 26 25 24 3
28 12 11 10 23 2
29 13 3 2 9 22 1
30 14 4 0 1 8 21 <- Y=0
31 15 5 6 7 20 ... -1
32 16 17 18 19 38 -2
33 34 35 36 37 -3
^
-6 -5 -4 -3 -2 -1 X=0 1 2 3 4 5 6
In this numbering the X axis N=0,1,8,21,etc is the octagonal numbers
3*X*(X+1).
=head1 FUNCTIONS
See L<Math::PlanePath/FUNCTIONS> for behaviour common to all path classes.
=over 4
=item C<$path = Math::PlanePath::HexSpiral-E<gt>new ()>
=item C<$path = Math::PlanePath::HexSpiral-E<gt>new (wider =E<gt> $w)>
Create and return a new hex spiral object. An optional C<wider> parameter
widens the path, it defaults to 0 which is no widening.
=item C<($x,$y) = $path-E<gt>n_to_xy ($n)>
Return the X,Y coordinates of point number C<$n> on the path.
For C<$n < 1> the return is an empty list, it being considered the path
starts at 1.
=item C<$n = $path-E<gt>xy_to_n ($x,$y)>
Return the point number for coordinates C<$x,$y>. C<$x> and C<$y> are
each rounded to the nearest integer, which has the effect of treating each
C<$n> in the path as a square of side 1.
Only every second square in the plane has an N, being those where X,Y both
odd or both even. If C<$x,$y> is a position without an N, ie. one of X,Y
odd the other even, then the return is C<undef>.
=back
=head1 OEIS
Entries in Sloane's Online Encyclopedia of Integer Sequences related to
this path include
=over
L<http://oeis.org/A056105> (etc)
=back
A328818 X coordinate
A307012 Y coordinate
A307011 (X-Y)/2
A307013 (X+Y)/2
A056105 N on X axis
A056106 N on X=Y diagonal
A056107 N on North-West diagonal
A056108 N on negative X axis
A056109 N on South-West diagonal
A003215 N on South-East diagonal
A063178 total sum N previous row or diagonal
A135711 boundary length of N hexagons
A135708 grid sticks of N hexagons
n_start=0
A001399 N positions of turns (extra initial 1)
A000567 N on X axis, octagonal numbers
A049451 N on X negative axis
A049450 N on X=Y diagonal north-east
A033428 N on north-west diagonal, 3*k^2
A045944 N on south-west diagonal, octagonal numbers second kind
A063436 N on WSW slope dX=-3,dY=-1
A028896 N on south-east diagonal
=head1 SEE ALSO
L<Math::PlanePath>,
L<Math::PlanePath::HexSpiralSkewed>,
L<Math::PlanePath::HexArms>,
L<Math::PlanePath::TriangleSpiral>,
L<Math::PlanePath::TriangularHypot>
=head1 HOME PAGE
=head1 LICENSE
Copyright 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020 Kevin Ryde
This file is part of Math-PlanePath.
Math-PlanePath is free software; you can redistribute it and/or modify it
under the terms of the GNU General Public License as published by the Free
Software Foundation; either version 3, or (at your option) any later
version.
Math-PlanePath is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
more details.
You should have received a copy of the GNU General Public License along with
Math-PlanePath. If not, see <http://www.gnu.org/licenses/>.
=cut