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:
120
src/main.ts
Normal file
120
src/main.ts
Normal 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();
|
||||
Reference in New Issue
Block a user