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:
Axel Meyer
2026-04-07 10:32:52 +00:00
parent 5a19864fb5
commit f302932ea8
20 changed files with 1942 additions and 0 deletions

120
src/main.ts Normal file
View File

@@ -0,0 +1,120 @@
import './style/main.css';
import { initMap } from './map/map-init.js';
import { HexOverlayLayer } from './map/hex-layer.js';
import { attachHexInteraction } from './map/hex-interaction.js';
import { HexMap } from '../core/hex-map.js';
import type { AxialCoord, TerrainType } from '../core/types.js';
import { createSidebar } from './ui/sidebar.js';
import { createToolbar, type ToolMode } from './ui/toolbar.js';
import { createTerrainPicker } from './ui/terrain-picker.js';
import { createHexInspector } from './ui/hex-inspector.js';
import { createMapSettings } from './ui/map-settings.js';
import {
edgeMask,
toggleEdge,
enforceEdgeConstraints,
applyConstraintActions,
} from '../core/edge-connectivity.js';
// --- State ---
const hexMap = new HexMap();
let currentMode: ToolMode = 'select';
let selectedTerrain: TerrainType | null = null;
let selectedHex: AxialCoord | null = null;
let hexSize = 48;
const origin = { x: 0, y: 0 };
// --- Init Map ---
const map = initMap('map');
// --- Hex Layer ---
let hexLayer = new HexOverlayLayer({
hexSize,
hexMap,
origin,
showGrid: true,
opacity: 0.7,
});
hexLayer.addTo(map);
// --- Sidebar ---
const sidebarEl = document.getElementById('sidebar')!;
const { toolbar, terrainPicker, hexInspector, settings } = createSidebar(sidebarEl);
const toolbarUI = createToolbar(toolbar, (mode) => {
currentMode = mode;
terrainPickerUI.setMode(mode);
});
const terrainPickerUI = createTerrainPicker(terrainPicker, (terrain) => {
selectedTerrain = terrain;
});
const hexInspectorUI = createHexInspector(hexInspector);
createMapSettings(settings, { hexSize, showGrid: true, opacity: 0.7 }, (s) => {
if (s.hexSize !== hexSize) {
hexSize = s.hexSize;
rebuildHexLayer(s.showGrid, s.opacity);
} else {
hexLayer.setShowGrid(s.showGrid);
hexLayer.setHexOpacity(s.opacity);
}
});
// --- Rebuild hex layer (when hex size changes) ---
function rebuildHexLayer(showGrid: boolean, opacity: number) {
map.removeLayer(hexLayer);
hexLayer = new HexOverlayLayer({
hexSize,
hexMap,
origin,
showGrid,
opacity,
});
hexLayer.addTo(map);
reattachInteraction();
}
// --- Hex Interaction ---
let detachInteraction: (() => void) | null = null;
function reattachInteraction() {
detachInteraction?.();
detachInteraction = attachHexInteraction(map, hexSize, origin, (event) => {
if (currentMode === 'select') {
selectedHex = event.coord;
hexLayer.setSelectedHex(selectedHex);
hexInspectorUI.update(selectedHex, hexMap.getTerrain(selectedHex));
} else if (currentMode === 'paint' && selectedTerrain) {
hexMap.setBase(event.coord, selectedTerrain.id);
selectedHex = event.coord;
hexLayer.setSelectedHex(selectedHex);
hexLayer.redraw();
hexInspectorUI.update(selectedHex, hexMap.getTerrain(selectedHex));
} else if (currentMode === 'feature' && selectedTerrain) {
const coord = event.coord;
const terrain = hexMap.getTerrain(coord);
const existing = terrain.features.find(f => f.terrainId === selectedTerrain!.id);
const currentMask = existing?.edgeMask ?? 0;
const newMask = toggleEdge(currentMask, event.edge);
hexMap.setFeature(coord, selectedTerrain.id, newMask);
// Enforce edge constraints
if (newMask > currentMask) {
// An edge was added — ensure neighbor has continuation
const addedEdgeMask = edgeMask(event.edge);
const actions = enforceEdgeConstraints(hexMap, coord, selectedTerrain.id, addedEdgeMask);
applyConstraintActions(hexMap, actions);
}
selectedHex = coord;
hexLayer.setSelectedHex(selectedHex);
hexLayer.redraw();
hexInspectorUI.update(selectedHex, hexMap.getTerrain(selectedHex));
}
});
}
reattachInteraction();