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:
118
core/edge-connectivity.ts
Normal file
118
core/edge-connectivity.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import type { AxialCoord, EdgeMask, HexFeature } from './types.js';
|
||||
import { HexEdge, OPPOSITE_EDGE, ALL_EDGES } from './types.js';
|
||||
import { getNeighbor } from './coords.js';
|
||||
import type { HexMap } from './hex-map.js';
|
||||
|
||||
/** Create an edge mask from a list of edges */
|
||||
export function edgeMask(...edges: HexEdge[]): EdgeMask {
|
||||
let mask = 0;
|
||||
for (const e of edges) mask |= (1 << e);
|
||||
return mask;
|
||||
}
|
||||
|
||||
/** Check if an edge is set in a mask */
|
||||
export function hasEdge(mask: EdgeMask, edge: HexEdge): boolean {
|
||||
return (mask & (1 << edge)) !== 0;
|
||||
}
|
||||
|
||||
/** Toggle an edge in a mask */
|
||||
export function toggleEdge(mask: EdgeMask, edge: HexEdge): EdgeMask {
|
||||
return mask ^ (1 << edge);
|
||||
}
|
||||
|
||||
/** Set an edge in a mask */
|
||||
export function setEdge(mask: EdgeMask, edge: HexEdge): EdgeMask {
|
||||
return mask | (1 << edge);
|
||||
}
|
||||
|
||||
/** Clear an edge from a mask */
|
||||
export function clearEdge(mask: EdgeMask, edge: HexEdge): EdgeMask {
|
||||
return mask & ~(1 << edge);
|
||||
}
|
||||
|
||||
/** Count active edges */
|
||||
export function edgeCount(mask: EdgeMask): number {
|
||||
let n = 0;
|
||||
for (let i = 0; i < 6; i++) {
|
||||
if (mask & (1 << i)) n++;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
/** Get list of active edges */
|
||||
export function connectedEdges(mask: EdgeMask): HexEdge[] {
|
||||
const result: HexEdge[] = [];
|
||||
for (let i = 0; i < 6; i++) {
|
||||
if (mask & (1 << i)) result.push(i as HexEdge);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Rotate a mask clockwise by N steps (each step = 60 degrees) */
|
||||
export function rotateMask(mask: EdgeMask, steps: number): EdgeMask {
|
||||
const s = ((steps % 6) + 6) % 6;
|
||||
// Rotate the 6 low bits
|
||||
return ((mask << s) | (mask >> (6 - s))) & 0x3f;
|
||||
}
|
||||
|
||||
export interface ConstraintAction {
|
||||
coord: AxialCoord;
|
||||
terrainId: string;
|
||||
edge: HexEdge;
|
||||
action: 'add_dead_end';
|
||||
}
|
||||
|
||||
/**
|
||||
* After a feature edge is set on a hex, check if the neighbor
|
||||
* needs a corresponding entry. Returns actions to apply.
|
||||
*/
|
||||
export function enforceEdgeConstraints(
|
||||
hexMap: HexMap,
|
||||
coord: AxialCoord,
|
||||
terrainId: string,
|
||||
mask: EdgeMask,
|
||||
): ConstraintAction[] {
|
||||
const actions: ConstraintAction[] = [];
|
||||
|
||||
for (const edge of ALL_EDGES) {
|
||||
if (!hasEdge(mask, edge)) continue;
|
||||
|
||||
const neighbor = getNeighbor(coord, edge);
|
||||
const oppositeEdge = OPPOSITE_EDGE[edge];
|
||||
const neighborTerrain = hexMap.getTerrain(neighbor);
|
||||
const neighborFeature = neighborTerrain.features.find(f => f.terrainId === terrainId);
|
||||
|
||||
if (!neighborFeature || !hasEdge(neighborFeature.edgeMask, oppositeEdge)) {
|
||||
actions.push({
|
||||
coord: neighbor,
|
||||
terrainId,
|
||||
edge: oppositeEdge,
|
||||
action: 'add_dead_end',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return actions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply constraint actions to the hex map.
|
||||
* Adds dead-end features on neighbor hexes.
|
||||
*/
|
||||
export function applyConstraintActions(hexMap: HexMap, actions: ConstraintAction[]): void {
|
||||
for (const action of actions) {
|
||||
const terrain = hexMap.getTerrain(action.coord);
|
||||
const existing = terrain.features.find(f => f.terrainId === action.terrainId);
|
||||
|
||||
if (existing) {
|
||||
existing.edgeMask = setEdge(existing.edgeMask, action.edge);
|
||||
} else {
|
||||
terrain.features.push({
|
||||
terrainId: action.terrainId,
|
||||
edgeMask: edgeMask(action.edge),
|
||||
});
|
||||
}
|
||||
|
||||
hexMap.setTerrain(action.coord, terrain);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user