Files
hexifyer/pipeline/generate-tiles.ts
Axel Meyer f302932ea8 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>
2026-04-07 10:32:52 +00:00

103 lines
3.2 KiB
TypeScript

/**
* Generate Leaflet tile pyramid from a source image.
*
* Usage: npx tsx pipeline/generate-tiles.ts <path-to-image>
*
* Output: tiles/{z}/{x}/{y}.jpg (also copied to public/tiles/ for dev server)
*/
import sharp from 'sharp';
import { mkdirSync, existsSync, cpSync } from 'fs';
import { join, resolve } from 'path';
const TILE_SIZE = 256;
const ROOT = resolve(import.meta.dirname, '..');
async function generateTiles(sourcePath: string) {
if (!existsSync(sourcePath)) {
console.error(`Source image not found: ${sourcePath}`);
process.exit(1);
}
const tilesDir = join(ROOT, 'tiles');
const publicTilesDir = join(ROOT, 'public', 'tiles');
console.log(`Reading source image: ${sourcePath}`);
const metadata = await sharp(sourcePath).metadata();
const width = metadata.width!;
const height = metadata.height!;
console.log(`Image size: ${width}x${height}`);
const maxDim = Math.max(width, height);
const maxZoom = Math.ceil(Math.log2(maxDim / TILE_SIZE));
console.log(`Max zoom: ${maxZoom} (grid: ${Math.pow(2, maxZoom) * TILE_SIZE}px)`);
let totalTiles = 0;
const startTime = Date.now();
for (let z = 0; z <= maxZoom; z++) {
const scale = Math.pow(2, z) / Math.pow(2, maxZoom);
const scaledW = Math.ceil(width * scale);
const scaledH = Math.ceil(height * scale);
const tilesX = Math.ceil(scaledW / TILE_SIZE);
const tilesY = Math.ceil(scaledH / TILE_SIZE);
const levelTiles = tilesX * tilesY;
console.log(`Zoom ${z}: ${scaledW}x${scaledH} -> ${tilesX}x${tilesY} = ${levelTiles} tiles`);
const buffer = await sharp(sourcePath)
.resize(scaledW, scaledH, { fit: 'fill', kernel: 'lanczos3' })
.raw()
.toBuffer();
let done = 0;
for (let x = 0; x < tilesX; x++) {
for (let y = 0; y < tilesY; y++) {
const tileDir = join(tilesDir, `${z}`, `${x}`);
mkdirSync(tileDir, { recursive: true });
const left = x * TILE_SIZE;
const top = y * TILE_SIZE;
const tileW = Math.min(TILE_SIZE, scaledW - left);
const tileH = Math.min(TILE_SIZE, scaledH - top);
await sharp(buffer, {
raw: { width: scaledW, height: scaledH, channels: 3 },
})
.extract({ left, top, width: tileW, height: tileH })
.extend({
right: TILE_SIZE - tileW,
bottom: TILE_SIZE - tileH,
background: { r: 42, g: 85, b: 116, alpha: 1 },
})
.jpeg({ quality: 85 })
.toFile(join(tileDir, `${y}.jpg`));
totalTiles++;
done++;
}
}
process.stdout.write(` -> ${done} tiles written\n`);
}
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
console.log(`\nGenerated ${totalTiles} tiles in ${tilesDir} (${elapsed}s)`);
mkdirSync(join(ROOT, 'public'), { recursive: true });
cpSync(tilesDir, publicTilesDir, { recursive: true });
console.log(`Copied to ${publicTilesDir}`);
console.log(`\nMap config: imageSize: [${width}, ${height}], maxZoom: ${maxZoom}`);
}
const source = process.argv[2];
if (!source) {
console.error('Usage: npx tsx pipeline/generate-tiles.ts <path-to-image>');
process.exit(1);
}
generateTiles(source).catch(err => {
console.error('Tile generation failed:', err);
process.exit(1);
});