Files
hexifyer/pipeline/import-from-json.ts
Axel Meyer bc7d5a3cc7 Add vision-LLM import pipeline for hex terrain classification
Adds a 3-phase pipeline to populate a HexMap from a source image using
Claude's vision capabilities instead of naive pixel-color matching:

Phase 1 (extract-submaps): crops annotated submap PNGs per hex,
  including center + 6 neighbours with SVG overlay.
Phase 2: Claude classifies submaps in a Code session, writing
  classifications.json — resumable across sessions.
Phase 3 (import-from-json): reads classifications.json into the DB.

Also adds assemble-map.ts to reconstruct a full image from a Leaflet
tile pyramid (used to recover the Aventurien source map from kiepenkerl).

Adds CLAUDE.md documenting the approach, scale constants
(PIXELS_PER_MEILE=8, hexSize=40 = 10 Meilen/Hex), and the
Nord/Mitte/Süd region split for the Aventurien map.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 09:49:01 +02:00

95 lines
2.7 KiB
TypeScript

/**
* Phase 3: Import hex classifications from JSON into the DB.
*
* Usage:
* npx tsx pipeline/import-from-json.ts <classifications.json> <map-id>
*
* Input JSON format (array):
* [
* { "q": 0, "r": 0, "base": "forest", "features": [] },
* { "q": 1, "r": 0, "base": "plains", "features": [{ "terrainId": "river", "edgeMask": 18 }] },
* ...
* ]
*
* Edge mask bits: NE=1, E=2, SE=4, SW=8, W=16, NW=32
*/
import { readFileSync } from 'fs';
import { resolve } from 'path';
import { initDb, getDb, saveDb } from '../server/db.js';
interface HexClassification {
q: number;
r: number;
base: string;
features: Array<{ terrainId: string; edgeMask: number }>;
}
async function main() {
const [jsonPath, mapIdStr] = process.argv.slice(2);
if (!jsonPath || !mapIdStr) {
console.error('Usage: npx tsx pipeline/import-from-json.ts <classifications.json> <map-id>');
process.exit(1);
}
const mapId = parseInt(mapIdStr, 10);
const hexes: HexClassification[] = JSON.parse(readFileSync(resolve(jsonPath), 'utf-8'));
console.log(`Importing ${hexes.length} hexes into map ${mapId}...`);
await initDb();
const db = getDb();
// Verify map exists
const mapRows = db.exec('SELECT id FROM hex_maps WHERE id = ?', [mapId]);
if (mapRows.length === 0 || mapRows[0].values.length === 0) {
console.error(`Map ${mapId} not found`);
process.exit(1);
}
db.run('BEGIN TRANSACTION');
let written = 0;
let errors = 0;
try {
for (const hex of hexes) {
// Upsert hex
db.run(
`INSERT INTO hexes (map_id, q, r, base_terrain, updated_at)
VALUES (?, ?, ?, ?, datetime('now'))
ON CONFLICT(map_id, q, r)
DO UPDATE SET base_terrain = excluded.base_terrain, updated_at = datetime('now')`,
[mapId, hex.q, hex.r, hex.base],
);
const idRows = db.exec(
'SELECT id FROM hexes WHERE map_id = ? AND q = ? AND r = ?',
[mapId, hex.q, hex.r],
);
const hexId = idRows[0].values[0][0] as number;
db.run('DELETE FROM hex_features WHERE hex_id = ?', [hexId]);
for (const f of (hex.features ?? [])) {
if (!f.edgeMask) continue;
db.run(
'INSERT INTO hex_features (hex_id, terrain_id, edge_mask) VALUES (?, ?, ?)',
[hexId, f.terrainId, f.edgeMask],
);
}
written++;
}
db.run("UPDATE hex_maps SET updated_at = datetime('now') WHERE id = ?", [mapId]);
db.run('COMMIT');
saveDb();
console.log(`Done: ${written} hexes written, ${errors} errors.`);
} catch (err) {
db.run('ROLLBACK');
console.error('Transaction failed:', err);
process.exit(1);
}
}
main().catch(err => { console.error('Fatal:', err); process.exit(1); });