Skip to content

Commit

Permalink
feat(hex,grid): traversers return hexes (as before), fix move() trave…
Browse files Browse the repository at this point in the history
…rser, add neighborOf() function

The move() traverser didn't work properly, fixed it by converting to offset coordinates, update row
or col (depending on hex orientation) and then converting back to axial coordinates. Because of this
some new functions were needed: hexToOffset() and offsetToAxial(). Also added row and col getters
(which use hexToOffset()) on hex prototype for convenience. The functionality to get a neighboring
hex was moved from the move() traverser to a new neighborOf() grid function. This function also
supports ambiguous directions (North and South for pointy and East and West for flat hexes). But
because it was very awkward to create a hex (needed for the generic neighborOf() function) and then
returning only its coordinates in move(), I decided that all traversers should just receive and
return hexes (instead of receiving hexes but returning coordinates). For this I added a copyHex()
function (and delegating copy() method on hex prototype) that's able to copy the "cursor" hex a
traverser receives with new coordinates. This hex is then returned from traversers. Finally, both
copyHex() and createHex() now copy/create any custom props in their returning hex.
  • Loading branch information
flauwekeul committed Apr 22, 2021
1 parent 751be5a commit b2da583
Show file tree
Hide file tree
Showing 19 changed files with 157 additions and 102 deletions.
30 changes: 15 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,42 +27,42 @@ All existing JS hex grid libraries I could find are coupled with some form of vi

- [ ] hex functions (apply to single hexes):
- [ ] ? add
- [ ] cartesian
- [ ] ? cartesianToCube (alias: toCube)
- [x] ~~cartesian~~ replaced with `row` and `col` props
- [x] ~~cartesianToCube (alias: toCube)~~ replaced with `offsetToAxial()`
- [ ] center
- [ ] ? coordinates (returns cube by default?)
- [ ] corners
- [ ] cube
- [ ] cubeToCartesian (alias: toCartesian)
- [ ] equals
- [x] ~~coordinates (returns cube by default?)~~ considered obsolete
- [x] corners
- [x] ~~cube~~ considered obsolete
- [x] ~~cubeToCartesian (alias: toCartesian)~~ replaced with `hexToOffset()`
- [x] equals
- [ ] from (convert anything? to a hex)
- [ ] height
- [ ] isFlat
- [ ] isPointy
- [x] height
- [x] isFlat
- [x] isPointy
- [ ] lerp
- [ ] nudge
- [ ] round
- [ ] ? set
- [ ] ? subtract
- [ ] thirdCoordinate
- [ ] toString
- [ ] width
- [x] toString
- [x] width
- [ ] grid functions (apply to multiple hexes):
- [ ] ? distance
- [ ] hexToPoint
- [x] hexToPoint
- [ ] pointToHex
- [ ] get
- [ ] hexesBetween
- [ ] hexesInRange
- [ ] ? line (can be infinite)
- [ ] neighborsOf
- [x] ~~neighborsOf~~ replaced with `neighborOf()` (singular)
- [ ] pointHeight
- [ ] pointWidth
- [ ] ? set
- [ ] parallelogram
- [ ] triangle (can be infinite?)
- [ ] hexagon (can be infinite?)
- [ ] rectangle
- [x] rectangle
- [ ] ring (can be infinite?)
- [ ] spiral (can be infinite)

Expand Down
12 changes: 1 addition & 11 deletions src/grid/constants.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,4 @@
import { AxialCoordinates, CubeCoordinates } from '../hex'

// fixme: compass has 8 directions, this should too?
export const DIRECTION_COORDINATES: AxialCoordinates[] = [
{ q: 1, r: 0 },
{ q: 0, r: 1 },
{ q: -1, r: 1 },
{ q: -1, r: 0 },
{ q: 0, r: -1 },
{ q: 1, r: -1 },
]
import { CubeCoordinates } from '../hex'

