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>
This commit is contained in:
102
pipeline/generate-tiles.ts
Normal file
102
pipeline/generate-tiles.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* 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);
|
||||
});
|
||||
Reference in New Issue
Block a user