Fix sidebar init, client-side first, coastline rework

- Fix: move terrainPickerUI declaration before createToolbar to avoid
  "can't access lexical declaration before initialization" error
- Hex size is now fixed per map (not adjustable at runtime)
- Client-side first: localStorage for persistence, no server needed
  for editing. Added Export/Import JSON buttons in sidebar.
- Removed server dependency from main.ts init flow
- Coastline rework: routes edge-to-edge like road/river (bezier),
  fills one side with water color (the side away from hex center).
  No longer draws along hex edges — it's a proper dividing curve.
- Simplified map-settings to just grid toggle + opacity slider

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Axel Meyer
2026-04-07 10:59:29 +00:00
parent 367ba8af07
commit f144063db9
5 changed files with 299 additions and 203 deletions

View File

@@ -1,6 +1,6 @@
import './style/main.css';
import { initMap } from './map/map-init.js';
import { HexOverlayLayer } from './map/hex-layer.js';
import { createHexLayer } from './map/hex-layer.js';
import { attachHexInteraction, type HexClickEvent } from './map/hex-interaction.js';
import { HexMap } from '../core/hex-map.js';
import type { AxialCoord, TerrainType } from '../core/types.js';
@@ -15,23 +15,23 @@ import {
enforceEdgeConstraints,
applyConstraintActions,
} from '../core/edge-connectivity.js';
import * as api from './data/api-client.js';
const STORAGE_KEY = 'hexifyer_map';
// --- State ---
const hexMap = new HexMap();
let currentMode: ToolMode = 'select';
let selectedTerrain: TerrainType | null = null;
let selectedHex: AxialCoord | null = null;
let hexSize = 48;
const hexSize = 48; // Fixed per map — set once
const origin = { x: 0, y: 0 };
let currentMapId: number | null = null;
let saveTimeout: ReturnType<typeof setTimeout> | null = null;
// --- Init Map ---
const map = initMap('map');
// --- Hex Layer ---
let hexLayer = new HexOverlayLayer({
const hexLayer = createHexLayer({
hexSize,
hexMap,
origin,
@@ -44,63 +44,83 @@ hexLayer.addTo(map);
const sidebarEl = document.getElementById('sidebar')!;
const { toolbar, terrainPicker, hexInspector, settings } = createSidebar(sidebarEl);
const terrainPickerUI = createTerrainPicker(terrainPicker, (terrain) => {
selectedTerrain = terrain;
});
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);
}
createMapSettings(settings, { showGrid: true, opacity: 0.7 }, (s) => {
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();
}
// --- Auto-save (debounced) ---
// --- Auto-save to localStorage (debounced) ---
function scheduleSave() {
if (!currentMapId) return;
if (saveTimeout) clearTimeout(saveTimeout);
saveTimeout = setTimeout(async () => {
if (!currentMapId || !hexMap.dirty) return;
saveTimeout = setTimeout(() => {
if (!hexMap.dirty) return;
try {
const data = hexMap.serialize();
await api.saveHexes(currentMapId, data.hexes.map(h => ({
q: h.q,
r: h.r,
base: h.base,
features: h.features.map(f => ({ terrainId: f.terrainId, edgeMask: f.edgeMask })),
})));
localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
hexMap.markClean();
console.log(`[save] Saved ${data.hexes.length} hexes`);
} catch (err) {
console.error('[save] Failed:', err);
console.error('[save] localStorage failed:', err);
}
}, 1000);
}, 500);
}
// --- File export/import ---
function exportMap() {
const data = hexMap.serialize();
const json = JSON.stringify(data, null, 2);
const blob = new Blob([json], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'hexmap.json';
a.click();
URL.revokeObjectURL(url);
}
function importMap() {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.json';
input.addEventListener('change', () => {
const file = input.files?.[0];
if (!file) return;
const reader = new FileReader();
reader.onload = () => {
try {
const data = JSON.parse(reader.result as string);
hexMap.clear();
for (const hex of data.hexes) {
hexMap.setTerrain({ q: hex.q, r: hex.r }, {
base: hex.base,
features: hex.features,
});
}
hexMap.markClean();
hexLayer.redraw();
scheduleSave();
} catch (err) {
console.error('[import] Failed:', err);
}
};
reader.readAsText(file);
});
input.click();
}
// Expose to sidebar buttons
(window as any).__hexifyer = { exportMap, importMap };
// --- Interaction handlers ---
function handleSelect(event: HexClickEvent) {
@@ -142,54 +162,33 @@ function handleFeature(event: HexClickEvent) {
}
// --- Hex Interaction ---
let detachInteraction: (() => void) | null = null;
attachHexInteraction(
map, hexSize, origin,
(event) => {
if (currentMode === 'select') handleSelect(event);
else if (currentMode === 'paint') handlePaint(event);
else if (currentMode === 'feature') handleFeature(event);
},
(event) => {
if (currentMode === 'paint') handlePaint(event);
},
);
function reattachInteraction() {
detachInteraction?.();
detachInteraction = attachHexInteraction(
map, hexSize, origin,
(event) => {
if (currentMode === 'select') handleSelect(event);
else if (currentMode === 'paint') handlePaint(event);
else if (currentMode === 'feature') handleFeature(event);
},
(event) => {
if (currentMode === 'paint') handlePaint(event);
},
);
}
reattachInteraction();
// --- Load or create map from API ---
async function init() {
try {
const maps = await api.listMaps();
if (maps.length > 0) {
currentMapId = maps[0].id;
console.log(`[init] Loading map "${maps[0].name}" (id: ${currentMapId})`);
} else {
const result = await api.createMap({ name: 'Default Map' });
currentMapId = result.id;
console.log(`[init] Created new map (id: ${currentMapId})`);
// --- Load from localStorage ---
try {
const saved = localStorage.getItem(STORAGE_KEY);
if (saved) {
const data = JSON.parse(saved);
for (const hex of data.hexes) {
hexMap.setTerrain({ q: hex.q, r: hex.r }, {
base: hex.base,
features: hex.features,
});
}
// Load existing hexes
const hexes = await api.loadHexes(currentMapId);
if (hexes.length > 0) {
for (const hex of hexes) {
hexMap.setTerrain({ q: hex.q, r: hex.r }, {
base: hex.base,
features: hex.features,
});
}
hexMap.markClean();
hexLayer.redraw();
console.log(`[init] Loaded ${hexes.length} hexes`);
}
} catch (err) {
console.warn('[init] API not available, running without persistence:', err);
hexMap.markClean();
hexLayer.redraw();
console.log(`[init] Loaded ${data.hexes.length} hexes from localStorage`);
}
} catch (err) {
console.warn('[init] Failed to load from localStorage:', err);
}
init();