Skip to content

Commit

Permalink
feat(grid): line() now accepts either "line as vector" options or "li…
Browse files Browse the repository at this point in the history
…ne between" options

The line() traverser can create a line of hexes starting at the given coordinates in a given
(compass) direction with a given length. Or it can create a line of hexes between start and end
coordinates. These end coordinates can be passed as either "until" or "through". Being similar to
"at" and "start", where "until" excludes the terminal coordinate and "through" includes the terminal
coordinate.
  • Loading branch information
flauwekeul committed Apr 24, 2021
1 parent 052342e commit 2fe7f1d
Show file tree
Hide file tree
Showing 4 changed files with 90 additions and 15 deletions.
38 changes: 35 additions & 3 deletions src/grid/traversers/line.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,51 @@ const hexPrototype = createHexPrototype()
const cursor = createHex(hexPrototype, { q: 1, r: 2 })
const getHex = jest.fn((coordinates) => createHex(hexPrototype, coordinates))

describe('when called with only until', () => {
test('returns a traverser that returns hexes between the cursor and until (excluding until)', () => {
expect(line({ until: { q: -2, r: 6 } })(cursor, getHex)).toMatchObject([
{ q: 0, r: 3 },
{ q: 0, r: 4 },
{ q: -1, r: 5 },
])
})
})

describe('when called with at and until', () => {
test('returns a traverser that returns hexes between at and until (excluding at and until)', () => {
expect(line({ at: { q: 0, r: 1 }, until: { q: 3, r: 3 } })(cursor, getHex)).toMatchObject([
{ q: 1, r: 1 },
{ q: 1, r: 2 },
{ q: 2, r: 2 },
{ q: 2, r: 3 },
])
})
})

describe('when called with start and through', () => {
test('returns a traverser that returns hexes between start and through (including start and through)', () => {
expect(line({ start: { q: 8, r: 1 }, through: { q: 5, r: 3 } })(cursor, getHex)).toMatchObject([
{ q: 8, r: 1 },
{ q: 7, r: 2 },
{ q: 6, r: 2 },
{ q: 5, r: 3 },
])
})
})

describe('when called with only a direction', () => {
test('returns a traverser that returns a hex in the passed direction relative to the cursor', () => {
expect(line({ direction: CompassDirection.E })(cursor, getHex)).toMatchObject([{ q: 2, r: 2 }])
})
})

