Skip to content

Commit

Permalink
feat(grid): update rectangle() to behave as most traversers should
Browse files Browse the repository at this point in the history
By default, rectangle() leaves out its start hex. When a `start` coordinate is passed it starts and
includes that hex, when `at` is passed it starts but doesn't include that hex. The overload where 2
opposing corners can be passed (`rectangle(cornerA, cornerB)`) by default *does* include cornerA.
When `false` is passed as a 3rd argument, cornerA isn't included.

BREAKING CHANGE: When rectangle() isn't passed `start` coordinates it now excludes its first hex
(the cursor, usually `{ q: 0, r: 0 }`). In other words: you probably want to pass `start`
coordinates when using rectangle()
  • Loading branch information
flauwekeul committed Apr 22, 2021
1 parent 5205e93 commit 5415029
Show file tree
Hide file tree
Showing 3 changed files with 94 additions and 14 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ import { createHexPrototype, Grid, rectangle } from 'honeycomb-grid'
const hexPrototype = createHexPrototype({ dimensions: 30 })

// 2. Create a grid with this hex prototype and also pass a "traverser" for a rectangular-shaped grid:
let grid = new Grid(hexPrototype, rectangle({ width: 10, height: 10 }))
let grid = new Grid(hexPrototype, rectangle({ start: { q: 0, r: 0 }, width: 10, height: 10 }))

// 3. Iterate over the grid to log each hex (notice a new grid instance is returned):
grid = grid.each((hex) => console.log(hex))
Expand Down Expand Up @@ -109,7 +109,7 @@ import { createHexPrototype, Grid, rectangle } from 'honeycomb-grid'
// You may want the origin to be the top left corner of a hex's bounding box instead of its center (which is the default)
const hexPrototype = createHexPrototype({ dimensions: 30, origin: 'topLeft' })

