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

@@ -5,7 +5,7 @@ import { getHexesInBounds, type PixelBounds } from '../../core/hex-grid.js';
import type { HexMap } from '../../core/hex-map.js';
import { renderHex } from '../svg/renderer.js';
export interface HexLayerOptions extends L.GridLayerOptions {
export interface HexLayerOptions {
hexSize: number;
hexMap: HexMap;
origin?: { x: number; y: number };
@@ -16,84 +16,88 @@ export interface HexLayerOptions extends L.GridLayerOptions {
/**
* Leaflet GridLayer that renders the hex overlay using Canvas.
* Delegates all hex rendering to svg/renderer.ts.
* Uses L.GridLayer.extend() for compatibility with Leaflet's class system.
*/
export class HexOverlayLayer extends L.GridLayer {
private hexSize: number;
private hexMap: HexMap;
private origin: { x: number; y: number };
private _selectedHex: AxialCoord | null = null;
private _showGrid = true;
private _hexOpacity = 0.7;
export function createHexLayer(options: HexLayerOptions): L.GridLayer & {
setSelectedHex: (coord: AxialCoord | null) => void;
setShowGrid: (show: boolean) => void;
setHexOpacity: (opacity: number) => void;
} {
let hexSize = options.hexSize;
let hexMap = options.hexMap;
const origin = options.origin ?? { x: 0, y: 0 };
let selectedHex: AxialCoord | null = options.selectedHex ?? null;
let showGrid = options.showGrid ?? true;
let hexOpacity = options.opacity ?? 0.7;
constructor(options: HexLayerOptions) {
super(options);
this.hexSize = options.hexSize;
this.hexMap = options.hexMap;
this.origin = options.origin ?? { x: 0, y: 0 };
this._selectedHex = options.selectedHex ?? null;
this._showGrid = options.showGrid ?? true;
this._hexOpacity = options.opacity ?? 0.7;
}
const HexLayer = L.GridLayer.extend({
createTile(coords: L.Coords): HTMLCanvasElement {
const canvas = document.createElement('canvas');
const tileSize = this.getTileSize();
canvas.width = tileSize.x;
canvas.height = tileSize.y;
const ctx = canvas.getContext('2d')!;
setSelectedHex(coord: AxialCoord | null): void {
this._selectedHex = coord;
this.redraw();
}
const nwPoint = coords.scaleBy(tileSize);
const sePoint = nwPoint.add(tileSize);
setShowGrid(show: boolean): void {
this._showGrid = show;
this.redraw();
}
const zoom = coords.z;
const maxZoom = this._map?.getMaxZoom() ?? 6;
const scale = Math.pow(2, maxZoom - zoom);
setHexOpacity(opacity: number): void {
this._hexOpacity = opacity;
this.redraw();
}
const bounds: PixelBounds = {
minX: nwPoint.x * scale,
minY: nwPoint.y * scale,
maxX: sePoint.x * scale,
maxY: sePoint.y * scale,
};
createTile(coords: L.Coords): HTMLCanvasElement {
const canvas = document.createElement('canvas');
const tileSize = this.getTileSize();
canvas.width = tileSize.x;
canvas.height = tileSize.y;
const ctx = canvas.getContext('2d')!;
const hexCoords = getHexesInBounds(bounds, hexSize, origin);
const nwPoint = coords.scaleBy(tileSize);
const sePoint = nwPoint.add(tileSize);
for (const coord of hexCoords) {
const terrain = hexMap.getTerrain(coord);
const pixelCenter = axialToPixel(coord, hexSize, origin);
const zoom = coords.z;
const maxZoom = this._map?.getMaxZoom() ?? 6;
const scale = Math.pow(2, maxZoom - zoom);
const localX = (pixelCenter.x - bounds.minX) / scale;
const localY = (pixelCenter.y - bounds.minY) / scale;
const localSize = hexSize / scale;
const bounds: PixelBounds = {
minX: nwPoint.x * scale,
minY: nwPoint.y * scale,
maxX: sePoint.x * scale,
maxY: sePoint.y * scale,
};
const geom = computeHexGeometry(localX, localY, localSize);
const isSelected = selectedHex !== null &&
selectedHex.q === coord.q &&
selectedHex.r === coord.r;
const hexCoords = getHexesInBounds(bounds, this.hexSize, this.origin);
renderHex(ctx, geom, terrain, {
opacity: hexOpacity,
showGrid,
selected: isSelected,
});
}
for (const coord of hexCoords) {
const terrain = this.hexMap.getTerrain(coord);
const pixelCenter = axialToPixel(coord, this.hexSize, this.origin);
return canvas;
},
});
const localX = (pixelCenter.x - bounds.minX) / scale;
const localY = (pixelCenter.y - bounds.minY) / scale;
const localSize = this.hexSize / scale;
const layer = new HexLayer() as L.GridLayer & {
setSelectedHex: (coord: AxialCoord | null) => void;
setShowGrid: (show: boolean) => void;
setHexOpacity: (opacity: number) => void;
};
const geom = computeHexGeometry(localX, localY, localSize);
const isSelected = this._selectedHex !== null &&
this._selectedHex.q === coord.q &&
this._selectedHex.r === coord.r;
layer.setSelectedHex = (coord: AxialCoord | null) => {
selectedHex = coord;
layer.redraw();
};
renderHex(ctx, geom, terrain, {
opacity: this._hexOpacity,
showGrid: this._showGrid,
selected: isSelected,
});
}
layer.setShowGrid = (show: boolean) => {
showGrid = show;
layer.redraw();
};
return canvas;
}
layer.setHexOpacity = (opacity: number) => {
hexOpacity = opacity;
layer.redraw();
};
return layer;
}