diff --git a/docs/guide/defining-hexes.md b/docs/guide/defining-hexes.md index 4b2a1c25..3fb929d9 100644 --- a/docs/guide/defining-hexes.md +++ b/docs/guide/defining-hexes.md @@ -10,6 +10,6 @@ In a grid with pointy hexes, each row is offsetted half a hex relative to the previous row. In grids with flat hexes, this applies to the columns. Redblobgames has a [visual example](https://www.redblobgames.com/grids/hexagons/#coordinates-offset). -Set the offset property to `1` or `-1` (default) to control whether the even or odd rows/columns are offsetted. Or go loco 🚂 and use other values (at your own risk). +Set the offset property to `1` or `-1` (default) to control whether the even or odd rows/columns are offsetted. ## Customization diff --git a/playground/index.ts b/playground/index.ts index e91ffa2a..8b0abb8a 100644 --- a/playground/index.ts +++ b/playground/index.ts @@ -1,23 +1,28 @@ -import { createHexPrototype, Grid, Hex, rectangle } from '../src' +import { defineHex, Grid, rectangle } from '../src' import { render } from './render' -interface CustomHex extends Hex { - custom: string -} +// interface CustomHex extends Hex { +// custom: string +// } -const hexPrototype = createHexPrototype({ - dimensions: 30, - orientation: 'pointy', - origin: 'topLeft', -}) -const grid = new Grid(hexPrototype, rectangle({ width: 10, height: 10 })) +// this creates this prototype chain: +// CustomHex -> class extends Hex -> Hex -> Object +// while creating a class that extends Hex has this: +// CustomHex -> Hex -> Object +class CustomHex extends defineHex({ dimensions: 30, origin: 'topLeft' }) { + custom = 'test' +} +// class CustomHex extends Hex { +// get dimensions(): Ellipse { +// return createHexDimensions(30) +// } +// get origin(): Point { +// return createHexOrigin('topLeft', this) +// } -/** - * todo: change how directions work: with degrees by default? Compass direction converts to degrees, or maybe drop compass direction altogether? - * todo: add integration tests for concatenating traversers - * todo: traverser should adhere to rules: - * - when a cursor is passed, but no start: skip the first hex (doesn't apply to all traversers) - */ +// custom = 'test' +// } +const grid = new Grid(CustomHex, rectangle({ width: 10, height: 10 })) let i = 0 for (const hex of grid) { diff --git a/playground/render.ts b/playground/render.ts index 93c4b4ff..ccbfe926 100644 --- a/playground/render.ts +++ b/playground/render.ts @@ -1,5 +1,5 @@ import { SVG } from '@svgdotjs/svg.js' -import { Hex } from '../dist' +import { Hex } from '../src' const draw = SVG().addTo('body').size('100%', '100%') diff --git a/src/hex/functions/assertCubeCoordinates.ts b/src/hex/functions/assertCubeCoordinates.ts index 4655545f..7c2ed1ef 100644 --- a/src/hex/functions/assertCubeCoordinates.ts +++ b/src/hex/functions/assertCubeCoordinates.ts @@ -1,5 +1,6 @@ import { isOffset, isTuple, tupleToCube } from '../../utils' -import { CubeCoordinates, HexCoordinates, HexPrototype } from '../types' +import { Hex } from '../hex' +import { CubeCoordinates, HexCoordinates } from '../types' import { completeCubeCoordinates } from './completeCubeCoordinates' import { offsetToCube } from './offsetToCube' @@ -9,11 +10,11 @@ import { offsetToCube } from './offsetToCube' * @privateRemarks It's not placed in /src/utils because that causes circular dependencies. */ export function assertCubeCoordinates( - hexPrototype: Pick, + hex: Pick, coordinates: HexCoordinates, ): CubeCoordinates { return isOffset(coordinates) - ? offsetToCube(hexPrototype, coordinates) + ? offsetToCube(hex, coordinates) : isTuple(coordinates) ? tupleToCube(coordinates) : completeCubeCoordinates(coordinates) diff --git a/src/hex/functions/center.test.ts b/src/hex/functions/center.test.ts deleted file mode 100644 index 1104f922..00000000 --- a/src/hex/functions/center.test.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { expect, test } from 'vitest' -import { center } from './center' -import { createHex } from './createHex' -import { createHexPrototype } from './createHexPrototype' - -test('returns the center point relative to the hex with coordinates {q: 0, r: 0}, when a hex is passed', () => { - const hexPrototype = createHexPrototype({ dimensions: 10, origin: 'topLeft' }) - const hex = createHex(hexPrototype, { q: 1, r: 2 }) - - expect(center(hex)).toEqual({ x: -34.64101615137754, y: -30 }) -}) - -test(`returns the center point relative to any hex's origin, when a hex prototype is passed`, () => { - const hexPrototype = createHexPrototype({ dimensions: 10, origin: { x: 1, y: 2 } }) - expect(center(hexPrototype)).toEqual({ x: 7.6602540378443855, y: 8 }) -}) diff --git a/src/hex/functions/center.ts b/src/hex/functions/center.ts deleted file mode 100644 index 347fe2a1..00000000 --- a/src/hex/functions/center.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Hex, HexPrototype, Point } from '../types' -import { isHex } from './isHex' - -/** - * When passed a **{@link Hex}**, its center relative to the **"origin hex"** (with coordinates `[0, 0]`) is returned. This is different for every hex. - * - * When passed a **{@link HexPrototype}**, the center relative to its **own origin** is returned. This is the same for every hex. - * - * @category Hex - */ -export function center(hexOrPrototype: Hex | Pick): Point { - const { width, height } = hexOrPrototype - const { x, y } = isHex(hexOrPrototype) ? hexOrPrototype : hexOrPrototype.origin - return { x: width / 2 - x, y: height / 2 - y } -} diff --git a/src/hex/functions/cloneHex.test.ts b/src/hex/functions/cloneHex.test.ts deleted file mode 100644 index 03f8472a..00000000 --- a/src/hex/functions/cloneHex.test.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { expect, test } from 'vitest' -import { Hex } from '../types' -import { cloneHex } from './cloneHex' -import { createHex } from './createHex' -import { createHexPrototype } from './createHexPrototype' - -const prototypeOptions = { origin: { x: 10, y: 10 } } -const hexPrototype = createHexPrototype(prototypeOptions) -const hex = createHex(hexPrototype, { q: 1, r: 2 }) - -test('returns a clone of the passed hex', () => { - const result = cloneHex(hex) - - expect(result).toMatchObject(hex) - expect(result).not.toBe(hex) - expect(Object.getPrototypeOf(result)).toBe(hexPrototype) - expect(result.origin).toStrictEqual(prototypeOptions.origin) -}) - -test('returns a clone of the passed hex with the passed properties', () => { - const newProps = { q: 3, r: 4, custom: 'A' } - expect(cloneHex(hex, newProps)).toMatchObject(newProps) -}) - -test('returns a clone of the passed hex with the passed properties containing offset coordinates', () => { - const newProps = { col: 3, row: 4, custom: 'B' } - expect(cloneHex(hex, newProps)).toMatchObject({ q: 1, r: 4, custom: 'B' }) -}) - -test('returns a clone of the passed hex with the passed tuple coordinates', () => { - const newProps = [3, 4] - expect(cloneHex(hex, newProps)).toMatchObject({ q: 3, r: 4 }) -}) - -interface CustomHex extends Hex { - custom: string -} diff --git a/src/hex/functions/cloneHex.ts b/src/hex/functions/cloneHex.ts deleted file mode 100644 index 7ba878fb..00000000 --- a/src/hex/functions/cloneHex.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { isOffset, isTuple, tupleToCube } from '../../utils' -import { Hex, HexCoordinates } from '../types' -import { offsetToCube } from './offsetToCube' - -/** - * @category Hex - */ -export const cloneHex = (hex: T, newProps: Partial | HexCoordinates = {}): T => { - if (isOffset(newProps)) { - const { col, row, ...otherProps } = newProps - const coordinates = offsetToCube(hex, { col, row }) - return Object.assign(Object.create(Object.getPrototypeOf(hex) as T) as T, hex, coordinates, otherProps) - } - - newProps = isTuple(newProps) ? tupleToCube(newProps) : newProps - return Object.assign(Object.create(Object.getPrototypeOf(hex) as T) as T, hex, newProps) -} diff --git a/src/hex/functions/corners.test.ts b/src/hex/functions/corners.test.ts deleted file mode 100644 index ac386890..00000000 --- a/src/hex/functions/corners.test.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { expect, test } from 'vitest' -import { Orientation } from '../types' -import { corners } from './corners' -import { createHex } from './createHex' -import { createHexPrototype } from './createHexPrototype' - -test('returns corners relative to the hex, when a hex is passed', () => { - const pointyHexPrototype = createHexPrototype({ orientation: 'pointy', dimensions: 1 }) - const pointyHex = createHex(pointyHexPrototype, { q: 1, r: 2 }) - const flatHexPrototype = createHexPrototype({ orientation: 'flat', dimensions: 1 }) - const flatHex = createHex(flatHexPrototype, { q: 1, r: 2 }) - - expect(corners(pointyHex)).toEqual([ - { x: 4.330127018922193, y: 2.5 }, - { x: 4.330127018922193, y: 3.5 }, - { x: 3.4641016151377544, y: 4 }, - { x: 2.598076211353316, y: 3.5 }, - { x: 2.598076211353316, y: 2.5 }, - { x: 3.4641016151377544, y: 2 }, - ]) - expect(corners(flatHex)).toEqual([ - { x: 2, y: 3.4641016151377544 }, - { x: 2.5, y: 4.330127018922193 }, - { x: 2, y: 5.196152422706631 }, - { x: 1, y: 5.196152422706631 }, - { x: 0.5, y: 4.330127018922193 }, - { x: 1, y: 3.4641016151377544 }, - ]) -}) - -test(`returns corners relative to any hex's origin, when hex settings are passed`, () => { - const pointyHexSettings = { - orientation: Orientation.POINTY, - dimensions: { xRadius: 1, yRadius: 1 }, - origin: { x: 1, y: 2 }, - } - const flatHexSettings = { - orientation: Orientation.FLAT, - dimensions: { xRadius: 1, yRadius: 1 }, - origin: { x: 1, y: 2 }, - } - - expect(corners(pointyHexSettings)).toEqual([ - { x: 1.8660254037844386, y: 1.5 }, - { x: 1.8660254037844386, y: 2.5 }, - { x: 1, y: 3 }, - { x: 0.1339745962155614, y: 2.5 }, - { x: 0.1339745962155614, y: 1.5 }, - { x: 1, y: 1 }, - ]) - expect(corners(flatHexSettings)).toEqual([ - { x: 1.5, y: 1.1339745962155614 }, - { x: 2, y: 2 }, - { x: 1.5, y: 2.8660254037844384 }, - { x: 0.5, y: 2.8660254037844384 }, - { x: 0, y: 2 }, - { x: 0.5, y: 1.1339745962155614 }, - ]) -}) diff --git a/src/hex/functions/corners.ts b/src/hex/functions/corners.ts deleted file mode 100644 index 1e905eef..00000000 --- a/src/hex/functions/corners.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { Hex, HexSettings, Orientation, Point } from '../types' -import { heightFlat, heightPointy } from './height' -import { hexToPoint } from './hexToPoint' -import { isHex } from './isHex' -import { widthFlat, widthPointy } from './width' - -export const cornersPointy = (width: number, height: number, { x, y }: Point) => [ - { x: x + width * 0.5, y: y - height * 0.25 }, - { x: x + width * 0.5, y: y + height * 0.25 }, - { x, y: y + height * 0.5 }, - { x: x - width * 0.5, y: y + height * 0.25 }, - { x: x - width * 0.5, y: y - height * 0.25 }, - { x, y: y - height * 0.5 }, -] - -export const cornersFlat = (width: number, height: number, { x, y }: Point) => [ - { x: x + width * 0.25, y: y - height * 0.5 }, - { x: x + width * 0.5, y }, - { x: x + width * 0.25, y: y + height * 0.5 }, - { x: x - width * 0.25, y: y + height * 0.5 }, - { x: x - width * 0.5, y }, - { x: x - width * 0.25, y: y - height * 0.5 }, -] - -/** - * When passed a **{@link Hex}**, its corners relative to the **"origin hex"** (with coordinates `[0, 0]`) is returned. This is different for every hex. - * - * When passed **{@link HexSettings}**, the corners relative to its **own origin** is returned. This is the same for every hex. - * - * @category Hex - */ -export function corners(hex: Hex): Point[] -export function corners(hexSettings: Omit): Point[] -export function corners(hexOrHexSettings: Omit): Point[] { - const { - orientation, - dimensions: { xRadius, yRadius }, - } = hexOrHexSettings - const point = isHex(hexOrHexSettings) ? hexToPoint(hexOrHexSettings) : hexOrHexSettings.origin - return orientation === Orientation.POINTY - ? cornersPointy(widthPointy(xRadius), heightPointy(yRadius), point) - : cornersFlat(widthFlat(xRadius), heightFlat(yRadius), point) -} diff --git a/src/hex/functions/createHex.test.ts b/src/hex/functions/createHex.test.ts deleted file mode 100644 index 7924ff55..00000000 --- a/src/hex/functions/createHex.test.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { expect, test } from 'vitest' -import { Hex } from '../types' -import { createHex } from './createHex' -import { createHexPrototype } from './createHexPrototype' - -test('returns a new hex when passed a hex prototype', () => { - const hexPrototype = createHexPrototype() - const result = createHex(hexPrototype) - - expect(result).toMatchObject({ q: 0, r: 0 }) - expect(Object.getPrototypeOf(result)).toBe(hexPrototype) -}) - -test('returns a new hex with the passed properties on the instance', () => { - const hexPrototype = createHexPrototype() - const result = createHex(hexPrototype, { q: 1, r: 2, custom: 'A' }) - - expect(result).toMatchObject({ q: 1, r: 2, custom: 'A' }) - expect(Object.getPrototypeOf(result)).not.toHaveProperty('custom') -}) - -test('returns a new hex from the passed properties containing offset coordinates', () => { - const hexPrototype = createHexPrototype() - const result = createHex(hexPrototype, { col: 1, row: 2, custom: 'B' }) - - expect(result).toMatchObject({ q: 0, r: 2, custom: 'B' }) -}) - -test('returns a new hex from the passed tuple coordinates', () => { - const hexPrototype = createHexPrototype() - const result = createHex(hexPrototype, [1, 2]) - - expect(result).toMatchObject({ q: 1, r: 2 }) -}) - -test('returns a clone when a hex (instance) is passed', () => { - const hexPrototype = createHexPrototype() - const hex = createHex(hexPrototype) - const result = createHex(hex, { q: 1, r: 2, custom: 'A' }) - - expect(result).toMatchObject({ q: 1, r: 2, custom: 'A' }) - expect(result).not.toBe(hex) -}) - -interface CustomHex extends Hex { - custom: string -} diff --git a/src/hex/functions/createHex.ts b/src/hex/functions/createHex.ts deleted file mode 100644 index e7f3491a..00000000 --- a/src/hex/functions/createHex.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { isOffset, isTuple, tupleToCube } from '../../utils' -import { Hex, HexCoordinates } from '../types' -import { isHex } from './isHex' -import { offsetToCube } from './offsetToCube' - -/** - * @category Hex - */ -export const createHex = (prototypeOrHex: T, props: Partial | HexCoordinates = { q: 0, r: 0 }): T => { - if (isHex(prototypeOrHex)) { - return prototypeOrHex.clone(props) - } - - if (isOffset(props)) { - const { col, row, ...otherProps } = props - const coordinates = offsetToCube(prototypeOrHex, { col, row }) - return Object.assign(Object.create(prototypeOrHex) as T, coordinates, otherProps) - } - - props = isTuple(props) ? tupleToCube(props) : props - return Object.assign(Object.create(prototypeOrHex) as T, props) -} diff --git a/src/hex/functions/createHexDimensions.ts b/src/hex/functions/createHexDimensions.ts new file mode 100644 index 00000000..d0fa18e0 --- /dev/null +++ b/src/hex/functions/createHexDimensions.ts @@ -0,0 +1,28 @@ +import { isObject } from '../../utils' +import { BoundingBox, Ellipse, Orientation } from '../types' + +export function createHexDimensions(radius: number): Ellipse +export function createHexDimensions(boundingBox: BoundingBox, orientation: Orientation): Ellipse +export function createHexDimensions(ellipse: Ellipse): Ellipse +export function createHexDimensions(input: number | BoundingBox | Ellipse, orientation?: Orientation): Ellipse { + if (isObject(input) && input.xRadius > 0 && input.yRadius > 0) { + return input + } + + if (isObject(input) && input.width > 0 && input.height > 0) { + const { width, height } = input + return orientation === Orientation.POINTY + ? { xRadius: width / Math.sqrt(3), yRadius: height / 2 } + : { xRadius: width / 2, yRadius: height / Math.sqrt(3) } + } + + if (input > 0) { + return { xRadius: input, yRadius: input } as Ellipse + } + + throw new TypeError( + `Invalid dimensions: ${JSON.stringify( + input, + )}. Dimensions must be expressed as an Ellipse ({ xRadius: number, yRadius: number }), a Rectangle ({ width: number, height: number }) or a number.`, + ) +} diff --git a/src/hex/functions/createHexOrigin.ts b/src/hex/functions/createHexOrigin.ts new file mode 100644 index 00000000..787ff64d --- /dev/null +++ b/src/hex/functions/createHexOrigin.ts @@ -0,0 +1,21 @@ +import { isPoint } from '../../utils' +import { BoundingBox, Point } from '../types' + +export function createHexOrigin(input: 'topLeft', boundingBox: BoundingBox): Point +export function createHexOrigin(input: Point): Point +export function createHexOrigin(input: Point | 'topLeft', boundingBox?: BoundingBox): Point { + if (isPoint(input)) return input + + if (!boundingBox) + throw new TypeError( + `Supply a bounding box ({ width: number, height: number }). Received: ${JSON.stringify(boundingBox)}`, + ) + + if (input === 'topLeft') return { x: boundingBox.width * -0.5, y: boundingBox.height * -0.5 } + + throw new TypeError( + `Invalid origin: ${JSON.stringify( + input, + )}. Origin must be expressed as a Point ({ x: number, y: number }) or the string 'topLeft'.`, + ) +} diff --git a/src/hex/functions/createHexPrototype.test.ts b/src/hex/functions/createHexPrototype.test.ts deleted file mode 100644 index 0e1ce530..00000000 --- a/src/hex/functions/createHexPrototype.test.ts +++ /dev/null @@ -1,239 +0,0 @@ -import { describe, expect, test, vi } from 'vitest' -import { BoundingBox, Ellipse, Hex, HexPrototype, Orientation } from '../types' -import { cloneHex } from './cloneHex' -import { corners } from './corners' -import { createHex } from './createHex' -import { createHexPrototype } from './createHexPrototype' - -vi.mock('./cloneHex') -vi.mock('./corners') - -test('returns the default hex prototype when no options are passed', () => { - const prototype = createHexPrototype() - expect(Object.getOwnPropertyDescriptors(prototype)).toStrictEqual>({ - dimensions: { - value: { xRadius: 1, yRadius: 1 }, - writable: true, - enumerable: true, - configurable: true, - }, - orientation: { - value: Orientation.POINTY, - writable: true, - enumerable: true, - configurable: true, - }, - origin: { - value: { x: 0, y: 0 }, - writable: true, - enumerable: true, - configurable: true, - }, - offset: { value: -1, writable: true, enumerable: true, configurable: true }, - equals: { - value: expect.any(Function), - writable: true, - enumerable: true, - configurable: true, - }, - center: { - get: expect.any(Function), - set: undefined, - enumerable: false, - configurable: false, - }, - clone: { - value: expect.any(Function), - writable: true, - enumerable: true, - configurable: true, - }, - __isHoneycombHex: { - value: true, - writable: false, - enumerable: false, - configurable: false, - }, - col: { - get: expect.any(Function), - set: undefined, - enumerable: false, - configurable: false, - }, - corners: { - get: expect.any(Function), - set: undefined, - enumerable: false, - configurable: false, - }, - height: { - get: expect.any(Function), - set: undefined, - enumerable: false, - configurable: false, - }, - isFlat: { - get: expect.any(Function), - set: undefined, - enumerable: false, - configurable: false, - }, - isPointy: { - get: expect.any(Function), - set: undefined, - enumerable: false, - configurable: false, - }, - row: { - get: expect.any(Function), - set: undefined, - enumerable: false, - configurable: false, - }, - s: { - get: expect.any(Function), - set: expect.any(Function), - enumerable: false, - configurable: false, - }, - toString: { - value: expect.any(Function), - writable: true, - enumerable: true, - configurable: true, - }, - width: { - get: expect.any(Function), - set: undefined, - enumerable: false, - configurable: false, - }, - x: { - get: expect.any(Function), - set: undefined, - enumerable: false, - configurable: false, - }, - y: { - get: expect.any(Function), - set: undefined, - enumerable: false, - configurable: false, - }, - [Symbol.toStringTag]: { - configurable: false, - enumerable: false, - value: 'Hex', - writable: false, - }, - }) -}) - -test('returns the hex prototype with clone method', () => { - const prototype = createHexPrototype() - const newProps = {} - prototype.clone(newProps) - - expect(cloneHex).toBeCalledWith(prototype, newProps) -}) - -test('returns the hex prototype with corners getter', () => { - const prototype = createHexPrototype() - prototype.corners - - expect(corners).toBeCalledWith(prototype) -}) - -describe('equals()', () => { - test('returns the hex prototype with equals method', () => { - const prototype = createHexPrototype() - expect(prototype.equals.call({ q: 1, r: 2 } as Hex, { q: 1, r: 2 })).toBe(true) - }) - - test('accepts offset coordinates', () => { - const prototype = createHexPrototype() - expect(prototype.equals.call({ q: 1, r: 2 } as Hex, { col: 1, row: 2 })).toBe(true) - }) -}) - -test('returns the hex prototype with toString method', () => { - const prototype = createHexPrototype() - const hex = createHex(prototype, { q: 1, r: 2 }) - const result = hex.toString() - - expect(result).toBe('1,2') -}) - -describe('dimensions', () => { - test('accepts an ellipse', () => { - const prototype = createHexPrototype({ dimensions: { xRadius: 1, yRadius: 2 } }) - expect(prototype.dimensions).toEqual({ xRadius: 1, yRadius: 2 }) - }) - - test('accepts a rectangular bounding box', () => { - const pointyPrototype = createHexPrototype({ orientation: 'pointy', dimensions: { width: 10, height: 20 } }) - expect(pointyPrototype.dimensions).toEqual({ xRadius: 5.773502691896258, yRadius: 10 }) - - const flatPrototype = createHexPrototype({ orientation: 'flat', dimensions: { width: 10, height: 20 } }) - expect(flatPrototype.dimensions).toEqual({ xRadius: 5, yRadius: 11.547005383792516 }) - }) - - test('accepts a radius', () => { - const prototype = createHexPrototype({ dimensions: 1 }) - expect(prototype.dimensions).toEqual({ xRadius: 1, yRadius: 1 }) - }) - - test('throws for invalid dimensions', () => { - const invalidEllipse: Ellipse = { xRadius: -1, yRadius: -2 } - expect(() => createHexPrototype({ dimensions: invalidEllipse })).toThrow( - `Invalid dimensions: ${JSON.stringify( - invalidEllipse, - )}. Dimensions must be expressed as an Ellipse ({ xRadius: number, yRadius: number }), a Rectangle ({ width: number, height: number }) or a number.`, - ) - - const invalidBoundingBox: BoundingBox = { width: -1, height: -2 } - expect(() => createHexPrototype({ dimensions: invalidBoundingBox })).toThrow( - `Invalid dimensions: ${JSON.stringify( - invalidBoundingBox, - )}. Dimensions must be expressed as an Ellipse ({ xRadius: number, yRadius: number }), a Rectangle ({ width: number, height: number }) or a number.`, - ) - - const invalidRadius = -1 - expect(() => createHexPrototype({ dimensions: invalidRadius })).toThrow( - `Invalid dimensions: ${JSON.stringify( - invalidRadius, - )}. Dimensions must be expressed as an Ellipse ({ xRadius: number, yRadius: number }), a Rectangle ({ width: number, height: number }) or a number.`, - ) - }) -}) - -describe('orientation', () => { - test(`accepts Orientation, 'pointy' or 'flat'`, () => { - expect(createHexPrototype({ orientation: Orientation.POINTY }).orientation).toBe(Orientation.POINTY) - expect(createHexPrototype({ orientation: 'pointy' }).orientation).toBe(Orientation.POINTY) - expect(createHexPrototype({ orientation: 'flat' }).orientation).toBe(Orientation.FLAT) - }) -}) - -describe('origin', () => { - test('accepts a point', () => { - const prototype = createHexPrototype({ origin: { x: 1, y: 2 } }) - expect(prototype.origin).toEqual({ x: 1, y: 2 }) - }) - - test(`accepts 'topLeft'`, () => { - const prototype = createHexPrototype({ origin: 'topLeft', dimensions: { width: 10, height: 10 } }) - expect(prototype.origin).toEqual({ x: -5, y: -5 }) - }) - - test('accepts a function', () => { - const callback = vi.fn(() => ({ x: 1, y: 2 })) - const prototype = createHexPrototype({ origin: callback }) - - expect(callback).toBeCalledWith(prototype) - expect(prototype.origin).toEqual({ x: 1, y: 2 }) - }) -}) - -// copied from internal type that Object.getOwnPropertyDescriptors() returns -type TypedPropertyDescriptors = { [P in keyof T]: TypedPropertyDescriptor } | PropertyDescriptorMap diff --git a/src/hex/functions/createHexPrototype.ts b/src/hex/functions/createHexPrototype.ts deleted file mode 100644 index b00b179c..00000000 --- a/src/hex/functions/createHexPrototype.ts +++ /dev/null @@ -1,204 +0,0 @@ -import { isFunction, isObject, isOffset, isPoint } from '../../utils' -import { BoundingBox, Ellipse, Hex, HexPrototype, HexSettings, Orientation, Point } from '../types' -import { center } from './center' -import { cloneHex } from './cloneHex' -import { corners } from './corners' -import { equals } from './equals' -import { height } from './height' -import { hexToOffset } from './hexToOffset' -import { hexToPoint } from './hexToPoint' -import { isFlat } from './isFlat' -import { isPointy } from './isPointy' -import { offsetToCube } from './offsetToCube' -import { width } from './width' - -/** - * @hidden - * @category Hex - */ -export const defaultHexSettings: HexSettings = { - dimensions: { xRadius: 1, yRadius: 1 }, - orientation: Orientation.POINTY, - origin: { x: 0, y: 0 }, - offset: -1, -} - -/** - * @category Hex - * @remarks ⚠️ The methods `clone()`, `equals()` and `toString()` are "protected". - * If you custom hex prototype overwrites these methods, things may break unexpectedly. - */ -export const createHexPrototype = ( - options?: Partial | HexPrototypeOptions>, -): T => { - // pseudo private property - const s = new WeakMap() - - const prototype = { - ...defaultHexSettings, - clone(newProps) { - return cloneHex(this, newProps) - }, - equals(coordinates) { - return equals(this, isOffset(coordinates) ? offsetToCube(this, coordinates) : coordinates) - }, - toString() { - return `${this.q},${this.r}` - }, - ...options, - } as T & HexPrototypeOptions - - // use Object.defineProperties() to create readonly properties - // origin is set in the final "step" - Object.defineProperties(prototype, { - [Symbol.toStringTag]: { value: 'Hex' }, - __isHoneycombHex: { value: true, writable: false }, - // 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) - center: { - get() { - return center(this) - }, - }, - col: { - get() { - return hexToOffset(this).col - }, - }, - corners: { - get() { - return corners(this) - }, - }, - dimensions: { value: normalizeDimensions(prototype) }, - height: { - get() { - return height(this) - }, - }, - isFlat: { - get() { - return isFlat(this) - }, - }, - isPointy: { - get() { - return isPointy(this) - }, - }, - orientation: { value: normalizeOrientation(prototype) }, - offset: { value: assertOffset(prototype) }, - row: { - get() { - return hexToOffset(this).row - }, - }, - s: { - get() { - const _s = s.get(this) - return Number.isFinite(_s) ? _s : -this.q - this.r - }, - set(_s: number) { - s.set(this, _s) - }, - }, - width: { - get() { - return width(this) - }, - }, - x: { - get() { - return hexToPoint(this).x - }, - }, - y: { - get() { - return hexToPoint(this).y - }, - }, - } as PropertyDescriptorMap & ThisType) - - return Object.defineProperties(prototype, { - origin: { value: normalizeOrigin(prototype) }, - }) -} - -/** - * @category Hex - */ -export type OriginFn = >(prototype: T) => Point - -/** - * @category Hex - */ -export interface HexPrototypeOptions { - dimensions: Ellipse | BoundingBox | number - orientation: Orientation | 'pointy' | 'flat' - origin: Point | 'topLeft' | OriginFn - offset: number -} - -function normalizeDimensions(prototype: HexPrototypeOptions) { - const { dimensions } = prototype - - if (isObject(dimensions)) { - if ((dimensions as Ellipse).xRadius > 0 && (dimensions as Ellipse).yRadius > 0) { - return { ...(dimensions as Ellipse) } - } - - const { width, height } = dimensions as BoundingBox - if (width > 0 && height > 0) { - return normalizeOrientation(prototype) === Orientation.POINTY - ? { xRadius: width / Math.sqrt(3), yRadius: height / 2 } - : { xRadius: width / 2, yRadius: height / Math.sqrt(3) } - } - } - - if (dimensions > 0) { - return { xRadius: dimensions, yRadius: dimensions } as Ellipse - } - - throw new TypeError( - `Invalid dimensions: ${JSON.stringify( - dimensions, - )}. Dimensions must be expressed as an Ellipse ({ xRadius: number, yRadius: number }), a Rectangle ({ width: number, height: number }) or a number.`, - ) -} - -function normalizeOrientation({ orientation }: HexPrototypeOptions) { - return orientation.toUpperCase() as Orientation -} - -function assertOffset({ offset }: HexPrototypeOptions) { - if (!Number.isFinite(offset)) { - throw new TypeError(`Invalid offset: ${offset}. Offset must be a number.`) - } - - return offset -} - -// origin can be a function that will be called with the almost-complete hex prototype (complete except for origin) -function normalizeOrigin( - prototype: Omit & Pick, -): Point { - const { origin } = prototype - - if (isPoint(origin)) { - return { ...origin } - } - - if (origin === 'topLeft') { - return { x: prototype.width * -0.5, y: prototype.height * -0.5 } - } - - if (isFunction(origin)) { - return origin(prototype) - } - - throw new TypeError( - `Invalid origin: ${JSON.stringify( - origin, - )}. Origin must be expressed as a Point ({ x: number, y: number }), 'topLeft' or a function that returns a Point.`, - ) -} diff --git a/src/hex/functions/defineHex.ts b/src/hex/functions/defineHex.ts new file mode 100644 index 00000000..88cfdd30 --- /dev/null +++ b/src/hex/functions/defineHex.ts @@ -0,0 +1,26 @@ +import { defaultHexSettings, Hex } from '../hex' +import { BoundingBox, Ellipse, HexOffset, HexOptions, Orientation, Point } from '../types' +import { createHexDimensions } from './createHexDimensions' +import { createHexOrigin } from './createHexOrigin' + +export function defineHex(hexPrototypeOptions?: Partial): typeof Hex { + const { dimensions, orientation, origin, offset } = { ...defaultHexSettings, ...hexPrototypeOptions } + + return class extends Hex { + get dimensions(): Ellipse { + return createHexDimensions(dimensions as BoundingBox, orientation) + } + + get orientation(): Orientation { + return orientation + } + + get origin(): Point { + return createHexOrigin(origin as 'topLeft', this) + } + + get offset(): HexOffset { + return offset + } + } +} diff --git a/src/hex/functions/height.test.ts b/src/hex/functions/height.test.ts deleted file mode 100644 index 8e0fe225..00000000 --- a/src/hex/functions/height.test.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { expect, test } from 'vitest' -import { createHex } from './createHex' -import { createHexPrototype } from './createHexPrototype' -import { height } from './height' - -test(`returns the hex's height`, () => { - const pointyHexPrototype = createHexPrototype({ orientation: 'pointy', dimensions: { xRadius: 1, yRadius: 1 } }) - const pointyHex = createHex(pointyHexPrototype) - const flatHexPrototype = createHexPrototype({ orientation: 'flat', dimensions: { xRadius: 1, yRadius: 1 } }) - const flatHex = createHex(flatHexPrototype) - - expect(height(pointyHex)).toBe(2) - expect(height(flatHex)).toBe(1.7320508075688772) -}) diff --git a/src/hex/functions/height.ts b/src/hex/functions/height.ts deleted file mode 100644 index f049890d..00000000 --- a/src/hex/functions/height.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { HexSettings, Orientation } from '../types' - -/** - * @hidden - */ -export const heightPointy = (yRadius: number) => yRadius * 2 - -/** - * @hidden - */ -export const heightFlat = (yRadius: number) => yRadius * Math.sqrt(3) - -/** - * @category Hex - */ -export const height = ({ orientation, dimensions: { yRadius } }: HexSettings) => - orientation === Orientation.POINTY ? heightPointy(yRadius) : heightFlat(yRadius) diff --git a/src/hex/functions/hexToOffset.ts b/src/hex/functions/hexToOffset.ts index ca5a35ca..36d816b5 100644 --- a/src/hex/functions/hexToOffset.ts +++ b/src/hex/functions/hexToOffset.ts @@ -1,12 +1,13 @@ import { offsetFromZero } from '../../utils' -import { Hex, OffsetCoordinates } from '../types' +import { Hex } from '../hex' +import { HexOffset, OffsetCoordinates } from '../types' -const hexToOffsetPointy = (q: number, r: number, offset: number): OffsetCoordinates => ({ +const hexToOffsetPointy = (q: number, r: number, offset: HexOffset): OffsetCoordinates => ({ col: q + offsetFromZero(offset, r), row: r, }) -const hexToOffsetFlat = (q: number, r: number, offset: number): OffsetCoordinates => ({ +const hexToOffsetFlat = (q: number, r: number, offset: HexOffset): OffsetCoordinates => ({ col: q, row: r + offsetFromZero(offset, q), }) diff --git a/src/hex/functions/hexToPoint.ts b/src/hex/functions/hexToPoint.ts index c72961b5..1f841ec8 100644 --- a/src/hex/functions/hexToPoint.ts +++ b/src/hex/functions/hexToPoint.ts @@ -1,4 +1,5 @@ -import { Hex, Orientation, Point } from '../types' +import { Hex } from '../hex' +import { Orientation, Point } from '../types' /** * @category Hex diff --git a/src/hex/functions/index.ts b/src/hex/functions/index.ts index 9c614ce0..f89a6833 100644 --- a/src/hex/functions/index.ts +++ b/src/hex/functions/index.ts @@ -1,19 +1,13 @@ export * from './assertCubeCoordinates' -export * from './center' -export * from './cloneHex' export * from './completeCubeCoordinates' -export * from './corners' -export * from './createHex' -export * from './createHexPrototype' +export * from './createHexDimensions' +export * from './createHexOrigin' +export * from './defineHex' export * from './equals' -export * from './height' export * from './hexToOffset' export * from './hexToPoint' -export * from './isFlat' -export * from './isHex' -export * from './isPointy' +export * from './isHexInstance' export * from './offsetToCube' export * from './pointToCube' export * from './round' export * from './translate' -export * from './width' diff --git a/src/hex/functions/isFlat.test.ts b/src/hex/functions/isFlat.test.ts deleted file mode 100644 index 62d5cccc..00000000 --- a/src/hex/functions/isFlat.test.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { expect, test } from 'vitest' -import { createHex } from './createHex' -import { createHexPrototype } from './createHexPrototype' -import { isFlat } from './isFlat' - -test('returns whether the passed hex has a flat orientation', () => { - const pointyHexPrototype = createHexPrototype({ orientation: 'pointy' }) - const pointyHex = createHex(pointyHexPrototype) - const flatHexPrototype = createHexPrototype({ orientation: 'flat' }) - const flatHex = createHex(flatHexPrototype) - - expect(isFlat(pointyHex)).toBe(false) - expect(isFlat(flatHex)).toBe(true) -}) diff --git a/src/hex/functions/isFlat.ts b/src/hex/functions/isFlat.ts deleted file mode 100644 index 54dfb2f8..00000000 --- a/src/hex/functions/isFlat.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { HexSettings, Orientation } from '../types' - -/** - * @category Hex - */ -export const isFlat = ({ orientation }: HexSettings) => orientation === Orientation.FLAT diff --git a/src/hex/functions/isHex.test.ts b/src/hex/functions/isHex.test.ts deleted file mode 100644 index 0c2d1eff..00000000 --- a/src/hex/functions/isHex.test.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { expect, test } from 'vitest' -import { createHex } from './createHex' -import { createHexPrototype } from './createHexPrototype' -import { isHex } from './isHex' - -test('returns whether the passed value is a hex (i.e. an object with the hex prototype)', () => { - const hexPrototype = createHexPrototype() - const hex = createHex(hexPrototype) - - expect(isHex(hex)).toBe(true) - expect(isHex(hexPrototype)).toBe(false) - expect(isHex({})).toBe(false) - expect(isHex(1)).toBe(false) -}) diff --git a/src/hex/functions/isHex.ts b/src/hex/functions/isHex.ts deleted file mode 100644 index 860bfcce..00000000 --- a/src/hex/functions/isHex.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { isObject } from '../../utils' -import { Hex } from '../types' - -/** - * @category Hex - */ -export const isHex = (value: unknown): value is Hex => - isObject(value) && !!(Object.getPrototypeOf(value) as Hex).__isHoneycombHex diff --git a/src/hex/functions/isPointy.test.ts b/src/hex/functions/isPointy.test.ts deleted file mode 100644 index 31150e1d..00000000 --- a/src/hex/functions/isPointy.test.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { expect, test } from 'vitest' -import { createHex } from './createHex' -import { createHexPrototype } from './createHexPrototype' -import { isPointy } from './isPointy' - -test('returns whether the passed hex has a flat orientation', () => { - const pointyHexPrototype = createHexPrototype({ orientation: 'pointy' }) - const pointyHex = createHex(pointyHexPrototype) - const flatHexPrototype = createHexPrototype({ orientation: 'flat' }) - const flatHex = createHex(flatHexPrototype) - - expect(isPointy(pointyHex)).toBe(true) - expect(isPointy(flatHex)).toBe(false) -}) diff --git a/src/hex/functions/isPointy.ts b/src/hex/functions/isPointy.ts deleted file mode 100644 index 30326404..00000000 --- a/src/hex/functions/isPointy.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { HexSettings, Orientation } from '../types' - -/** - * @category Hex - */ -export const isPointy = ({ orientation }: HexSettings) => orientation === Orientation.POINTY diff --git a/src/hex/functions/offsetToCube.ts b/src/hex/functions/offsetToCube.ts index a7980066..28ea9d2a 100644 --- a/src/hex/functions/offsetToCube.ts +++ b/src/hex/functions/offsetToCube.ts @@ -1,10 +1,11 @@ import { offsetFromZero } from '../../utils' -import { CubeCoordinates, HexPrototype, OffsetCoordinates } from '../types' +import { Hex } from '../hex' +import { CubeCoordinates, HexOffset, OffsetCoordinates } from '../types' /** * @hidden */ -export const offsetToCubePointy = (col: number, row: number, offset: number): CubeCoordinates => { +export const offsetToCubePointy = (col: number, row: number, offset: HexOffset): CubeCoordinates => { const q = col - offsetFromZero(offset, row) const r = row const s = -q - r @@ -14,7 +15,7 @@ export const offsetToCubePointy = (col: number, row: number, offset: number): Cu /** * @hidden */ -export const offsetToCubeFlat = (col: number, row: number, offset: number): CubeCoordinates => { +export const offsetToCubeFlat = (col: number, row: number, offset: HexOffset): CubeCoordinates => { const q = col const r = row - offsetFromZero(offset, col) const s = -q - r @@ -24,7 +25,5 @@ export const offsetToCubeFlat = (col: number, row: number, offset: number): Cube /** * @category Hex */ -export const offsetToCube = ( - { offset, isPointy }: Pick, - { col, row }: OffsetCoordinates, -) => (isPointy ? offsetToCubePointy(col, row, offset) : offsetToCubeFlat(col, row, offset)) +export const offsetToCube = ({ offset, isPointy }: Pick, { col, row }: OffsetCoordinates) => + isPointy ? offsetToCubePointy(col, row, offset) : offsetToCubeFlat(col, row, offset) diff --git a/src/hex/functions/pointToCube.ts b/src/hex/functions/pointToCube.ts index e6b4d97e..adaa0400 100644 --- a/src/hex/functions/pointToCube.ts +++ b/src/hex/functions/pointToCube.ts @@ -1,4 +1,5 @@ -import { HexPrototype, Point } from '../types' +import { Hex } from '../hex' +import { Point } from '../types' import { round } from './round' // inspired by https://github.com/gojuno/hexgrid-py @@ -8,7 +9,7 @@ import { round } from './round' * @category Hex */ export const pointToCube = ( - { dimensions: { xRadius, yRadius }, origin, isPointy }: Pick, + { dimensions: { xRadius, yRadius }, origin, isPointy }: Pick, { x, y }: Point, ) => { x += origin.x diff --git a/src/hex/functions/translate.ts b/src/hex/functions/translate.ts index 9a426cca..2ea3760a 100644 --- a/src/hex/functions/translate.ts +++ b/src/hex/functions/translate.ts @@ -1,9 +1,8 @@ -import { CubeCoordinates, Hex, PartialCubeCoordinates } from '../types' +import { Hex } from '../hex' +import { CubeCoordinates, PartialCubeCoordinates } from '../types' import { assertCubeCoordinates } from './assertCubeCoordinates' import { completeCubeCoordinates } from './completeCubeCoordinates' -import { isHex } from './isHex' -// todo: add to hex prototype /** * @category Hex */ @@ -15,7 +14,7 @@ export function translate( ): T | CubeCoordinates { const { q: deltaQ, r: deltaR, s: deltaS } = completeCubeCoordinates(delta) - if (isHex(input)) { + if (input instanceof Hex) { const { q, r, s } = assertCubeCoordinates(input, input) return input.clone({ q: q + deltaQ, r: r + deltaR, s: s + deltaS }) } diff --git a/src/hex/functions/width.test.ts b/src/hex/functions/width.test.ts deleted file mode 100644 index ad78b35e..00000000 --- a/src/hex/functions/width.test.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { expect, test } from 'vitest' -import { createHex } from './createHex' -import { createHexPrototype } from './createHexPrototype' -import { width } from './width' - -test(`returns the hex's width`, () => { - const pointyHexPrototype = createHexPrototype({ orientation: 'pointy', dimensions: { xRadius: 1, yRadius: 1 } }) - const pointyHex = createHex(pointyHexPrototype) - const flatHexPrototype = createHexPrototype({ orientation: 'flat', dimensions: { xRadius: 1, yRadius: 1 } }) - const flatHex = createHex(flatHexPrototype) - - expect(width(pointyHex)).toBe(1.7320508075688772) - expect(width(flatHex)).toBe(2) -}) diff --git a/src/hex/functions/width.ts b/src/hex/functions/width.ts deleted file mode 100644 index 5fcfb79d..00000000 --- a/src/hex/functions/width.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { HexSettings, Orientation } from '../types' - -/** - * @hidden - */ -export const widthPointy = (xRadius: number) => xRadius * Math.sqrt(3) - -/** - * @hidden - */ -export const widthFlat = (xRadius: number) => xRadius * 2 - -/** - * @category Hex - */ -export const width = ({ orientation, dimensions: { xRadius } }: HexSettings) => - orientation === Orientation.POINTY ? widthPointy(xRadius) : widthFlat(xRadius) diff --git a/src/hex/hex.ts b/src/hex/hex.ts new file mode 100644 index 00000000..a93996b0 --- /dev/null +++ b/src/hex/hex.ts @@ -0,0 +1,149 @@ +/* eslint @typescript-eslint/class-literal-property-style: ["error", "getters"] */ + +import { isOffset } from '../utils' +import { assertCubeCoordinates, equals, hexToOffset, hexToPoint, offsetToCube, translate } from './functions' +import { + BoundingBox, + CubeCoordinates, + Ellipse, + HexConstructor, + HexCoordinates, + HexOffset, + HexSettings, + OffsetCoordinates, + Orientation, + PartialCubeCoordinates, + Point, +} from './types' + +export class Hex + implements Readonly, Readonly, Readonly, Readonly +{ + get [Symbol.toStringTag](): string { + return `Hex(${this.toString()})` + } + + // todo: add to docs that this always returns a point relative to Hex(0, 0) + get center(): Point { + const { width, height, x, y } = this + return { x: width / 2 - x, y: height / 2 - y } + } + + get col(): number { + return hexToOffset(this).col + } + + // todo: add to docs that this always returns corners relative to Hex(0, 0) + get corners(): Point[] { + const { orientation, width, height, x, y } = this + return orientation === Orientation.POINTY ? cornersPointy(width, height, x, y) : cornersFlat(width, height, x, y) + } + + get dimensions(): Ellipse { + return defaultHexSettings.dimensions + } + + get height(): number { + const { + orientation, + dimensions: { yRadius }, + } = this + return orientation === Orientation.POINTY ? yRadius * 2 : yRadius * Math.sqrt(3) + } + + get isFlat(): boolean { + return this.orientation === Orientation.FLAT + } + + get isPointy(): boolean { + return this.orientation === Orientation.POINTY + } + + get orientation(): Orientation { + return defaultHexSettings.orientation + } + + get origin(): Point { + return defaultHexSettings.origin + } + + get offset(): HexOffset { + return defaultHexSettings.offset + } + + get row(): number { + return hexToOffset(this).row + } + + get width(): number { + const { + orientation, + dimensions: { xRadius }, + } = this + return orientation === Orientation.POINTY ? xRadius * Math.sqrt(3) : xRadius * 2 + } + + get x(): number { + return hexToPoint(this).x + } + + get y(): number { + return hexToPoint(this).y + } + + readonly q: number + readonly r: number + readonly s: number + + constructor(coordinates: HexCoordinates = [0, 0]) { + const { q, r, s } = assertCubeCoordinates(this, coordinates) + this.q = q + this.r = r + this.s = s + } + + // todo: test if this works intuitively when Hex is extended with custom props + clone(newProps?: HexCoordinates): T { + return new (this.constructor as HexConstructor)(newProps) + } + + equals(coordinates: HexCoordinates) { + return equals(this, isOffset(coordinates) ? offsetToCube(this, coordinates) : coordinates) + } + + toString() { + return `${this.q},${this.r}` + } + + translate(delta: PartialCubeCoordinates) { + return translate(this, delta) + } +} + +/** + * @hidden + */ +export const defaultHexSettings: HexSettings = { + dimensions: { xRadius: 1, yRadius: 1 }, + orientation: Orientation.POINTY, + origin: { x: 0, y: 0 }, + offset: -1, +} + +const cornersPointy = (width: number, height: number, x: number, y: number) => [ + { x: x + width * 0.5, y: y - height * 0.25 }, + { x: x + width * 0.5, y: y + height * 0.25 }, + { x, y: y + height * 0.5 }, + { x: x - width * 0.5, y: y + height * 0.25 }, + { x: x - width * 0.5, y: y - height * 0.25 }, + { x, y: y - height * 0.5 }, +] + +const cornersFlat = (width: number, height: number, x: number, y: number) => [ + { x: x + width * 0.25, y: y - height * 0.5 }, + { x: x + width * 0.5, y }, + { x: x + width * 0.25, y: y + height * 0.5 }, + { x: x - width * 0.25, y: y + height * 0.5 }, + { x: x - width * 0.5, y }, + { x: x - width * 0.25, y: y - height * 0.5 }, +] diff --git a/src/hex/index.ts b/src/hex/index.ts index ab6b3541..0d6cb695 100644 --- a/src/hex/index.ts +++ b/src/hex/index.ts @@ -1,2 +1,3 @@ export * from './functions' +export * from './hex' export * from './types' diff --git a/src/hex/types.ts b/src/hex/types.ts index 0f21aea5..4dcf2e1d 100644 --- a/src/hex/types.ts +++ b/src/hex/types.ts @@ -1,3 +1,5 @@ +import { Hex } from './hex' + /** * @category Coordinates */ @@ -34,7 +36,9 @@ export interface AxialCoordinates { /** * @category Coordinates */ -export interface CubeCoordinates extends AxialCoordinates { +export interface CubeCoordinates { + q: number + r: number s: number } @@ -69,7 +73,7 @@ export interface BoundingBox { /** * @category Hex */ -export type hexDimensions = Ellipse | BoundingBox | number +export type HexOffset = 1 | -1 /** * @category Hex @@ -78,34 +82,20 @@ export interface HexSettings { dimensions: Ellipse orientation: Orientation origin: Point - offset: number + offset: HexOffset } /** * @category Hex */ -export interface HexPrototype extends HexSettings { - readonly __isHoneycombHex: true - readonly [Symbol.toStringTag]: 'Hex' - readonly center: Point - readonly col: number - readonly corners: Point[] - readonly height: number - readonly isFlat: boolean - readonly isPointy: boolean - readonly row: number - readonly width: number - readonly x: number - readonly y: number - - s: number - - equals(this: this, coordinates: HexCoordinates): boolean - clone(this: this, newProps?: Partial | HexCoordinates): this - toString(this: this): string +export interface HexOptions { + dimensions: Ellipse | BoundingBox | number + orientation: Orientation + origin: Point | 'topLeft' + offset: HexOffset } /** * @category Hex */ -export interface Hex extends HexPrototype, AxialCoordinates {} +export type HexConstructor = new (coordinates?: HexCoordinates) => T diff --git a/src/utils/isObject.ts b/src/utils/isObject.ts index 9a9c3685..64df4c9a 100644 --- a/src/utils/isObject.ts +++ b/src/utils/isObject.ts @@ -1,5 +1,5 @@ /** - * In TypeScript: pass a type variable to isObject() for best result. E.g.: `isObject(value)` + * Pass a type variable to isObject() for best results. E.g.: `isObject(value)`. */ export const isObject = >(value: unknown): value is T => typeof value === 'object' && value !== null diff --git a/src/utils/offsetFromZero.ts b/src/utils/offsetFromZero.ts index c9b69ff2..2989764f 100644 --- a/src/utils/offsetFromZero.ts +++ b/src/utils/offsetFromZero.ts @@ -1,3 +1,6 @@ // todo: rename (also rename offset)? + +import { HexOffset } from '../hex' + // todo: change to https://www.redblobgames.com/grids/hexagons/#conversions-offset -export const offsetFromZero = (offset: number, distance: number) => (distance + offset * (distance & 1)) >> 1 +export const offsetFromZero = (offset: HexOffset, distance: number) => (distance + offset * (distance & 1)) >> 1