Skip to content

Tool for spiral walking in a 2D matrix or a plain in a 3D matrix. Works as a iterator on a list with coordinates starting at centre.

License

Notifications You must be signed in to change notification settings

TerjeUrnes/spiral-walk

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

30 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

  

Spiral Walking
on a 2D plane in a 2D or 3D world
A iterator that generates coordinates

From a given point in a 2D plain you can walk outwards in a spiral with help of this class. The plain can be a 2D matrix or a slice (slabs with one cell thickness) of a 3d volume, or other that takes coordinates as input. It can repeat the spiral while traversing through a volume (VolumeMode). Can go both clockwise and counter clockwise (Walking). Multiple different conditions to stop the spiral (StopCondition). And can filter the spiral path with a custom function (Filter).

It works like an iterator that you use in a for...of loop, as seen in the code block below. In that example the result is a 1D list with all the elements in spiral order.

const matrixToWalkIn = [
    [ Y00, J01, K02, L03, M04 ],
    [ X10, I11, B12, C13, N14 ],
    [ W20, H21, A22, D23, O24 ],
    [ V30, G31, F32, E33, P34 ] ];

const inSpiralOrder = [];
for (const { x, y } of SpiralWalkCoordGen) {
    inSpiralOrder.push( matrixToWalkIn[ y ][ x ] );
}

_.isEqual(inSpiralOrder, [ A22, B12, C13, D23, E33, F32, G31, H21, I11, 
        J01, K02, L03, M04, N14, O24, P34, V30, W20, X10, Y00 ]) == true;

  

Note

It's implemented as vanilla javascript class in a ECMAscript module. The module is in its most simple form, just remove the export key in front of the class definition to use it as a regular class.
All inputs and outputs to this class follows the destructuring assignment syntax

  

// How to use as static

SpiralWalkCoordGen.StopCondition = { maxCircles: 2 }
SpiralWalkCoordGen.StartCoord = { x: 5, y: 5 };

for (const coord of SpiralWalkCoordGen) {
    const result = testMatrix[coord.x][coord.y] != null ? "full" : "empty"
    console.log(coord, "The cell is " + result);
}
// How to use as instance

const generator = new SpiralWalkCoordGen();

generator.StopCondition = { maxCircles: 2 }
generator.StartCoord = { x: 5, y: 5 };

for (const coord of generator) {
    const result = testMatrix[coord.x][coord.y] != null ? "full" : "empty"
    console.log(coord, "The cell is " + result);
}
outprint: { x: 5, y: 5 } - The cell is empty
{ x: 5, y: 4 } - The cell is full
{ x: 6, y: 4 } - The cell is full
{ x: 6, y: 5 } - The cell is full
{ x: 6, y: 6 } - The cell is empty
{ x: 5, y: 6 } - The cell is empty
{ x: 4, y: 6 } - The cell is full
{ x: 4, y: 5 } - The cell is empty
{ x: 4, y: 4 } - The cell is empty
{ x: 4, y: 3 } - The cell is full
{ x: 5, y: 3 } - The cell is empty
{ x: 6, y: 3 } - The cell is full
{ x: 7, y: 3 } - The cell is full
{ x: 7, y: 4 } - The cell is full
{ x: 7, y: 5 } - The cell is full
{ x: 7, y: 6 } - The cell is full
{ x: 7, y: 7 } - The cell is full
{ x: 6, y: 7 } - The cell is empty
{ x: 5, y: 7 } - The cell is empty
{ x: 4, y: 7 } - The cell is empty
{ x: 3, y: 7 } - The cell is empty
{ x: 3, y: 6 } - The cell is full
{ x: 3, y: 5 } - The cell is empty
{ x: 3, y: 4 } - The cell is empty
{ x: 3, y: 3 } - The cell is empty

  

Iteration Algorithm (Simplified)

while ("Should walk one more circle") {
    for ( "Traverse top row" )
        "new coordinate"
    for ( "Traverse most right column" )
        "new coordinate"
    for ( "Traverse bottom row" )
        "new coordinate"
    for ( "Traverse most left column" )
        "new coordinate"
}

