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>
This commit is contained in:
94
core/hex-grid.ts
Normal file
94
core/hex-grid.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user