describe('when called with at', () => {
describe('when called with a direction and at', () => {
test('returns a traverser that returns a hex in the passed direction relative to the "at" coordinates', () => {
expect(line({ direction: CompassDirection.E, at: { q: 3, r: 4 } })(cursor, getHex)).toMatchObject([{ q: 4, r: 4 }])
})
})

describe('when called with start', () => {
describe('when called with a direction and start', () => {
test('returns a traverser that returns the start and a hex in the passed direction relative to the "start" coordinates', () => {
expect(line({ direction: CompassDirection.E, start: { q: 3, r: 4 } })(cursor, getHex)).toMatchObject([
{ q: 3, r: 4 },
Expand All @@ -27,7 +59,7 @@ describe('when called with start', () => {
})
})

describe('when called with length', () => {
describe('when called with a direction and length', () => {
test('returns a traverser that returns the passed amount of hexes in the passed direction relative to the cursor', () => {
expect(line({ direction: CompassDirection.NW, length: 3 })(cursor, getHex)).toMatchObject([
{ q: 1, r: 1 },
Expand Down
59 changes: 50 additions & 9 deletions src/grid/traversers/line.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,65 @@
import { CompassDirection } from '../../compass'
import { Hex } from '../../hex'
import { neighborOf } from '../functions'
import { StartOrAt, Traverser } from '../types'
import {
assertCubeCoordinates,
AxialCoordinates,
CubeCoordinates,
Hex,
HexCoordinates,
PartialCubeCoordinates,
round,
} from '../../hex'
import { distance, neighborOf } from '../functions'
import { StartOrAt, Traverser, XOR } from '../types'

export const line = <T extends Hex>({ direction, start, at, length = 1 }: LineOptions): Traverser<T, T[]> => {
export function line<T extends Hex>(options: LineAsVectorOptions): Traverser<T, T[]>
export function line<T extends Hex>(options: LineBetweenOptions): Traverser<T, T[]>
export function line<T extends Hex>(options: LineAsVectorOptions | LineBetweenOptions): Traverser<T, T[]> {
return (cursor, getHex) => {
const { start, at } = options
const startHex = start && getHex(start)
const hexes: T[] = startHex ? [startHex] : []
let _cursor = startHex ?? (at ? getHex(at) : cursor)

for (let i = 1; i <= length; i++) {
_cursor = getHex(neighborOf(_cursor, direction))
hexes.push(_cursor)
if ((options as LineAsVectorOptions).direction in CompassDirection) {
const { direction, length = 1 } = options as LineAsVectorOptions
let _cursor = startHex ?? (at ? getHex(at) : cursor)

for (let i = 1; i <= length; i++) {
_cursor = getHex(neighborOf(_cursor, direction))
hexes.push(_cursor)
}
} else {
const { until, through } = options as LineBetweenOptions
const _start = start ?? at ?? cursor
const _through = until ?? (through as HexCoordinates)
const startCube = assertCubeCoordinates(_start, cursor)
const throughCube = assertCubeCoordinates(_through, cursor)
const length = distance(cursor, _start, _through)
const step = 1.0 / Math.max(length, 1)

for (let i = 1; until ? i < length : i <= length; i++) {
const coordinates = round(lerp(nudge(startCube), nudge(throughCube), step * i))
hexes.push(getHex(coordinates))
}
}

return hexes
}
}

export type LineOptions = StartOrAt & {
export type LineAsVectorOptions = StartOrAt & {
direction: CompassDirection
length?: number
}

export type LineBetweenOptions = StartOrAt & XOR<{ until: HexCoordinates }, { through: HexCoordinates }>

function nudge({ q, r, s }: CubeCoordinates): CubeCoordinates {
return { q: q + 1e-6, r: r + 1e-6, s: s + -2e-6 }
}

// linear interpolation
function lerp(a: PartialCubeCoordinates, b: PartialCubeCoordinates, t: number): AxialCoordinates {
const q = a.q * (1 - t) + b.q * t
const r = a.r * (1 - t) + b.r * t
return { q, r }
}
4 changes: 3 additions & 1 deletion src/grid/traversers/rectangle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import { branch } from './branch'
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
// todo: when passed opposing corners:
// maybe add option to determine if row or col is traversed first
// maybe accept an object: { at, start, until, through }, similar to line()
export function rectangle<T extends Hex>(options: RectangleOptions): Traverser<T, T[]>
export function rectangle<T extends Hex>(
cornerA: HexCoordinates,
Expand Down
4 changes: 2 additions & 2 deletions src/grid/traversers/spiral.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { CompassDirection } from '../../compass'
import { Hex } from '../../hex'
import { Rotation, StartOrAt, Traverser } from '../types'
import { branch } from './branch'
import { line, LineOptions } from './line'
import { line, LineAsVectorOptions } from './line'
import { ring } from './ring'

export const spiral = <T extends Hex>({ radius, start, at, rotation }: SpiralOptions): Traverser<T, T[]> => (
Expand All @@ -11,7 +11,7 @@ export const spiral = <T extends Hex>({ radius, start, at, rotation }: SpiralOpt
) => {
const center = start ? getHex(start) : at ? getHex(at) : cursor
return branch<T>(
line({ start, at, direction: CompassDirection.N, length: radius } as LineOptions),
line({ start, at, direction: CompassDirection.N, length: radius } as LineAsVectorOptions),
ring({ center, rotation }),
)(getHex(center), getHex)
}
Expand Down

0 comments on commit 2fe7f1d

Please sign in to comment.