Math-PlanePath
view release on metacpan or search on metacpan
lib/Math/PlanePath.pm view on Meta::CPAN
equilateral triangle. That sqrt(3) factor can be applied if desired,
X, Y*sqrt(3) side length 2
X/2, Y*sqrt(3)/2 side length 1
Integer Y values have the advantage of fitting pixels on the usual kind of
raster computer screen, and not losing precision in floating point results.
If doing a general-purpose coordinate rotation then be sure to apply the
sqrt(3) scale factor before rotating or the result will be skewed. 60
degree rotations can be made within the integer X,Y coordinates directly as
follows, all giving integer X,Y results.
( X-3Y)/2, ( X+Y)/2 rotate +60 (anti-clockwise)
( X+3Y)/2, (-X+Y)/2 rotate -60 (clockwise)
(-X-3Y)/2, ( X-Y)/2 rotate +120
(-X+3Y)/2, (-X-Y)/2 rotate -120
-X,-Y rotate 180
(X+3Y)/2, (X-Y)/2 mirror across the X=3*Y twelfth (30deg)
=cut
# GP-DEFINE sqrt3i = quadgen(-12);
# GP-Test sqrt3i^2 == -3
# GP-DEFINE w6 = 1/2 + 1/2*sqrt3i;
# GP-DEFINE w3 = -1/2 + 1/2*sqrt3i;
# GP-DEFINE b12 = 3/2 + 1/2*sqrt3i;
# rot +/-60
# GP-Test ('x/2 + 'y*sqrt3i/2)*w6 \
# GP-Test == (('x - 3*'y)/2)*1/2 + (( 'x + 'y)/2)*sqrt3i/2
# GP-Test ('x/2 + 'y*sqrt3i/2)/w6 \
# GP-Test == (('x + 3*'y)/2)*1/2 + ((-'x + 'y)/2)*sqrt3i/2
# rot +/-120
# GP-Test ('x/2 + 'y*sqrt3i/2)*w3 \
# GP-Test == ((-'x - 3*'y)/2)*1/2 + (( 'x - 'y)/2)*sqrt3i/2
# GP-Test ('x/2 + 'y*sqrt3i/2)/w3 \
# GP-Test == ((-'x + 3*'y)/2)*1/2 + ((-'x - 'y)/2)*sqrt3i/2
# mirror across 30deg
# GP-Test conj(('x/2 + 'y*sqrt3i/2)/b12)*b12 \
# GP-Test == (('x + 3*'y)/2)*1/2 + (('x - 'y)/2)*sqrt3i/2
=pod
The sqrt(3) factor can be worked into a hypotenuse radial distance
calculation as follows if comparing distances from the origin.
hypot = sqrt(X*X + 3*Y*Y)
See for instance C<TriangularHypot> which is triangular points ordered by
this radial distance.
=head1 FORMULAS
The formulas section in the POD of each class describes some of the
calculations. This might be of interest even if the code is not.
=head2 Triangular Calculations
For a triangular lattice, the rotation formulas above allow calculations to
be done in the rectangular X,Y coordinates which are the inputs and outputs
of the PlanePath functions. Another way is to number vertically on a 60
degree angle with coordinates i,j,
...
* * * 2
* * * 1
* * * j=0
i=0 1 2
These coordinates are sometimes used for hexagonal grids in board games etc.
Using this internally can simplify rotations a little,
-j, i+j rotate +60 (anti-clockwise)
i+j, -i rotate -60 (clockwise)
-i-j, i rotate +120
j, -i-j rotate -120
-i, -j rotate 180
Conversions between i,j and the rectangular X,Y are
X = 2*i + j i = (X-Y)/2
Y = j j = Y
A third coordinate k at a +120 degrees angle can be used too,
k=0 k=1 k=2
* * *
* * *
* * *
0 1 2
This is redundant in that it doesn't number anything i,j alone can't
already, but it has the advantage of turning rotations into just sign
changes and swaps,
-k, i, j rotate +60
j, k, -i rotate -60
-j, -k, i rotate +120
k, -i, -j rotate -120
-i, -j, -k rotate 180
The conversions between i,j,k and the rectangular X,Y are like the i,j above
but with k worked in too.
X = 2i + j - k i = (X-Y)/2 i = (X+Y)/2
Y = j + k j = Y or j = 0
k = 0 k = Y
=head2 N to dX,dY -- Fractional
C<n_to_dxdy()> is the change from N to N+1, and is designed both for integer
N and fractional N. For fractional N it can be convenient to calculate a
dX,dY at floor(N) and at floor(N)+1 and then combine the two in proportion
to frac(N).
int+2
|
|
N+1 \
/| |
/ | |
/ | | frac
/ | |
/ | |
/ | /
int-----N------int+1
this_dX dX,dY next_dX
this_dY next_dY
|-------|------|
frac 1-frac
int = int(N)
frac = N - int 0 <= frac < 1
this_dX,this_dY at int
next_dX,next_dY at int+1
at fractional N
dX = this_dX * (1-frac) + next_dX * frac
dY = this_dY * (1-frac) + next_dY * frac
This is combination of this_dX,this_dY and next_dX,next_dY in proportion to
the distances from positions N to int+1 and from int+1 to N+1.
The formulas can be rearranged to
dX = this_dX + frac*(next_dX - this_dX)
dY = this_dY + frac*(next_dY - this_dY)
which is like dX,dY at the integer position plus fractional part of a turn
or change to the next dX,dY.
=head2 N to dX,dY -- Self-Similar
For most of the self-similar paths such as C<HilbertCurve>, the change dX,dY
is determined by following the state table transitions down through either
all digits of N, or to the last non-9 digit, ie. drop any low digits equal
to radix-1.
Generally paths which are the edges of some tiling use all digits, and those
which are the centres of a tiling stop at the lowest non-9. This can be
seen for example in the C<DekkingCurve> using all digits, whereas its
C<DekkingCentres> variant stops at the lowest non-24.
Perhaps this all-digits vs low-non-9 would even characterize path style as
edges or centres of a tiling, when a path is specified in some way that a
tiling is not quite obvious.
=head1 SUBCLASSING
The mandatory methods for a PlanePath subclass are
n_to_xy()
xy_to_n()
xy_to_n_list() if multiple N's map to an X,Y
rect_to_n_range()
It sometimes happens that one of C<n_to_xy()> or C<xy_to_n()> is easier than
the other but both should be implemented.
C<n_to_xy()> should do something sensible on fractional N. The suggestion
is to make it an X,Y proportionally between integer N positions. It could
be along a straight line or an arc as best suits the path. A straight line
can be done simply by two calculations of the surrounding integer points,
until it's clear how to work the fraction into the code directly.
C<xy_to_n_list()> has a base implementation calling plain C<xy_to_n()> to
give a single N at X,Y. If a path has multiple Ns at an X,Y
(eg. C<DragonCurve>) then it must implement C<xy_to_n_list()> to return all
those Ns, and must also implement a plain C<xy_to_n()> returning the first
of them.
C<rect_to_n_range()> can initially be any convenient over-estimate. It
should give N big enough that from there onwards all points are sure to be
beyond the given X,Y rectangle.
The following descriptive methods have base implementations
n_start() 1
class_x_negative() \ 1, so whole plane
class_y_negative() /
x_negative() calls class_x_negative()
y_negative() calls class_x_negative()
x_negative_at_n() undef \ as for no negatives
y_negative_at_n() undef /
( run in 0.482 second using v1.01-cache-2.11-cpan-df04353d9ac )