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:
147
src/main.ts
147
src/main.ts
@@ -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 {
|
||||
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,11 +162,7 @@ function handleFeature(event: HexClickEvent) {
|
||||
}
|
||||
|
||||
// --- Hex Interaction ---
|
||||
let detachInteraction: (() => void) | null = null;
|
||||
|
||||
function reattachInteraction() {
|
||||
detachInteraction?.();
|
||||
detachInteraction = attachHexInteraction(
|
||||
attachHexInteraction(
|
||||
map, hexSize, origin,
|
||||
(event) => {
|
||||
if (currentMode === 'select') handleSelect(event);
|
||||
@@ -156,28 +172,14 @@ function reattachInteraction() {
|
||||
(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 existing hexes
|
||||
const hexes = await api.loadHexes(currentMapId);
|
||||
if (hexes.length > 0) {
|
||||
for (const hex of hexes) {
|
||||
// --- 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,
|
||||
@@ -185,11 +187,8 @@ async function init() {
|
||||
}
|
||||
hexMap.markClean();
|
||||
hexLayer.redraw();
|
||||
console.log(`[init] Loaded ${hexes.length} hexes`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('[init] API not available, running without persistence:', err);
|
||||
console.log(`[init] Loaded ${data.hexes.length} hexes from localStorage`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('[init] Failed to load from localStorage:', err);
|
||||
}
|
||||
|
||||
init();
|
||||
|
||||
@@ -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,41 +16,21 @@ 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;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
setSelectedHex(coord: AxialCoord | null): void {
|
||||
this._selectedHex = coord;
|
||||
this.redraw();
|
||||
}
|
||||
|
||||
setShowGrid(show: boolean): void {
|
||||
this._showGrid = show;
|
||||
this.redraw();
|
||||
}
|
||||
|
||||
setHexOpacity(opacity: number): void {
|
||||
this._hexOpacity = opacity;
|
||||
this.redraw();
|
||||
}
|
||||
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;
|
||||
|
||||
const HexLayer = L.GridLayer.extend({
|
||||
createTile(coords: L.Coords): HTMLCanvasElement {
|
||||
const canvas = document.createElement('canvas');
|
||||
const tileSize = this.getTileSize();
|
||||
@@ -72,28 +52,52 @@ export class HexOverlayLayer extends L.GridLayer {
|
||||
maxY: sePoint.y * scale,
|
||||
};
|
||||
|
||||
const hexCoords = getHexesInBounds(bounds, this.hexSize, this.origin);
|
||||
const hexCoords = getHexesInBounds(bounds, hexSize, origin);
|
||||
|
||||
for (const coord of hexCoords) {
|
||||
const terrain = this.hexMap.getTerrain(coord);
|
||||
const pixelCenter = axialToPixel(coord, this.hexSize, this.origin);
|
||||
const terrain = hexMap.getTerrain(coord);
|
||||
const pixelCenter = axialToPixel(coord, hexSize, origin);
|
||||
|
||||
const localX = (pixelCenter.x - bounds.minX) / scale;
|
||||
const localY = (pixelCenter.y - bounds.minY) / scale;
|
||||
const localSize = this.hexSize / scale;
|
||||
const localSize = hexSize / scale;
|
||||
|
||||
const geom = computeHexGeometry(localX, localY, localSize);
|
||||
const isSelected = this._selectedHex !== null &&
|
||||
this._selectedHex.q === coord.q &&
|
||||
this._selectedHex.r === coord.r;
|
||||
const isSelected = selectedHex !== null &&
|
||||
selectedHex.q === coord.q &&
|
||||
selectedHex.r === coord.r;
|
||||
|
||||
renderHex(ctx, geom, terrain, {
|
||||
opacity: this._hexOpacity,
|
||||
showGrid: this._showGrid,
|
||||
opacity: hexOpacity,
|
||||
showGrid,
|
||||
selected: isSelected,
|
||||
});
|
||||
}
|
||||
|
||||
return canvas;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const layer = new HexLayer() as L.GridLayer & {
|
||||
setSelectedHex: (coord: AxialCoord | null) => void;
|
||||
setShowGrid: (show: boolean) => void;
|
||||
setHexOpacity: (opacity: number) => void;
|
||||
};
|
||||
|
||||
layer.setSelectedHex = (coord: AxialCoord | null) => {
|
||||
selectedHex = coord;
|
||||
layer.redraw();
|
||||
};
|
||||
|
||||
layer.setShowGrid = (show: boolean) => {
|
||||
showGrid = show;
|
||||
layer.redraw();
|
||||
};
|
||||
|
||||
layer.setHexOpacity = (opacity: number) => {
|
||||
hexOpacity = opacity;
|
||||
layer.redraw();
|
||||
};
|
||||
|
||||
return layer;
|
||||
}
|
||||
|
||||
@@ -255,7 +255,7 @@ function drawLinearFeature(
|
||||
const { cx, cy, edgeMidpoints, vertices } = geom;
|
||||
|
||||
if (terrainId === 'coastline') {
|
||||
drawCoastline(ctx, vertices, edges, color, size);
|
||||
drawCoastlineFeature(ctx, geom, edges, size);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -374,41 +374,131 @@ function drawBezierRoute(
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
function drawCoastline(
|
||||
/**
|
||||
* Coastline: routes edge-to-edge like road/river, but fills one side with water.
|
||||
* The water side is the side AWAY from the hex center (the "outside" of the curve).
|
||||
* We determine this using a cross-product test on the bezier midpoint.
|
||||
*/
|
||||
function drawCoastlineFeature(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
vertices: PixelCoord[],
|
||||
geom: HexGeometry,
|
||||
edges: HexEdge[],
|
||||
color: string,
|
||||
size: number,
|
||||
): void {
|
||||
ctx.strokeStyle = color;
|
||||
ctx.lineWidth = Math.max(2, size / 5);
|
||||
ctx.lineCap = 'round';
|
||||
ctx.setLineDash([]);
|
||||
const { cx, cy, edgeMidpoints, vertices } = geom;
|
||||
const pairs = pairEdges(edges);
|
||||
const waterColor = '#2a5574';
|
||||
const coastColor = '#1a4a6a';
|
||||
|
||||
// Coastline runs along hex edges between vertices
|
||||
// Edge i runs from vertex i to vertex (i+1)%6
|
||||
// But our edges are reordered — we need the vertex indices for each HexEdge
|
||||
// HexEdge NE(0)=vertex pair [5,0], E(1)=[0,1], SE(2)=[1,2],
|
||||
// SW(3)=[2,3], W(4)=[3,4], NW(5)=[4,5]
|
||||
const edgeToVertices: Record<HexEdge, [number, number]> = {
|
||||
[HexEdge.NE]: [5, 0],
|
||||
[HexEdge.E]: [0, 1],
|
||||
[HexEdge.SE]: [1, 2],
|
||||
[HexEdge.SW]: [2, 3],
|
||||
[HexEdge.W]: [3, 4],
|
||||
[HexEdge.NW]: [4, 5],
|
||||
};
|
||||
ctx.save();
|
||||
clipToHex(ctx, geom);
|
||||
|
||||
for (const edge of edges) {
|
||||
const [vi1, vi2] = edgeToVertices[edge];
|
||||
const v1 = vertices[vi1];
|
||||
const v2 = vertices[vi2];
|
||||
for (const pair of pairs) {
|
||||
if (pair.length === 2) {
|
||||
const p1 = edgeMidpoints[pair[0]];
|
||||
const p2 = edgeMidpoints[pair[1]];
|
||||
|
||||
// Control point for the quadratic bezier (through center)
|
||||
const cp = { x: cx, y: cy };
|
||||
|
||||
// Determine which side of the curve is "away" from center
|
||||
// Sample the bezier midpoint, then offset perpendicular
|
||||
const bezMidX = 0.25 * p1.x + 0.5 * cp.x + 0.25 * p2.x;
|
||||
const bezMidY = 0.25 * p1.y + 0.5 * cp.y + 0.25 * p2.y;
|
||||
|
||||
// Tangent at midpoint (derivative of quadratic bezier at t=0.5)
|
||||
const tanX = (p2.x - p1.x);
|
||||
const tanY = (p2.y - p1.y);
|
||||
|
||||
// Perpendicular (rotated 90° CW)
|
||||
const perpX = tanY;
|
||||
const perpY = -tanX;
|
||||
|
||||
// The "water side" offset: we pick the side that is FARTHER from center
|
||||
const testX = bezMidX + perpX * 0.1;
|
||||
const testY = bezMidY + perpY * 0.1;
|
||||
const distFromCenter = Math.hypot(testX - cx, testY - cy);
|
||||
const distOther = Math.hypot(bezMidX - perpX * 0.1 - cx, bezMidY - perpY * 0.1 - cy);
|
||||
|
||||
const waterSide = distFromCenter > distOther ? 1 : -1;
|
||||
|
||||
// Fill the water side: build a path from the curve to the hex boundary on that side
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(v1.x, v1.y);
|
||||
ctx.lineTo(v2.x, v2.y);
|
||||
ctx.moveTo(p1.x, p1.y);
|
||||
ctx.quadraticCurveTo(cp.x, cp.y, p2.x, p2.y);
|
||||
|
||||
// Walk hex vertices on the water side from p2's edge to p1's edge
|
||||
const waterVerts = getVerticesOnSide(pair[0], pair[1], waterSide, vertices);
|
||||
for (const v of waterVerts) {
|
||||
ctx.lineTo(v.x, v.y);
|
||||
}
|
||||
ctx.closePath();
|
||||
ctx.fillStyle = waterColor;
|
||||
ctx.globalAlpha = 0.6;
|
||||
ctx.fill();
|
||||
ctx.globalAlpha = 1;
|
||||
|
||||
// Draw the coastline stroke
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(p1.x, p1.y);
|
||||
ctx.quadraticCurveTo(cp.x, cp.y, p2.x, p2.y);
|
||||
ctx.strokeStyle = coastColor;
|
||||
ctx.lineWidth = Math.max(1.5, size / 10);
|
||||
ctx.stroke();
|
||||
} else {
|
||||
// Dead-end: just draw as a line to center
|
||||
const mp = edgeMidpoints[pair[0]];
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(mp.x, mp.y);
|
||||
ctx.lineTo(cx, cy);
|
||||
ctx.strokeStyle = coastColor;
|
||||
ctx.lineWidth = Math.max(1.5, size / 10);
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get hex vertices on one side of a coastline running from edge1 to edge2.
|
||||
* side: 1 = clockwise from edge2 to edge1, -1 = counter-clockwise.
|
||||
* Returns vertices between the two edge midpoints, walking around the hex boundary.
|
||||
*/
|
||||
function getVerticesOnSide(
|
||||
edge1: HexEdge,
|
||||
edge2: HexEdge,
|
||||
side: number,
|
||||
vertices: PixelCoord[],
|
||||
): PixelCoord[] {
|
||||
// Map HexEdge to the vertex AFTER the edge midpoint (clockwise)
|
||||
// Edge NE(0) midpoint is between vertex 5 and 0 → next vertex CW = 0
|
||||
// Edge E(1) → 1, SE(2) → 2, SW(3) → 3, W(4) → 4, NW(5) → 5
|
||||
const edgeToNextVertex = [0, 1, 2, 3, 4, 5];
|
||||
|
||||
const result: PixelCoord[] = [];
|
||||
const startVertIdx = edgeToNextVertex[edge2];
|
||||
const endVertIdx = (edgeToNextVertex[edge1] + 5) % 6; // vertex BEFORE edge1's midpoint
|
||||
|
||||
if (side > 0) {
|
||||
// Walk clockwise from edge2's next vertex to edge1's previous vertex
|
||||
let idx = startVertIdx;
|
||||
for (let i = 0; i < 6; i++) {
|
||||
result.push(vertices[idx]);
|
||||
if (idx === endVertIdx) break;
|
||||
idx = (idx + 1) % 6;
|
||||
}
|
||||
} else {
|
||||
// Walk counter-clockwise
|
||||
let idx = (startVertIdx + 5) % 6;
|
||||
for (let i = 0; i < 6; i++) {
|
||||
result.push(vertices[idx]);
|
||||
if (idx === (endVertIdx + 1) % 6) break;
|
||||
idx = (idx + 5) % 6;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// --- Main render function ---
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
export interface MapSettings {
|
||||
hexSize: number;
|
||||
showGrid: boolean;
|
||||
opacity: number;
|
||||
}
|
||||
@@ -14,22 +13,6 @@ export function createMapSettings(
|
||||
function render() {
|
||||
container.innerHTML = '';
|
||||
|
||||
// Hex size
|
||||
const sizeRow = document.createElement('div');
|
||||
sizeRow.className = 'setting-row';
|
||||
sizeRow.innerHTML = `<label>Hex size (px)</label>`;
|
||||
const sizeInput = document.createElement('input');
|
||||
sizeInput.type = 'number';
|
||||
sizeInput.min = '8';
|
||||
sizeInput.max = '256';
|
||||
sizeInput.value = String(settings.hexSize);
|
||||
sizeInput.addEventListener('change', () => {
|
||||
settings.hexSize = Math.max(8, Math.min(256, Number(sizeInput.value)));
|
||||
onChange(settings);
|
||||
});
|
||||
sizeRow.appendChild(sizeInput);
|
||||
container.appendChild(sizeRow);
|
||||
|
||||
// Show grid
|
||||
const gridRow = document.createElement('div');
|
||||
gridRow.className = 'setting-row';
|
||||
|
||||
@@ -34,10 +34,30 @@ export function createSidebar(container: HTMLElement): {
|
||||
settings.id = 'settings';
|
||||
settingsSection.appendChild(settings);
|
||||
|
||||
// File operations
|
||||
const fileSection = document.createElement('div');
|
||||
fileSection.className = 'sidebar-section';
|
||||
fileSection.innerHTML = '<h3>File</h3>';
|
||||
const fileButtons = document.createElement('div');
|
||||
fileButtons.className = 'toolbar';
|
||||
|
||||
const exportBtn = document.createElement('button');
|
||||
exportBtn.textContent = 'Export';
|
||||
exportBtn.addEventListener('click', () => (window as any).__hexifyer?.exportMap());
|
||||
|
||||
const importBtn = document.createElement('button');
|
||||
importBtn.textContent = 'Import';
|
||||
importBtn.addEventListener('click', () => (window as any).__hexifyer?.importMap());
|
||||
|
||||
fileButtons.appendChild(exportBtn);
|
||||
fileButtons.appendChild(importBtn);
|
||||
fileSection.appendChild(fileButtons);
|
||||
|
||||
container.appendChild(toolbarSection);
|
||||
container.appendChild(terrainSection);
|
||||
container.appendChild(inspectorSection);
|
||||
container.appendChild(settingsSection);
|
||||
container.appendChild(fileSection);
|
||||
|
||||
return { toolbar, terrainPicker, hexInspector, settings };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user