import type { AxialCoord, PixelCoord, HexGeometry } from './types.js'; import { HexEdge, EDGE_DIRECTIONS, ALL_EDGES } from './types.js'; const SQRT3 = Math.sqrt(3); /** Convert axial coords to pixel center (flat-top hex) */ export function axialToPixel(coord: AxialCoord, size: number, origin: PixelCoord = { x: 0, y: 0 }): PixelCoord { return { x: origin.x + size * (3 / 2) * coord.q, y: origin.y + size * (SQRT3 / 2 * coord.q + SQRT3 * coord.r), }; } /** Convert pixel position to fractional axial coords (flat-top hex) */ export function pixelToAxial(pixel: PixelCoord, size: number, origin: PixelCoord = { x: 0, y: 0 }): AxialCoord { const px = pixel.x - origin.x; const py = pixel.y - origin.y; const q = (2 / 3) * px / size; const r = (-1 / 3 * px + SQRT3 / 3 * py) / size; return axialRound({ q, r }); } /** Round fractional axial coords to nearest hex */ export function axialRound(coord: AxialCoord): AxialCoord { const s = -coord.q - coord.r; let rq = Math.round(coord.q); let rr = Math.round(coord.r); const rs = Math.round(s); const qDiff = Math.abs(rq - coord.q); const rDiff = Math.abs(rr - coord.r); const sDiff = Math.abs(rs - s); if (qDiff > rDiff && qDiff > sDiff) { rq = -rr - rs; } else if (rDiff > sDiff) { rr = -rq - rs; } return { q: rq, r: rr }; } /** Get the neighbor of a hex in a given direction */ export function getNeighbor(coord: AxialCoord, edge: HexEdge): AxialCoord { const dir = EDGE_DIRECTIONS[edge]; return { q: coord.q + dir.q, r: coord.r + dir.r }; } /** Get all 6 neighbors */ export function getNeighbors(coord: AxialCoord): AxialCoord[] { return ALL_EDGES.map(edge => getNeighbor(coord, edge)); } /** Axial distance between two hexes */ export function axialDistance(a: AxialCoord, b: AxialCoord): number { const dq = a.q - b.q; const dr = a.r - b.r; return (Math.abs(dq) + Math.abs(dq + dr) + Math.abs(dr)) / 2; } /** Create a unique string key for a hex coordinate */ export function coordKey(coord: AxialCoord): string { return `${coord.q},${coord.r}`; } /** Parse a coordinate key back to AxialCoord */ export function parseCoordKey(key: string): AxialCoord { const [q, r] = key.split(',').map(Number); return { q, r }; } /** Compute the 6 vertices of a flat-top hex */ export function hexVertices(cx: number, cy: number, size: number): PixelCoord[] { const vertices: PixelCoord[] = []; for (let i = 0; i < 6; i++) { const angle = (Math.PI / 180) * (60 * i); vertices.push({ x: cx + size * Math.cos(angle), y: cy + size * Math.sin(angle), }); } return vertices; } /** * Compute the 6 edge midpoints, indexed by HexEdge enum. * * Flat-top vertices are at 0°,60°,120°,180°,240°,300° (screen coords, y+ down). * Raw midpoints between consecutive vertices sit at 30°,90°,150°,210°,270°,330°. * EDGE_DIRECTIONS point at -30°(NE),30°(E),90°(SE),150°(SW),210°(W),270°(NW). * So raw midpoint index i maps to HexEdge (i+1)%6. * We reorder so result[HexEdge] gives the correct midpoint. */ export function hexEdgeMidpoints(vertices: PixelCoord[]): PixelCoord[] { // Raw midpoints between consecutive vertex pairs const raw: PixelCoord[] = []; for (let i = 0; i < 6; i++) { const next = (i + 1) % 6; raw.push({ x: (vertices[i].x + vertices[next].x) / 2, y: (vertices[i].y + vertices[next].y) / 2, }); } // Reorder: HexEdge j = raw[(j + 5) % 6] // NE(0)=raw[5], E(1)=raw[0], SE(2)=raw[1], SW(3)=raw[2], W(4)=raw[3], NW(5)=raw[4] const reordered: PixelCoord[] = []; for (let j = 0; j < 6; j++) { reordered.push(raw[(j + 5) % 6]); } return reordered; } /** Compute full hex geometry */ export function computeHexGeometry(cx: number, cy: number, size: number): HexGeometry { const vertices = hexVertices(cx, cy, size); const edgeMidpoints = hexEdgeMidpoints(vertices); return { cx, cy, size, vertices, edgeMidpoints }; } /** Width of a flat-top hex (vertex to vertex, horizontal) */ export function hexWidth(size: number): number { return size * 2; } /** Height of a flat-top hex (flat edge to flat edge, vertical) */ export function hexHeight(size: number): number { return size * SQRT3; } /** * Determine which edge of a hex is closest to a pixel point. * Useful for click-on-edge detection. */ export function closestEdge(hexCenter: PixelCoord, size: number, point: PixelCoord): HexEdge { const vertices = hexVertices(hexCenter.x, hexCenter.y, size); const midpoints = hexEdgeMidpoints(vertices); let minDist = Infinity; let closest = HexEdge.NE; for (let i = 0; i < 6; i++) { const mp = midpoints[i]; const dist = Math.hypot(point.x - mp.x, point.y - mp.y); if (dist < minDist) { minDist = dist; closest = i as HexEdge; } } return closest; }