From 6fc63cb384a74c8aa2321d0aca4c2bb6b4e99838 Mon Sep 17 00:00:00 2001 From: Abbe Keultjes Date: Tue, 9 Aug 2022 10:31:21 +0200 Subject: [PATCH] :sparkles: rename functions and add util Rename assertCubeCoordinates() to toCube() and completeCubeCoordinates() to completeCube() and add tests. Also add isNumber() util. --- docs/guide/coordinate-system.md | 8 +++--- src/grid/functions/distance.ts | 6 ++--- src/grid/traversers/line.ts | 4 +-- src/grid/traversers/rays.ts | 4 +-- src/grid/traversers/rectangle.ts | 4 +-- src/grid/traversers/ring.ts | 7 ++--- src/hex/functions/completeCube.test.ts | 21 +++++++++++++++ src/hex/functions/completeCube.ts | 23 ++++++++++++++++ src/hex/functions/completeCubeCoordinates.ts | 26 ------------------- src/hex/functions/index.ts | 4 +-- src/hex/functions/round.ts | 4 +-- src/hex/functions/toCube.test.ts | 21 +++++++++++++++ .../{assertCubeCoordinates.ts => toCube.ts} | 15 +++++------ src/hex/functions/translate.ts | 10 +++---- src/hex/hex.ts | 4 +-- src/utils/index.ts | 1 + src/utils/isAxial.ts | 3 ++- src/utils/isNumber.ts | 1 + src/utils/isOffset.ts | 3 ++- src/utils/isPoint.ts | 3 ++- src/utils/isTuple.ts | 3 ++- 21 files changed, 109 insertions(+), 66 deletions(-) create mode 100644 src/hex/functions/completeCube.test.ts create mode 100644 src/hex/functions/completeCube.ts delete mode 100644 src/hex/functions/completeCubeCoordinates.ts create mode 100644 src/hex/functions/toCube.test.ts rename src/hex/functions/{assertCubeCoordinates.ts => toCube.ts} (61%) create mode 100644 src/utils/isNumber.ts diff --git a/docs/guide/coordinate-system.md b/docs/guide/coordinate-system.md index 854d681f..b92b8322 100644 --- a/docs/guide/coordinate-system.md +++ b/docs/guide/coordinate-system.md @@ -46,14 +46,14 @@ hex.col = 2 // ❌ TypeError Most functions/methods that require coordinates accept `HexCoordinates`, which is a [union type](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#union-types) of the four coordinate types. -Because `HexCoordinates` can be any of the four types, you may use `assertCubeCoordinates()` to convert `HexCoordinates` to `CubeCoordinates`: +Because `HexCoordinates` can be any of the four types, you may use `toCube()` to convert `HexCoordinates` to `CubeCoordinates`: ```typescript const hexPrototype = createHexPrototype() -assertCubeCoordinates(hexPrototype, [1, 2]) // { q: 1, r: 2, s: -3 } -assertCubeCoordinates(hexPrototype, { col: 1, row: 2 }) // { q: 0, r: 2, s: -2 } -assertCubeCoordinates(hexPrototype, { q: 3, r: 4 }) // { q: 3, r: 4, s: -7 } +toCube(hexPrototype, [1, 2]) // { q: 1, r: 2, s: -3 } +toCube(hexPrototype, { col: 1, row: 2 }) // { q: 0, r: 2, s: -2 } +toCube(hexPrototype, { q: 3, r: 4 }) // { q: 3, r: 4, s: -7 } ``` ## Converting diff --git a/src/grid/functions/distance.ts b/src/grid/functions/distance.ts index ebc581fd..9903fef8 100644 --- a/src/grid/functions/distance.ts +++ b/src/grid/functions/distance.ts @@ -1,7 +1,7 @@ -import { assertCubeCoordinates, Hex, HexCoordinates } from '../../hex' +import { Hex, HexCoordinates, toCube } from '../../hex' export function distance(hex: Pick, from: HexCoordinates, to: HexCoordinates) { - const { q: fromQ, r: fromR, s: fromS } = assertCubeCoordinates(hex, from) - const { q: toQ, r: toR, s: toS } = assertCubeCoordinates(hex, to) + const { q: fromQ, r: fromR, s: fromS } = toCube(hex, from) + const { q: toQ, r: toR, s: toS } = toCube(hex, to) return Math.max(Math.abs(fromQ - toQ), Math.abs(fromR - toR), Math.abs(fromS - toS)) } diff --git a/src/grid/traversers/line.ts b/src/grid/traversers/line.ts index fedba5e4..90a2d945 100644 --- a/src/grid/traversers/line.ts +++ b/src/grid/traversers/line.ts @@ -1,5 +1,5 @@ import { CompassDirection } from '../../compass' -import { assertCubeCoordinates, AxialCoordinates, CubeCoordinates, Hex, HexCoordinates, round } from '../../hex' +import { AxialCoordinates, CubeCoordinates, Hex, HexCoordinates, round, toCube } from '../../hex' import { distance, neighborOf } from '../functions' import { Traverser } from '../types' @@ -61,7 +61,7 @@ function lineFromBetweenOptions({ start, stop }: LineBetweenOptio const hexes: T[] = [] const firstHex = createHex(start ?? cursor) const nudgedStart = nudge(firstHex) - const nudgedStop = nudge(assertCubeCoordinates(firstHex, stop)) + const nudgedStop = nudge(toCube(firstHex, stop)) const interpolate = lerp(nudgedStart, nudgedStop) const length = distance(firstHex, firstHex, stop) const step = 1.0 / Math.max(length, 1) diff --git a/src/grid/traversers/rays.ts b/src/grid/traversers/rays.ts index fe05466f..15e1df23 100644 --- a/src/grid/traversers/rays.ts +++ b/src/grid/traversers/rays.ts @@ -1,4 +1,4 @@ -import { assertCubeCoordinates, Hex, HexCoordinates } from '../../hex' +import { Hex, HexCoordinates, toCube } from '../../hex' import { RotationLike, Traverser } from '../types' import { line } from './line' import { ring } from './ring' @@ -22,7 +22,7 @@ export function rays({ return function raysTraverser(createHex, cursor) { const firstHex = createHex(start ?? cursor) - const { q, r, s } = assertCubeCoordinates(firstHex, firstHex) + const { q, r, s } = toCube(firstHex, firstHex) const firstStop = (options as RaysToHexOptions).firstStop ?? { q, r: r - length, s: s + length } return ring({ center: firstHex, start: firstStop, rotation })(createHex, cursor).flatMap((stop) => diff --git a/src/grid/traversers/rectangle.ts b/src/grid/traversers/rectangle.ts index 0b5df314..b17141cb 100644 --- a/src/grid/traversers/rectangle.ts +++ b/src/grid/traversers/rectangle.ts @@ -1,5 +1,5 @@ import { Compass, CompassDirection } from '../../compass' -import { completeCubeCoordinates, Hex, HexCoordinates, HexOffset, hexToOffset, OffsetCoordinates } from '../../hex' +import { completeCube, Hex, HexCoordinates, HexOffset, hexToOffset, OffsetCoordinates } from '../../hex' import { isOffset, isTuple, tupleToCube } from '../../utils' import { Traverser } from '../types' import { line } from './line' @@ -74,7 +74,7 @@ function optionsFromOpposingCorners( function assertOffsetCoordinates(coordinates: HexCoordinates, isPointy: boolean, offset: HexOffset): OffsetCoordinates { if (isOffset(coordinates)) return coordinates - const { q, r } = isTuple(coordinates) ? tupleToCube(coordinates) : completeCubeCoordinates(coordinates) + const { q, r } = isTuple(coordinates) ? tupleToCube(coordinates) : completeCube(coordinates) return hexToOffset({ q, r, isPointy, offset }) } diff --git a/src/grid/traversers/ring.ts b/src/grid/traversers/ring.ts index 59c7687a..209c3df6 100644 --- a/src/grid/traversers/ring.ts +++ b/src/grid/traversers/ring.ts @@ -1,4 +1,5 @@ -import { assertCubeCoordinates, Hex, HexCoordinates } from '../../hex' +import { Hex, HexCoordinates, toCube } from '../../hex' +import { isNumber } from '../../utils' import { distance } from '../functions' import { Rotation, RotationLike, Traverser } from '../types' @@ -16,7 +17,7 @@ export function ring(options: RingOptions | RingFromRadiusOptions let { radius } = options as RingFromRadiusOptions let firstHex: T - if (Number.isFinite(radius)) { + if (isNumber(radius)) { firstHex = createHex(center).translate({ q: radius, s: -radius }) } else { firstHex = createHex((options as RingOptions).start ?? cursor) @@ -24,7 +25,7 @@ export function ring(options: RingOptions | RingFromRadiusOptions } // always start at coordinates radius away from the center, reorder the hexes later - const { q, r, s } = assertCubeCoordinates(firstHex, center) + const { q, r, s } = toCube(firstHex, center) let _cursor = createHex({ q, r: r - radius, s: s + radius }) if (_rotation === Rotation.CLOCKWISE) { diff --git a/src/hex/functions/completeCube.test.ts b/src/hex/functions/completeCube.test.ts new file mode 100644 index 00000000..7a9905ae --- /dev/null +++ b/src/hex/functions/completeCube.test.ts @@ -0,0 +1,21 @@ +import { expect, test } from 'vitest' +import { completeCube } from './completeCube' + +test('returns complete cube coordinates', () => { + expect(completeCube({ q: 1, r: 2, s: -3 })).toEqual({ q: 1, r: 2, s: -3 }) +}) + +test('converts partial cube coordinates to complete cube coordinates', () => { + expect(completeCube({ q: 1, r: 2 })).toEqual({ q: 1, r: 2, s: -3 }) + expect(completeCube({ q: 1, s: 2 })).toEqual({ q: 1, r: -3, s: 2 }) + expect(completeCube({ r: 1, s: 2 })).toEqual({ q: -3, r: 1, s: 2 }) +}) + +test('throws when passed less than 2 coordinates', () => { + // @ts-expect-error + expect(() => completeCube({ q: 1 })).toThrowError( + TypeError( + `Can't determine three cube coordinates from less than two coordinates. Received: { q: 1, r: undefined, s: undefined }.`, + ), + ) +}) diff --git a/src/hex/functions/completeCube.ts b/src/hex/functions/completeCube.ts new file mode 100644 index 00000000..ed728fa4 --- /dev/null +++ b/src/hex/functions/completeCube.ts @@ -0,0 +1,23 @@ +import { isNumber } from '../../utils' +import { CubeCoordinates, PartialCubeCoordinates } from '../types' + +/** + * @category Coordinates + */ +export function completeCube({ q, r, s }: PartialCubeCoordinates): CubeCoordinates { + const validQ = isNumber(q) + const validR = isNumber(r) + const validS = isNumber(s) + + if (validQ && validR && validS) return { q, r, s } + + if (validQ && validR) return { q, r, s: -q - r } + + if (validQ && validS) return { q, r: -q - s, s } + + if (validR && validS) return { q: -r - s, r, s } + + throw new TypeError( + `Can't determine three cube coordinates from less than two coordinates. Received: { q: ${q}, r: ${r}, s: ${s} }.`, + ) +} diff --git a/src/hex/functions/completeCubeCoordinates.ts b/src/hex/functions/completeCubeCoordinates.ts deleted file mode 100644 index 9a9fa50f..00000000 --- a/src/hex/functions/completeCubeCoordinates.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { CubeCoordinates, PartialCubeCoordinates } from '../types' - -/** - * @category Coordinates - */ -export function completeCubeCoordinates({ q, r, s }: PartialCubeCoordinates): CubeCoordinates { - const { 0: definedQ, 1: definedR, 2: definedS, length } = [q, r, s].filter(Number.isFinite) - - if (length === 3) return { q, r, s } as CubeCoordinates - - if (length === 2) { - return ( - definedS == null - ? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - { q: definedQ, r: definedR, s: -definedQ! - definedR! } - : definedR == null - ? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - { q: definedQ, r: -definedQ! - definedS, s: definedS } - : { q: -definedR - definedS, r: definedR, s: definedS } - ) as CubeCoordinates - } - - throw new TypeError( - `Can't determine three cube coordinates from less than two coordinates. Received: { q: ${q}, r: ${r}, s: ${s} }.`, - ) -} diff --git a/src/hex/functions/index.ts b/src/hex/functions/index.ts index f89a6833..e14e717d 100644 --- a/src/hex/functions/index.ts +++ b/src/hex/functions/index.ts @@ -1,5 +1,4 @@ -export * from './assertCubeCoordinates' -export * from './completeCubeCoordinates' +export * from './completeCube' export * from './createHexDimensions' export * from './createHexOrigin' export * from './defineHex' @@ -10,4 +9,5 @@ export * from './isHexInstance' export * from './offsetToCube' export * from './pointToCube' export * from './round' +export * from './toCube' export * from './translate' diff --git a/src/hex/functions/round.ts b/src/hex/functions/round.ts index 5e976b13..4f5a1574 100644 --- a/src/hex/functions/round.ts +++ b/src/hex/functions/round.ts @@ -1,11 +1,11 @@ import { CubeCoordinates, PartialCubeCoordinates } from '../types' -import { completeCubeCoordinates } from './completeCubeCoordinates' +import { completeCube } from './completeCube' /** * @category Hex */ export const round = (coordinates: PartialCubeCoordinates): CubeCoordinates => { - const { q, r, s } = completeCubeCoordinates(coordinates) + const { q, r, s } = completeCube(coordinates) let roundedQ = Math.round(q) let roundedR = Math.round(r) let roundedS = Math.round(s) diff --git a/src/hex/functions/toCube.test.ts b/src/hex/functions/toCube.test.ts new file mode 100644 index 00000000..79e713d7 --- /dev/null +++ b/src/hex/functions/toCube.test.ts @@ -0,0 +1,21 @@ +import { expect, test } from 'vitest' +import { defineHex } from './defineHex' +import { toCube } from './toCube' + +const Hex = defineHex() +const hex = new Hex() + +test('converts tuple coordinates to cube coordinates', () => { + expect(toCube(hex, [1, 2])).toEqual({ q: 1, r: 2, s: -3 }) + expect(toCube(hex, [0, 2, -2])).toEqual({ q: 0, r: 2, s: -2 }) +}) + +test('converts offset coordinates to cube coordinates', () => { + expect(toCube(hex, { col: 1, row: 2 })).toEqual({ q: 0, r: 2, s: -2 }) +}) + +test('converts partial cube coordinates to cube coordinates', () => { + expect(toCube(hex, { q: 1, r: 2 })).toEqual({ q: 1, r: 2, s: -3 }) + expect(toCube(hex, { q: 1, s: 2 })).toEqual({ q: 1, r: -3, s: 2 }) + expect(toCube(hex, { r: 1, s: 2 })).toEqual({ q: -3, r: 1, s: 2 }) +}) diff --git a/src/hex/functions/assertCubeCoordinates.ts b/src/hex/functions/toCube.ts similarity index 61% rename from src/hex/functions/assertCubeCoordinates.ts rename to src/hex/functions/toCube.ts index 7c2ed1ef..2b1a9300 100644 --- a/src/hex/functions/assertCubeCoordinates.ts +++ b/src/hex/functions/toCube.ts @@ -1,7 +1,7 @@ import { isOffset, isTuple, tupleToCube } from '../../utils' import { Hex } from '../hex' import { CubeCoordinates, HexCoordinates } from '../types' -import { completeCubeCoordinates } from './completeCubeCoordinates' +import { completeCube } from './completeCube' import { offsetToCube } from './offsetToCube' /** @@ -9,13 +9,10 @@ import { offsetToCube } from './offsetToCube' * @category Coordinates * @privateRemarks It's not placed in /src/utils because that causes circular dependencies. */ -export function assertCubeCoordinates( - hex: Pick, - coordinates: HexCoordinates, -): CubeCoordinates { - return isOffset(coordinates) - ? offsetToCube(hex, coordinates) - : isTuple(coordinates) +export function toCube(hex: Pick, coordinates: HexCoordinates): CubeCoordinates { + return isTuple(coordinates) ? tupleToCube(coordinates) - : completeCubeCoordinates(coordinates) + : isOffset(coordinates) + ? offsetToCube(hex, coordinates) + : completeCube(coordinates) } diff --git a/src/hex/functions/translate.ts b/src/hex/functions/translate.ts index 2ea3760a..0ebdd648 100644 --- a/src/hex/functions/translate.ts +++ b/src/hex/functions/translate.ts @@ -1,7 +1,7 @@ import { Hex } from '../hex' import { CubeCoordinates, PartialCubeCoordinates } from '../types' -import { assertCubeCoordinates } from './assertCubeCoordinates' -import { completeCubeCoordinates } from './completeCubeCoordinates' +import { completeCube } from './completeCube' +import { toCube } from './toCube' /** * @category Hex @@ -12,13 +12,13 @@ export function translate( input: T | PartialCubeCoordinates, delta: PartialCubeCoordinates, ): T | CubeCoordinates { - const { q: deltaQ, r: deltaR, s: deltaS } = completeCubeCoordinates(delta) + const { q: deltaQ, r: deltaR, s: deltaS } = completeCube(delta) if (input instanceof Hex) { - const { q, r, s } = assertCubeCoordinates(input, input) + const { q, r, s } = toCube(input, input) return input.clone({ q: q + deltaQ, r: r + deltaR, s: s + deltaS }) } - const { q, r, s } = completeCubeCoordinates(input) + const { q, r, s } = completeCube(input) return { q: q + deltaQ, r: r + deltaR, s: s + deltaS } } diff --git a/src/hex/hex.ts b/src/hex/hex.ts index a93996b0..bc0533a7 100644 --- a/src/hex/hex.ts +++ b/src/hex/hex.ts @@ -1,7 +1,7 @@ /* eslint @typescript-eslint/class-literal-property-style: ["error", "getters"] */ import { isOffset } from '../utils' -import { assertCubeCoordinates, equals, hexToOffset, hexToPoint, offsetToCube, translate } from './functions' +import { equals, hexToOffset, hexToPoint, offsetToCube, toCube, translate } from './functions' import { BoundingBox, CubeCoordinates, @@ -96,7 +96,7 @@ export class Hex readonly s: number constructor(coordinates: HexCoordinates = [0, 0]) { - const { q, r, s } = assertCubeCoordinates(this, coordinates) + const { q, r, s } = toCube(this, coordinates) this.q = q this.r = r this.s = s diff --git a/src/utils/index.ts b/src/utils/index.ts index 32bb8543..9f698ac6 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,5 +1,6 @@ export * from './isAxial' export * from './isFunction' +export * from './isNumber' export * from './isObject' export * from './isOffset' export * from './isPoint' diff --git a/src/utils/isAxial.ts b/src/utils/isAxial.ts index f985e68c..04997239 100644 --- a/src/utils/isAxial.ts +++ b/src/utils/isAxial.ts @@ -1,8 +1,9 @@ import { AxialCoordinates } from '../hex' +import { isNumber } from './isNumber' import { isObject } from './isObject' /** * @category Coordinates */ export const isAxial = (value: unknown): value is AxialCoordinates => - isObject(value) && Number.isFinite(value.q) && Number.isFinite(value.r) + isObject(value) && isNumber(value.q) && isNumber(value.r) diff --git a/src/utils/isNumber.ts b/src/utils/isNumber.ts new file mode 100644 index 00000000..f18e45f5 --- /dev/null +++ b/src/utils/isNumber.ts @@ -0,0 +1 @@ +export const isNumber = (value: unknown): value is number => Number.isFinite(value) && !Number.isNaN(value) diff --git a/src/utils/isOffset.ts b/src/utils/isOffset.ts index c224393e..d7c5489f 100644 --- a/src/utils/isOffset.ts +++ b/src/utils/isOffset.ts @@ -1,8 +1,9 @@ import { OffsetCoordinates } from '../hex' +import { isNumber } from './isNumber' import { isObject } from './isObject' /** * @category Coordinates */ export const isOffset = (value: unknown): value is OffsetCoordinates => - isObject(value) && Number.isFinite(value.col) && Number.isFinite(value.row) + isObject(value) && isNumber(value.col) && isNumber(value.row) diff --git a/src/utils/isPoint.ts b/src/utils/isPoint.ts index 79d9feaf..d9586bb7 100644 --- a/src/utils/isPoint.ts +++ b/src/utils/isPoint.ts @@ -1,5 +1,6 @@ import { Point } from '../hex' +import { isNumber } from './isNumber' import { isObject } from './isObject' export const isPoint = (value: unknown): value is Point => - isObject(value) && Number.isFinite(value.x) && Number.isFinite(value.y) + isObject(value) && isNumber(value.x) && isNumber(value.y) diff --git a/src/utils/isTuple.ts b/src/utils/isTuple.ts index 181a1de8..adf53ecb 100644 --- a/src/utils/isTuple.ts +++ b/src/utils/isTuple.ts @@ -1,7 +1,8 @@ import { TupleCoordinates } from '../hex' +import { isNumber } from './isNumber' /** * @category Coordinates */ export const isTuple = (value: unknown): value is TupleCoordinates => - Array.isArray(value) && Number.isFinite(value[0]) && Number.isFinite(value[1]) + Array.isArray(value) && isNumber(value[0]) && isNumber(value[1])