// fixme: compass has 8 directions, this should too?
export const RECTANGLE_DIRECTIONS = [
Expand Down
1 change: 1 addition & 0 deletions src/grid/functions/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './neighborOf'
26 changes: 26 additions & 0 deletions src/grid/functions/neighborOf.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { AxialCoordinates, Hex, offsetToAxialFlat, offsetToAxialPointy } from '../../hex'
import { Compass } from '../types'

const DIRECTION_COORDINATES: AxialCoordinates[] = [
{ q: 1, r: 0 },
{ q: 0, r: 1 },
{ q: -1, r: 1 },
{ q: -1, r: 0 },
{ q: 0, r: -1 },
{ q: 1, r: -1 },
]

export const neighborOf = <T extends Hex>(hex: T, direction: Compass) => {
if ((direction === Compass.S || direction === Compass.N) && hex.isPointy) {
const nextRow = direction === Compass.S ? hex.row + 1 : hex.row - 1
return offsetToAxialPointy(hex.col, nextRow, hex.offset)
}

if ((direction === Compass.E || direction === Compass.W) && hex.isFlat) {
const nextCol = direction === Compass.E ? hex.col + 1 : hex.col - 1
return offsetToAxialFlat(nextCol, hex.row, hex.offset)
}

const neighbor = DIRECTION_COORDINATES[direction]
return { q: hex.q + neighbor.q, r: hex.r + neighbor.r }
}
17 changes: 11 additions & 6 deletions src/grid/grid.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { createHex, CubeCoordinates, equals, Hex, HexCoordinates } from '../hex'
import { offsetFromZero } from '../utils'
import { RECTANGLE_DIRECTIONS } from './constants'
import { neighborOf } from './functions'
import { Compass, RectangleOptions, Traverser } from './types'
import { forEach, map } from './utils'

Expand Down Expand Up @@ -61,17 +62,17 @@ export class Grid<T extends Hex> {
const [firstStop, secondStop] = this.hexPrototype.isPointy ? [width, height] : [height, width]
const relativeOffset = (coordinate: number) => offsetFromZero(this.hexPrototype.offset, coordinate)
const rectangle: Traverser<T> = (cursor) => {
const result: HexCoordinates[] = []
const result: T[] = []
let _cursor = cursor
for (let second = 0; second < secondStop; second++) {
const secondOffset = relativeOffset(second)
for (let first = -secondOffset; first < firstStop - secondOffset; first++) {
const nextCursor: unknown = {
const coordinates: unknown = {
[firstCoordinate]: first + _start[firstCoordinate],
[secondCoordinate]: second + _start[secondCoordinate],
[thirdCoordinate]: -first - second + _start[thirdCoordinate],
}
_cursor = nextCursor as CubeCoordinates
_cursor = _cursor.copy(coordinates as CubeCoordinates)
result.push(_cursor)
}
}
Expand Down Expand Up @@ -111,15 +112,15 @@ export class Grid<T extends Hex> {
const result: T[] = []
const hasTraversedBefore = this.traverser !== infiniteTraverser
const previousHexes = [...this.traverser()]
let cursor: HexCoordinates = previousHexes[previousHexes.length - 1] || { q: 0, r: 0 }
let cursor: T = previousHexes[previousHexes.length - 1] || createHex(this.hexPrototype)

for (const traverser of traversers) {
for (const nextCursor of traverser(cursor, this.hexPrototype)) {
for (const nextCursor of traverser(cursor)) {
cursor = nextCursor
if (hasTraversedBefore && !previousHexes.some((prevCoords) => equals(prevCoords, cursor))) {
return result // todo: or continue? or make this configurable?
}
result.push(createHex(this.hexPrototype, cursor))
result.push(cursor)
}
}

Expand All @@ -128,4 +129,8 @@ export class Grid<T extends Hex> {

return this.clone(traverse)
}

neighborOf(hex: T, direction: Compass) {
return neighborOf(hex, direction)
}
}
1 change: 1 addition & 0 deletions src/grid/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './constants'
export * from './functions'
export * from './grid'
export * from './traversers'
export * from './types'
Expand Down
4 changes: 2 additions & 2 deletions src/grid/traversers/at.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { DefaultHexPrototype, HexCoordinates } from '../../hex'
import { Hex, HexCoordinates } from '../../hex'
import { Traverser } from '../types'

export const at = <T extends DefaultHexPrototype>(cursor: HexCoordinates): Traverser<T> => () => [cursor]
export const at = <T extends Hex>(coordinates: HexCoordinates): Traverser<T> => (cursor) => [cursor.copy(coordinates)]

export const start = at
47 changes: 9 additions & 38 deletions src/grid/traversers/move.ts
Original file line number Diff line number Diff line change
@@ -1,46 +1,17 @@
import { DefaultHexPrototype, HexCoordinates } from '../../hex'
import { offsetFromZero } from '../../utils'
import { DIRECTION_COORDINATES } from '../constants'
import { Hex } from '../../hex'
import { neighborOf } from '../functions'
import { Compass, Traverser } from '../types'

// todo: also accept a string and/or number for direction?
export const move = <T extends DefaultHexPrototype>(direction: Compass, times = 1): Traverser<T> => {
const { q, r } = DIRECTION_COORDINATES[direction]

return (cursor, hexPrototype) => {
const result: HexCoordinates[] = []
const relativeOffset = (coordinate: number) => offsetFromZero(hexPrototype.offset, coordinate)

// todo: refactor, move ifs inside single for loop
if (hexPrototype.isPointy && (direction === Compass.S || direction === Compass.N)) {
for (let i = 1; i <= times; i++) {
const cursorCol = cursor.q - relativeOffset(cursor.r)
const cursorRow = cursor.r
const addCol = i * q - relativeOffset(i * r)
const addRow = i * r
const _q = cursorCol + relativeOffset(cursorRow) + addCol
const _r = cursorRow + addRow
result.push({ q: _q, r: _r })
}
return result
}

if (hexPrototype.isFlat && (direction === Compass.E || direction === Compass.W)) {
for (let i = 1; i <= times; i++) {
const cursorCol = cursor.q
const cursorRow = cursor.r - relativeOffset(cursor.q)
const addCol = i * q
const addRow = i * r - relativeOffset(i * q)
const _q = cursorCol + addCol
const _r = cursorRow + relativeOffset(cursorCol) + addRow
result.push({ q: _q, r: _r })
}
return result
}
export const move = <T extends Hex>(direction: Compass, times = 1): Traverser<T> => {
return (cursor) => {
const result: T[] = []
let _cursor = cursor

for (let i = 1; i <= times; i++) {
result.push({ q: cursor.q + q * i, r: cursor.r + r * i })
_cursor = _cursor.copy(neighborOf(_cursor, direction))
result.push(_cursor)
}

return result
}
}
3 changes: 2 additions & 1 deletion src/grid/traversers/rectangle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { offsetFromZero } from '../../utils'
import { RECTANGLE_DIRECTIONS } from '../constants'
import { Compass, RectangleOptions } from '../types'

// todo: either delete this or fix it so it can do what Grid.rectangle() can
export const rectangle = <T extends Hex>(
hexPrototype: T,
{
Expand Down Expand Up @@ -33,7 +34,7 @@ export const rectangle = <T extends Hex>(
// if (hasTraversedBefore && !previousHexes.some((prevCoords) => equals(prevCoords, coordinates))) {
// return result // todo: or continue? or make this configurable?
// }
result.push(createHex(hexPrototype, nextCoordinates as CubeCoordinates))
result.push(createHex<T>(hexPrototype, nextCoordinates as CubeCoordinates))
}
}

Expand Down
11 changes: 4 additions & 7 deletions src/grid/traversers/repeat.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
import { DefaultHexPrototype, HexCoordinates } from '../../hex'
import { Hex } from '../../hex'
import { Traverser } from '../types'

// todo: looks a lot like Grid.traverse()
export const repeat = <T extends DefaultHexPrototype>(amount: number, ...traversers: Traverser<T>[]): Traverser<T> => (
cursor,
hexPrototype,
) => {
const result: HexCoordinates[] = []
export const repeat = <T extends Hex>(amount: number, ...traversers: Traverser<T>[]): Traverser<T> => (cursor) => {
const result: T[] = []
let _cursor = cursor

for (let i = 0; i < amount; i++) {
for (const traverser of traversers) {
for (const nextCursor of traverser(_cursor, hexPrototype)) {
for (const nextCursor of traverser(_cursor)) {
_cursor = nextCursor
result.push(_cursor)
}
Expand Down
6 changes: 3 additions & 3 deletions src/grid/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { DefaultHexPrototype, HexCoordinates } from '../hex'
import { Hex, HexCoordinates } from '../hex'

export enum Compass {
E,
Expand All @@ -11,8 +11,8 @@ export enum Compass {
NE,
}

export interface Traverser<T extends DefaultHexPrototype> {
(cursor: HexCoordinates, hexPrototype: T): Iterable<HexCoordinates>
export interface Traverser<T extends Hex> {
(cursor: T): Iterable<T>
}

export interface RectangleOptions {
Expand Down
4 changes: 4 additions & 0 deletions src/hex/functions/copyHex.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { Hex, HexCoordinates } from '../types'

export const copyHex = <T extends Hex>(hex: T, newProps: Partial<T> | HexCoordinates = {}) =>
Object.assign(Object.create(Object.getPrototypeOf(hex)), hex, newProps)
9 changes: 6 additions & 3 deletions src/hex/functions/createHex.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { DefaultHexPrototype, Hex, HexCoordinates } from '../types'
import { Hex, HexCoordinates } from '../types'

export const createHex = <T extends DefaultHexPrototype>(prototype: T, { q, r, s = -q - r }: HexCoordinates) =>
export const createHex = <T extends Hex>(
prototype: T,
{ q, r, s = -q - r, ...rest }: HexCoordinates | T = { q: 0, r: 0 },
) =>
// fixme: when `prototype` is a hex instance, an object with that instance as prototype is created...
// either only accept a hex prototype or check if `prototype` is a hex instance and then clone it?
// todo: make coordinates readonly
Object.assign(Object.create(prototype), { q, r, s }) as T extends Hex ? T : T & Hex
Object.assign(Object.create(prototype), { q, r, s }, rest) as T extends Hex ? T : T & Hex
37 changes: 26 additions & 11 deletions src/hex/functions/createHexPrototype.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { isFunction, isObject, isPoint } from '../../utils'
import { DefaultHexPrototype, Ellipse, Hex, HexSettings, Orientation, Point, Rectangle } from '../types'
import { copyHex } from './copyHex'
import { corners } from './corners'
import { height } from './height'
import { hexToOffset } from './hexToOffset'
import { hexToPoint } from './hexToPoint'
import { isFlat } from './isFlat'
import { isPointy } from './isPointy'
Expand Down Expand Up @@ -82,31 +84,38 @@ const assertOffset = ({ offset }: HexPrototypeOptions) => {
export const createHexPrototype = <T extends DefaultHexPrototype>(
customPrototype?: T | Partial<HexPrototypeOptions>,
) => {
// pseudo private property
const s = new WeakMap()

const prototype = {
...defaultHexSettings,

// todo: make this a getter and name it `asPoint`?
copy(newProps = {}) {
return copyHex(this, newProps)
},
// fixme: make this a getter and name it `asPoint`, or better: add getters for x and y
toPoint() {
return hexToPoint(this)
},

// todo: add to docs that any of the above methods will be overwritten when present in customPrototype
...customPrototype,
} as T & HexPrototypeOptions

// use Object.defineProperties() to create readonly properties
// origin is set in the final "step"
Object.defineProperties(prototype, {
// todo: all props set with `value` are writable (somehow the default `writable: false` doesn't apply). Not sure if this is a problem though
// see: Object.getOwnPropertyDescriptors(hexPrototype)
dimensions: { value: normalizeDimensions(prototype) },
orientation: { value: normalizeOrientation(prototype) },
// origin is set in the final "step"
offset: { value: assertOffset(prototype) },
col: {
get() {
return hexToOffset(this).col
},
},
corners: {
get() {
return corners(this, this)
},
},
dimensions: { value: normalizeDimensions(prototype) },
height: {
get() {
return height(this)
Expand All @@ -122,13 +131,19 @@ export const createHexPrototype = <T extends DefaultHexPrototype>(
return isPointy(this)
},
},
orientation: { value: normalizeOrientation(prototype) },
offset: { value: assertOffset(prototype) },
row: {
get() {
return hexToOffset(this).row
},
},
s: {
get() {
// todo: typescript doesn't support this somehow: return this._s ?? -this.q - this.r
return Number.isFinite(this._s) ? this._s : -this.q - this.r
return Number.isFinite(s.get(this)) ? s.get(this) : -this.q - this.r
},
set(s: number) {
this._s = s
set(_s: number) {
s.set(this, _s)
},
},
width: {
Expand Down

0 comments on commit b2da583

Please sign in to comment.