- 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>
95 lines
2.6 KiB
TypeScript
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);
|
|
}
|