Add per-hex metadata system for scale-invariant classification

Each hex now gets a meta/<q>_<r>.json with stable ID, pixel center
coordinates, pixel bounds, labels, notes, and classification status.
The pixelCenter acts as a scale-independent anchor: when switching from
10 Meilen/Hex to 5 Meilen/Hex, pixelToAxial(meta.pixelCenter, newSize)
maps coarse hexes to fine hexes without re-running classification.

Adds:
- pipeline/build-hexmeta.ts: creates/updates metadata + exports
  data/hexmeta-<map-id>.jsonl (committed, survives git clones)
- pipeline/auto-classify-ocean.ts: pixel-based ocean auto-detection
- pipeline/create-map.ts: one-off DB map entry creation
- extract-submaps.ts: writes meta/<q>_<r>.json during extraction
- data/hexmeta-1.jsonl: 8844 hex metadata entries for Aventurien map 1

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Axel Meyer
2026-04-16 10:07:09 +02:00
parent bc7d5a3cc7
commit 12200735bf
7 changed files with 9207 additions and 5 deletions

View File

@@ -23,6 +23,7 @@ import { initDb, getDb } from '../server/db.js';
import { axialToPixel, hexVertices } from '../core/coords.js';
import { gridBoundsForImage } from '../core/hex-grid.js';
import { HexEdge, EDGE_DIRECTIONS, ALL_EDGES, type AxialCoord, type PixelCoord } from '../core/types.js';
import type { HexMeta } from './build-hexmeta.js';
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
@@ -183,7 +184,7 @@ async function main() {
const outDir = join(ROOT, 'pipeline', 'submaps', String(mapId));
mkdirSync(outDir, { recursive: true });
// Write manifest
// Write manifest (with stable IDs, starting at 1)
const manifest = {
mapId,
imageWidth: image_width,
@@ -192,17 +193,47 @@ async function main() {
originX: origin_x,
originY: origin_y,
meilenPerHex: hexesPerMeile,
hexes: sorted.map(c => ({ q: c.q, r: c.r })),
hexes: sorted.map((c, i) => ({ id: i + 1, q: c.q, r: c.r })),
};
writeFileSync(join(outDir, 'manifest.json'), JSON.stringify(manifest, null, 2));
console.log(`Manifest written: ${sorted.length} hexes`);
// Create meta directory for per-hex attribute files
const metaDir = join(outDir, 'meta');
mkdirSync(metaDir, { recursive: true });
let done = 0;
for (const coord of sorted) {
for (let i = 0; i < sorted.length; i++) {
const coord = sorted[i];
const filename = `${coord.q}_${coord.r}.png`;
const outPath = join(outDir, filename);
const metaPath = join(metaDir, `${coord.q}_${coord.r}.json`);
// Skip if already extracted (resumable)
// Create meta file if not present (never overwrite existing — preserves labels/notes)
if (!existsSync(metaPath)) {
const px = axialToPixel(coord, hex_size, origin);
const r = Math.ceil(hex_size * CROP_RADIUS_FACTOR);
const meta: HexMeta = {
id: i + 1,
q: coord.q, r: coord.r,
mapId,
hexSizePx: hex_size,
meilenPerHex: hexesPerMeile,
pixelCenter: { x: Math.round(px.x), y: Math.round(px.y) },
pixelBounds: {
left: Math.max(0, Math.round(px.x - r)),
top: Math.max(0, Math.round(px.y - r)),
right: Math.min(image_width, Math.round(px.x + r)),
bottom: Math.min(image_height, Math.round(px.y + r)),
},
labels: [],
notes: '',
classification: null,
};
writeFileSync(metaPath, JSON.stringify(meta, null, 2));
}
// Skip PNG if already extracted (resumable)
if (existsSync(outPath)) {
done++;
continue;