This is the spiral walk algorithm. It is one while loop and four for loops, the first do-while in the code below is not a part of the circle walk but for moving to the next plain and thus move through a volume. The while loop that is a part of the walk algorithm decides how many circle there should be, while the for loops is for traversing one of the four sides of the circle. See fig.A.

As seen the traversing starts on the second square, while in the code the X,Y and Z is set on the upper left corner. So the first to do in the for loop is to shift one square, that is whey starting with increase or decrease the axes that are traversed. Waiting with increase or decrease to after generated the coordinate will end up as seen in fig.B. Also see in fig.A that the length of a side that they traverse is the circle number multiplied with 2.

Note

In order to keep it simple planeCount is here zero based (counts from 0), while planeCount that are coming in to the custom function is one based (counts from 1).

*[Symbol.iterator]() {

    planeCount = -1

    do {

        planeCount++
        circleCount = 0

        if (includeStartCoord) {
            yield { x: startCoord.x, y: startCoord.y, 
                        z: startCoord.z + planeCount }
        }

        while (ShouldWalkOneMoreCircle()) {

            circleCount++
            sideLength = circleCount * 2
            X = startCoord.x - circleCount
            Y = startCoord.y - circleCount
            Z = startCoord.z + planeCount

            for (hTop = 0; hTop < sideLength; hTop++) {
                X++
                if (includeCoordinate( X, Y )) {
                    yield { x: X, y: Y, z: Z }
                }
            }
            for (vRight = 0; vRight < sideLength; vRight++) {
                Y++
                if (includeCoordinate( X, Y )) {
                    yield { x: X, y: Y, z: Z}
                }
            }
            for (hBottom = 0; hBottom < sideLength; hBottom++) {
                X--
                if (includeCoordinate( X, Y )) {
                    yield { x: X, y: Y, z: Z }
                }
            }
            for (vLeft = 0; vLeft < sideLength; vLeft++) {
                Y--
                if (includeCoordinate( X, Y)) {
                    yield {x: X, y: Y, z: Z }
                }
            }
        }

    }
    while (ShouldGoToNextPlane())
}

  

Properties

All uses destructuring assignment as inputs and outputs. i.e. you do not ned to use all arguments
All properties has only setters, so the only output is the iterator and the arguments that goes into the custom function.

  

Note

The properties exist as both static properties and as instance properties. All examples works also when using it as a instance.

Caution

It does not check the input. The user of this class is responsible for that it is correct.

  

StartCoord

Is the center and starting point for the spiral. It can be shifted between the iterations by setting the delta values and set increaseAfter to true or autoTraverse on VolumeMode to true.

Note

Coord outside of border can result in no iteration output.
A delta less then 1 can result in the same coordinates. x = 1.9 gives the value 1, adding dx = 0.2 gives the value 2, adding dx again gives the value 2 one more time.

Argument
name
Default
value
Values
x 0.00 number
y 0.00 number
z 0.00 number Only used in volume mode
dx 0.00 number Shifts the x coordinate after has ended the circle
dy 0.00 number As dx but for the y coordinate
dz 0.00 number As dx but for the z coordinate. Only used in volume mode
increaseAfter false boolean
includeInIteration true boolean Start with the center or the first circle.
SpiralWalkCoordGen.StartCoord = { x: 90, y: 50 };
for (const coord of SpiralWalkCoordGen) {
    // first coord x: 90 y: 50
}

SpiralWalkCoordGen.StartCoord = { x: 100 }
for (const coord of SpiralWalkCoordGen) {
    // first coord x: 100 y: 50
}

SpiralWalkCoordGen.StartCoord = { y: 60 }
for (const coord of SpiralWalkCoorGen) {
    // first coord x: 100 y: 60
}

  

VolumeMode

If enabled the walk will be on a 2d plain inside of a 3d volume.

Argument
name
Default
value
Values
enabled false boolean
iterateOverPlan "xy" "xy", "xz" or "yz"
autoTraverse true boolean
SpiralWalkCoordGen.VolumeMode = {
    enabled: true,
    iterateOverPlan: "xz"
};

