Phase 2: Terrain textures, bezier routing, drag-paint, HTTPS

- svg/renderer.ts: Full Canvas rendering engine with terrain textures
  (trees for forest, waves for water, peaks for mountains, contour
  lines for hills, hatch for farmland, buildings for settlements)
- Linear features: paired-edge bezier routing (straight-through,
  curves, dead-ends), river wobble, proper coastline along hex edges
- Drag-paint: click-and-drag in Paint mode paints multiple hexes,
  disables map panning during paint gesture
- NGINX reverse proxy + Let's Encrypt cert for hexifyer.davoryn.de
- Refactored hex-layer.ts to delegate rendering to renderer module

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Axel Meyer
2026-04-07 10:41:24 +00:00
parent f302932ea8
commit 0e2903b789
6 changed files with 652 additions and 195 deletions

View File

@@ -1,7 +1,7 @@
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 { attachHexInteraction, type HexClickEvent } 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';
@@ -76,45 +76,62 @@ function rebuildHexLayer(showGrid: boolean, opacity: number) {
reattachInteraction();
}
// --- Interaction handlers ---
function handleSelect(event: HexClickEvent) {
selectedHex = event.coord;
hexLayer.setSelectedHex(selectedHex);
hexInspectorUI.update(selectedHex, hexMap.getTerrain(selectedHex));
}
function handlePaint(event: HexClickEvent) {
if (!selectedTerrain) return;
hexMap.setBase(event.coord, selectedTerrain.id);
hexLayer.redraw();
selectedHex = event.coord;
hexInspectorUI.update(selectedHex, hexMap.getTerrain(selectedHex));
}
function handleFeature(event: HexClickEvent) {
if (!selectedTerrain) return;
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);
if (newMask > currentMask) {
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));
}
// --- 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));
}
});
detachInteraction = attachHexInteraction(
map, hexSize, origin,
// Click handler
(event) => {
if (currentMode === 'select') handleSelect(event);
else if (currentMode === 'paint') handlePaint(event);
else if (currentMode === 'feature') handleFeature(event);
},
// Drag-paint handler (only active in paint mode)
(event) => {
if (currentMode === 'paint') handlePaint(event);
},
);
}
reattachInteraction();