Files
hexifyer/core/hex-grid.ts
Axel Meyer f302932ea8 Phase 1: Core hex engine, Leaflet overlay, terrain painting UI
- core/: Pure TS hex engine (axial coords, hex grid, terrain types,
  edge connectivity with constraint solver, HexMap state model)
- src/map/: Leaflet L.CRS.Simple map init, Canvas-based hex overlay
  layer (L.GridLayer), click/edge interaction detection
- src/ui/: Sidebar with toolbar (Select/Paint/Feature modes),
  terrain picker, hex inspector, map settings (hex size, grid, opacity)
- pipeline/: Tile pyramid generator (sharp, from source image)
- tests/: 32 passing tests for coords, hex-grid, edge-connectivity
- Uses Kiepenkerl tiles (symlinked) for development

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 10:32:52 +00:00

95 lines
2.6 KiB
TypeScript

import type { AxialCoord, PixelCoord } from './types.js';
import { axialToPixel, pixelToAxial, hexWidth, hexHeight } from './coords.js';
/** Rectangular pixel bounds */
export interface PixelBounds {
minX: number;
minY: number;
maxX: number;
maxY: number;
}
/**
* Iterate all hex coordinates that overlap a rectangular pixel region.
* Returns axial coords for every hex whose center falls within
* the bounds (with one hex margin to avoid edge clipping).
*/
export function getHexesInBounds(
bounds: PixelBounds,
size: number,
origin: PixelCoord = { x: 0, y: 0 },
): AxialCoord[] {
const w = hexWidth(size);
const h = hexHeight(size);
const colStep = w * 3 / 4; // horizontal distance between hex centers
const rowStep = h; // vertical distance between hex centers
// Expand bounds by one hex to catch partial overlaps
const expandedBounds: PixelBounds = {
minX: bounds.minX - w,
minY: bounds.minY - h,
maxX: bounds.maxX + w,
maxY: bounds.maxY + h,
};
// Find the approximate q range
const qMin = Math.floor((expandedBounds.minX - origin.x) / colStep) - 1;
const qMax = Math.ceil((expandedBounds.maxX - origin.x) / colStep) + 1;
const result: AxialCoord[] = [];
for (let q = qMin; q <= qMax; q++) {
// For this q column, find the r range
const colCenterX = origin.x + size * (3 / 2) * q;
const colOffsetY = origin.y + size * (Math.sqrt(3) / 2) * q;
const rMin = Math.floor((expandedBounds.minY - colOffsetY) / rowStep) - 1;
const rMax = Math.ceil((expandedBounds.maxY - colOffsetY) / rowStep) + 1;
for (let r = rMin; r <= rMax; r++) {
const pixel = axialToPixel({ q, r }, size, origin);
// Check if hex center is within the expanded bounds
if (
pixel.x >= expandedBounds.minX && pixel.x <= expandedBounds.maxX &&
pixel.y >= expandedBounds.minY && pixel.y <= expandedBounds.maxY
) {
result.push({ q, r });
}
}
}
return result;
}
/**
* Get the bounding box (in pixels) of the entire hex grid
* that covers a given image size.
*/
export function gridBoundsForImage(
imageWidth: number,
imageHeight: number,
size: number,
origin: PixelCoord = { x: 0, y: 0 },
): { coords: AxialCoord[]; bounds: PixelBounds } {
const bounds: PixelBounds = {
minX: 0,
minY: 0,
maxX: imageWidth,
maxY: imageHeight,
};
const coords = getHexesInBounds(bounds, size, origin);
return { coords, bounds };
}
/**
* Find the hex coordinate at a given pixel position.
*/
export function hexAtPixel(
pixel: PixelCoord,
size: number,
origin: PixelCoord = { x: 0, y: 0 },
): AxialCoord {
return pixelToAxial(pixel, size, origin);
}