new Grid(hexPrototype, rectangle({ width: 10, height: 10 }))
new Grid(hexPrototype, rectangle({ start: { q: 0, r: 0 }, width: 10, height: 10 }))
.each((hex) => renderSVG(hex)) // or: renderCanvas(hex)
.run()
```
Expand Down
72 changes: 68 additions & 4 deletions src/grid/traversers/rectangle.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,69 @@
test.todo(
'returns a traverser that produces hexes in a rectangular shape with the passed width, height, start and direction',
)
import { CompassDirection } from '../../compass'
import { createHex, createHexPrototype } from '../../hex'
import { rectangle } from './rectangle'

test.todo('returns a traverser that produces hexes between the passed opposing corners of a rectangle')
const hexPrototype = createHexPrototype()
const cursor = createHex(hexPrototype, { q: 1, r: 2 })
const getHex = jest.fn((coordinates) => createHex(hexPrototype, coordinates))

describe('when only passed width and height', () => {
test('returns a traverser that returns hexes in a rectangular shape without the cursor', () => {
expect(rectangle({ width: 2, height: 2 })(cursor, getHex)).toMatchObject([
{ q: 2, r: 2 },
{ q: 1, r: 3 },
{ q: 2, r: 3 },
])
})
})

describe('when passed at', () => {
test('returns a traverser that returns hexes in a rectangular shape at the "at" coordinates', () => {
expect(rectangle({ at: { q: 0, r: 0 }, width: 2, height: 2 })(cursor, getHex)).toMatchObject([
{ q: 1, r: 0 },
{ q: 0, r: 1 },
{ q: 1, r: 1 },
])
})
})

describe('when passed start', () => {
test('returns a traverser that returns hexes in a rectangular shape at and including the "start" coordinates', () => {
expect(rectangle({ start: { q: 0, r: 0 }, width: 2, height: 2 })(cursor, getHex)).toMatchObject([
{ q: 0, r: 0 },
{ q: 1, r: 0 },
{ q: 0, r: 1 },
{ q: 1, r: 1 },
])
})
})

describe('when passed direction', () => {
test('returns a traverser that returns hexes in a rectangular shape in order of the passed direction', () => {
expect(rectangle({ width: 2, height: 2, direction: CompassDirection.S })(cursor, getHex)).toMatchObject([
{ q: 1, r: 3 },
{ q: 0, r: 2 },
{ q: 0, r: 3 },
])
})
})

describe('when passed two opposing corners', () => {
test('returns a traverser that returns hexes in a rectangular shape within those corners', () => {
expect(rectangle({ q: 1, r: 2 }, { q: 2, r: 3 })(cursor, getHex)).toMatchObject([
{ q: 1, r: 2 },
{ q: 2, r: 2 },
{ q: 1, r: 3 },
{ q: 2, r: 3 },
])
})
})

describe('when passed two opposing corners and false', () => {
test('returns a traverser that returns hexes in a rectangular shape within those corners, excluding the first corner', () => {
expect(rectangle({ q: 1, r: 2 }, { q: 2, r: 3 }, false)(cursor, getHex)).toMatchObject([
{ q: 2, r: 2 },
{ q: 1, r: 3 },
{ q: 2, r: 3 },
])
})
})
32 changes: 24 additions & 8 deletions src/grid/traversers/rectangle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,27 +8,42 @@ import { line } from './line'
// todo: add in docs: only 90° corners for cardinal directions
// todo: when passed opposing corners: maybe add option to determine if row or col is traversed first
export function rectangle<T extends Hex>(options: RectangleOptions): Traverser<T>
export function rectangle<T extends Hex>(cornerA: HexCoordinates, cornerB: HexCoordinates): Traverser<T>
export function rectangle<T extends Hex>(
cornerA: HexCoordinates,
cornerB: HexCoordinates,
includeCornerA?: boolean,
): Traverser<T>
export function rectangle<T extends Hex>(
optionsOrCornerA: RectangleOptions | HexCoordinates,
cornerB?: HexCoordinates,
includeCornerA = true,
): Traverser<T> {
return (cursor, getHex) => {
const { width, height, start = { q: 0, r: 0 }, direction = CompassDirection.E } = cornerB
? optionsFromOpposingCorners(optionsOrCornerA as HexCoordinates, cornerB, cursor.isPointy, cursor.offset)
const { width, height, start, at, direction = CompassDirection.E } = cornerB
? optionsFromOpposingCorners(
optionsOrCornerA as HexCoordinates,
cornerB,
cursor.isPointy,
cursor.offset,
includeCornerA,
)
: (optionsOrCornerA as RectangleOptions)

return branch<T>(
line({ start, direction: Compass.rotate(direction, 2), length: height - 1 }),
const firstHex = start ? getHex(start) : at ? getHex(at) : cursor
const hexes = branch<T>(
line({ start: firstHex, direction: Compass.rotate(direction, 2), length: height - 1 }),
line({ direction, length: width - 1 }),
)(cursor, getHex)
)(firstHex, getHex) as T[] // todo: internally, Traverser<T> always returns an array, maybe add a return type var

// leave out the start hex (traversers should never include their start hex to prevent duplicate hexes when they're chained)
return start ? hexes : hexes.slice(1)
}
}

export interface RectangleOptions {
width: number
height: number
start?: HexCoordinates
at?: HexCoordinates
direction?: CompassDirection
}

Expand All @@ -37,6 +52,7 @@ function optionsFromOpposingCorners(
cornerB: HexCoordinates,
isPointy: boolean,
offset: number,
includeCornerA: boolean,
): RectangleOptions {
const { col: cornerACol, row: cornerARow } = assertOffsetCoordinates(cornerA, isPointy, offset)
const { col: cornerBCol, row: cornerBRow } = assertOffsetCoordinates(cornerB, isPointy, offset)
Expand All @@ -50,7 +66,7 @@ function optionsFromOpposingCorners(
return {
width: swapWidthHeight ? height : width,
height: swapWidthHeight ? width : height,
start: cornerA,
[includeCornerA ? 'start' : 'at']: cornerA,
direction,
}
}
Expand Down

0 comments on commit 5415029

Please sign in to comment.