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>
95 lines
2.7 KiB
TypeScript
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); });
|