for (const coord of SpiralWalkCoorGen) {
    console.log(coord);
}
outprint: { x: 0, y: 0, z: 0 }
{ x: 0, y: 0, z: -1 }
{ x: 1, y: 0, z: -1 }
{ x: 1, y: 0, z: 0 }
{ x: 1, y: 0, z: 1 }
{ x: 0, y: 0, z: 1 }
{ x: -1, y: 0, z: 1 }
{ x: -1, y: 0, z: 0 }
{ x: -1, y: 0, z: -1 }
{ x: -1, y: 0, z: -2 }
{ x: 0, y: 0, z: -2 }
{ x: 1, y: 0, z: -2 }
{ x: 2, y: 0, z: -2 }
{ x: 2, y: 0, z: -1 }
{ x: 2, y: 0, z: 0 }
{ x: 2, y: 0, z: 1 }
{ x: 2, y: 0, z: 2 }

  

StopCondition

More then one condition can be active. Stops on the first to be fulfilled.
No active condition should make a infinitive loop. reachedIterationCount that is not reachable can also give infinitive loop, includeCoordsOutside on border can give that problem if it is set to false.

Warning

This can give a infinity loop if not set correctly.

Argument
name
Default
value
Values
maxCircles false number or false
reachedFirstBorder false boolean
reachedAllBorders true boolean
reachedIterationCount 10000 number or false
SpiralWalkCoordGen.StopCondition = {
    reachedFirstBorder: true,
    reachedIterationCount: 1000000
}

  

Filter

Function input come as destructuring assignment.

Argument
name
Default
value
Values
useCustomFunc false boolean
customFunc () => true function
Custom function
argument
contains
coord {x, y, z} The coordinate that is tested, in matrix/volume coords
planeCoord {a, b} The coordinate on the plane, identical with x and y in coord if it is in the xy plane.
startCoord {x, y, z} The coordinate where the walk started
circleNumber number Tells which circle you are on, 1 is the first circle around the center
planeNumber number Tells which plane it walks on, 1 is the first plane
borderX {min, max} The smallest and biggest x value on the border.
borderY {min, max} The smallest and biggest y value on the border.
borderZ {min, max} The smallest and biggest z value on the border.
SpiralWalkCoordGen.Filter = {
    useCustomFunc: true,
    customFunc: ({coord: {x: point}, borderX: {min, max}}) => {
        return min <= point && point <= max;
    }
}
From a unit test where using this function: The values are the indexes. The border is 7x7. includeCoordsOutside is set to true.

  

Border

Start coord outside of border give no iteration output if includeCoordsOutside is false, its default is false.

Argument
name
Default
value
Values
x -10 number Upper left corner
y -10 number Upper left corner
z -10 number Upper left corner
width 21 number
height 21 number
depth 21 number
includeCoordsOutside false boolean
leftPlaneMinX -10 number Will be autogenerated
rightPlaneMaxX 10 number Will be autogenerated
topPlaneMinY -10 number Will be autogenerated
bottomPlaneMaxY 10 number Will be autogenerated
frontPlaneMinZ -10 number Will be autogenerated
backPlaneMaxZ 10 number Will be autogenerated
SpiralWalkCoordGen.Border = {
    x: 50,
    y: 50,
    width: 100,
    height: 60
}

  

Walking

Argument
name
Default
value
Values
direction "cw" "cw" or "ccw"
SpiralWalkCoordGen.Walking = {
    direction: "ccw"
}

  

Functions

Reset()

It will set all static properties back to default values. Exist only as a static function. If working with a instance and need to reset, just make a new instance.

SpiralWalkCoordGen.Reset();

  

Performance

Optimization hasn't received much focus. Still, I don't think there will be any issues in most cases since a unit test generating a 10 million long spiral executes in about 0.7s on a M2 MacBook Air

  

The numbers are the indexes for the generated output.

About

Tool for spiral walking in a 2D matrix or a plain in a 3D matrix. Works as a iterator on a list with coordinates starting at centre.

Topics

Resources

License

Stars

Watchers

Forks