import math | |
const | |
even = 1 | |
odd = -1 | |
hexDirections = @[(1, 0, -1), (1, -1, 0), (0, -1, 1), (-1, 0, 1), (-1, 1, 0), (0, 1, -1)] | |
hexDiagonals = @[(2, -1, -1), (1, -2, 1), (-1, -1, 2), (-2, 1, 1), (-1, 2, -1), (1, 1, -2)] | |
type | |
Point = tuple[x,y: float] | |
Hex = tuple[q, r, s: int] | |
FractionalHex = tuple[q, r, s: float] | |
OffsetCoord = tuple[col, row: int] | |
Orientation = tuple[f0, f1, f2, f3, b0, b1, b2, b3, startAngle: float] | |
Layout = object | |
orientation: Orientation | |
size: Point | |
origin: Point | |
const | |
layoutPointy: Orientation = (sqrt(3.0), sqrt(3.0) / 2.0, 0.0, 3.0 / 2.0, sqrt(3.0) / 3.0, -1.0 / 3.0, 0.0, 2.0 / 3.0, 0.5) | |
layoutFlat: Orientation = (3.0 / 2.0, 0.0, sqrt(3.0) / 2.0, sqrt(3.0), 2.0 / 3.0, 0.0, -1.0 / 3.0, sqrt(3.0) / 3.0, 0.0) | |
proc add(a, b: Hex): Hex = | |
result = (a.q + b.q, a.r + b.r, a.s + b.s) | |
proc subtract(a, b: Hex): Hex = | |
result = (a.q - b.q, a.r - b.r, a.s - b.s) | |
proc scale(a: Hex, k: int): Hex = | |
result = (a.q * k, a.r * k, a.s * k) | |
proc rotateLeft(a: Hex): Hex = | |
result = (-a.s, -a.q, -a.r) | |
proc rotateRight(a: Hex): Hex = | |
result = (-a.r, -a.s, -a.q) | |
proc direction(direction: int): Hex = | |
result = hexDirections[direction] | |
proc neighbor(hex: Hex, direction: int): Hex = | |
result = add(hex, direction(direction)) | |
proc diagonalNeighbor(hex: Hex, direction: int): Hex = | |
result = add(hex, hexDiagonals[direction]) | |
proc length(hex: Hex): int = | |
result = ((abs(hex.q) + abs(hex.r) + abs(hex.s)) / 2).int | |
proc distance(a, b: Hex): int = | |
result = length(subtract(a, b)) | |
proc round(h: FractionalHex): Hex = | |
var q = round(h.q).int | |
var r = round(h.r).int | |
var s = round(h.s).int | |
let qDiff = abs(q.float - h.q) | |
let rDiff = abs(r.float - h.r) | |
let sDiff = abs(s.float - h.s) | |
if qDiff > rDiff and qDiff > sDiff: | |
q = -r - s | |
else: | |
if rDiff > sDiff: | |
r = -q - s | |
else: | |
s = -q - r | |
result = (q, r, s) | |
proc lerp(a, b: FractionalHex, t: float): FractionalHex = | |
return (a.q * (1 - t) + b.q * t, a.r * (1 - t) + b.r * t, a.s * (1 - t) + b.s * t) | |
proc lineDraw(a, b: Hex): seq[Hex] = | |
let n = distance(a, b) | |
let a_nudge = (a.q.float + 0.000001, a.r.float + 0.000001, a.s.float - 0.000002) | |
let bNudge = (b.q.float + 0.000001, b.r.float + 0.000001, b.s.float - 0.000002); | |
result = @[] | |
let step = 1.0 / max(n, 1).float | |
for i in 0..<n: | |
result.add(round(lerp(aNudge, bNudge, step * i.float))) | |
proc qOffsetFromCube(offset: int, h: Hex): OffsetCoord = | |
let col = h.q | |
let row = h.r + ((h.q + offset * (h.q and 1)) / 2).int | |
result = (col, row) | |
proc qOffsetToCube(offset: int, h: OffsetCoord): Hex = | |
let q = h.col | |
let r = h.row - ((h.col + offset * (h.col and 1)) / 2).int | |
let s = -q - r | |
result = (q, r, s) | |
proc rOffsetFromCube(offset: int, h: Hex): OffsetCoord = | |
let col = h.q + ((h.r + offset * (h.r and 1)) / 2).int | |
let row = h.r | |
result = (col, row) | |
proc rOffsetToCube(offset: int, h: OffsetCoord): Hex = | |
let q = h.col - ((h.row + offset * (h.row and 1)) / 2).int | |
let r = h.row | |
let s = -q - r | |
result = (q, r, s) | |
proc hexToPixel(layout: Layout, h: Hex): Point = | |
let m = layout.orientation | |
let size = layout.size | |
let origin = layout.origin | |
let x = (m.f0 * h.q.float + m.f1 * h.r.float) * size.x | |
let y = (m.f2 * h.q.float + m.f3 * h.r.float) * size.y | |
result = (x + origin.x, y + origin.y) | |
proc pixelToHex(layout: Layout, p: Point): FractionalHex = | |
let m = layout.orientation | |
let size = layout.size | |
let origin = layout.origin | |
let pt: Point = ((p.x - origin.x) / size.x, (p.y - origin.y) / size.y) | |
let q = m.b0 * pt.x + m.b1 * pt.y | |
let r = m.b2 * pt.x + m.b3 * pt.y | |
result = (q, r, -q - r) | |
proc hexCornerOffset(layout: Layout, corner: int): Point = | |
let m = layout.orientation | |
let size = layout.size | |
let angle = 2.0 * PI * (m.start_angle - corner.float) / 6 | |
result = (size.x * cos(angle), size.y * sin(angle)) | |
proc polygonCorners(layout: Layout, h: Hex): seq[Point] = | |
result = @[] | |
let center = hex_to_pixel(layout, h) | |
for i in 0..<6: | |
let offset = hexCornerOffset(layout, i) | |
result.add((center.x + offset.x, center.y + offset.y)) | |
when isMainModule: | |
import unittest | |
suite "hex tests": | |
test "arithmetic": | |
check((4, -10, 6) == add((1, -3, 2), (3, -7, 4))) | |
test "direction": | |
check((0, -1, 1) == direction(2)) | |
test "neighbor": | |
check((1, -3, 2) == neighbor((1, -2, 1), 2)) | |
test "diagonal": | |
check((-1, -1, 2) == diagonalNeighbor((1, -2, 1), 3)) | |
test "distance": | |
check(7 == distance((3, -7, 4), (0, 0, 0))) | |
test "rotate right": | |
check((3, -2, -1) == rotateRight((1, -3, 2))) | |
test "rotate right": | |
check((3, -2, -1) == rotateRight((1, -3, 2))) | |
test "rotate left": | |
check((-2, -1, 3) == rotateLeft((1, -3, 2))) | |
test "round": | |
let a: FractionalHex = (0.0, 0.0, 0.0) | |
let b: FractionalHex = (1.0, -1.0, 0.0) | |
let c: FractionalHex = (0.0, -1.0, 1.0) | |
check((5, -10, 5) == round(lerp((0.0, 0.0, 0.0), (10.0, -20.0, 10.0), 0.5))) | |
check(round(a) == round(lerp(a, b, 0.4999))) | |
check(round(b) == round(lerp(a, b, 0.501))) | |
check(round(a) == round((a.q * 0.4 + b.q * 0.3 + c.q * 0.3, a.r * 0.4 + b.r * 0.3 + c.r * 0.3, a.s * 0.4 + b.s * 0.3 + c.s * 0.3))) | |
check(round(c) == round((a.q * 0.3 + b.q * 0.3 + c.q * 0.4, a.r * 0.3 + b.r * 0.3 + c.r * 0.4, a.s * 0.3 + b.s * 0.3 + c.s * 0.4))) | |
test "layout": | |
let h = (3, 4, -7) | |
let flat = Layout( | |
orientation: layoutFlat, | |
size: (10.0, 15.0), | |
origin: (35.0, 71.0) | |
) | |
check(h == round(pixelToHex(flat, hexToPixel(flat, h)))) | |
let pointy = Layout( | |
orientation: layoutPointy, | |
size: (10.0, 15.0), | |
origin: (35.0, 71.0) | |
) | |
check(h == round(pixelToHex(pointy, hexToPixel(pointy, h)))) | |
test "conversion roundtrip": | |
let a = (3, 4, -7) | |
let b = (1, -3) | |
check(a == qOffsetToCube(even, qOffsetFromCube(even, a))) | |
check(b == qOffsetFromCube(even, qOffsetToCube(even, b))) | |
check(a == qOffsetToCube(odd, qOffsetFromCube(odd, a))) | |
check(b == qOffsetFromCube(odd, qOffsetToCube(odd, b))) | |
check(a == rOffsetToCube(even, rOffsetFromCube(even, a))) | |
check(b == rOffsetFromCube(even, rOffsetToCube(even, b))) | |
check(a == rOffsetToCube(odd, rOffsetFromCube(odd, a))) | |
check(b == rOffsetFromCube(odd, rOffsetToCube(odd, b))) | |
test "offset from cube": | |
check((1, 3) == qOffsetFromCube(even, (1, 2, -3))) | |
check((1, 2) == qOffsetFromCube(odd, (1, 2, -3))) | |
test "offset to cube": | |
check((1, 2, -3) == qOffsetToCube(even, (1, 3))) | |
check((1, 2, -3) == qOffsetToCube(odd, (1, 2))) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment