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>
6.1 KiB
Hexifyer — CLAUDE.md
Projektübersicht
Hexifyer ist ein Hex-Grid-Overlay-Tool für Fantasy-Karten. Eine Leaflet-Karte (Kacheln aus einem Quellbild) wird mit einem bearbeitbaren Hex-Raster überlagert. Jedes Hex speichert Terrain-Typ (Basis + lineare Features wie Flüsse, Straßen).
Stack: Vanilla TypeScript · Vite · Leaflet · Express · sql.js (SQLite WASM)
Architektur
core/ — Pure Logik: Koordinaten, Terrain-Typen, HexMap-Datenstruktur
server/ — Express API + sql.js DB (hex_maps, hexes, hex_features)
src/ — Frontend: Leaflet-Karte, SVG-Renderer, UI-Komponenten
pipeline/ — CLI-Scripts für Daten-Import und Karten-Aufbereitung
tests/ — Vitest-Tests für core/
Hex-Koordinatensystem
Flat-top Axial-Koordinaten (q, r).
// Pixel → Axial
x = origin.x + size * (3/2) * q
y = origin.y + size * (√3/2 * q + √3 * r)
Axiale Koordinaten sind im Pixelraum geschert — konstante r-Linien sind diagonal. Das ist relevant für die Regionsaufteilung (siehe unten).
Pipeline: Hex-Map aus Quellbild generieren
Warum nicht pixelbasiert?
Der naive Ansatz — Pixelfarbe des Hex-Mittelpunkts → nächste Terrain-Farbe — scheitert in der Praxis:
- Visuell ähnliche Terrains: Hügel, Wiesen und Waldränder haben oft überlappende Farbwerte. Eine Wiese im Abendlicht sieht farblich wie ein Wald aus; ein kahler Hügel wie Gebirge.
- Kein Kontextwissen: Ob ein Pixel zu einem Hügel oder einer Ebene gehört, ist oft nur aus dem Umfeld erkennbar — der Schattenwurf, die umgebenden Höhenlinien, angrenzende Gewässer.
- Lineare Features nicht ableitbar: Ob durch ein Hex ein Fluss oder eine Straße führt — und vor allem: durch welche Kanten — ist aus einem Einzelpixel nicht bestimmbar.
Submap-Ansatz mit Vision-LLM
Für jedes zu klassifizierende Hex wird ein Submap-Crop aus dem Quellbild erzeugt:
Zentrum-Hex (rot umrandet) + alle 6 Nachbar-Hexes (grau)
→ ca. 220×220 px bei hexSize=40
→ annotiertes PNG: pipeline/submaps/<map-id>/<q>_<r>.png
Das Vision-LLM (Claude) bekommt:
- Das annotierte Bild — für visuelle Mustererkennung (Texturen, Formen, Schatten)
- Bereits klassifizierte Nachbarn als Text-Kontext — für Feature-Kontinuität
Bereits klassifizierte Nachbarn:
NE: forest
E: plains + river(edges: W+E) ← Fluss kommt von W, geht nach E
SE: unknown
SW: plains
...
Dadurch kann das Modell z.B. erkennen: „der Fluss, der beim östlichen Nachbar via W-Kante eintritt, muss hier via E-Kante den Hex verlassen" — selbst wenn die Farbe im Bild alleine das nicht eindeutig zeigt.
Verarbeitungsreihenfolge
Hexes werden spaltenweise verarbeitet (q aufsteigend, r aufsteigend innerhalb jedes q). Damit sind beim Erreichen von Hex (q, r) die Nachbarn NW und W immer bereits klassifiziert.
Drei-Phasen-Pipeline
Phase 1 — Extract (pipeline/extract-submaps.ts)
Quellbild → annotierte PNG-Submaps + manifest.json
Kein API-Key nötig.
Phase 2 — Classify (Claude Code Session)
Claude liest die PNGs mit dem Read-Tool,
klassifiziert in Batches, schreibt pipeline/submaps/<id>/classifications.json.
Kann über mehrere Sessions fortgesetzt werden.
Phase 3 — Import (pipeline/import-from-json.ts)
classifications.json → SQLite-DB (hexes + hex_features)
Kein API-Key nötig.
Alternative: pipeline/import-from-image.ts kombiniert alle drei Phasen
in einem Script, benötigt aber einen eigenen ANTHROPIC_API_KEY.
Aventurien-Karte (DSA 4.1, 2016)
Quellbild
pipeline/source/aventurien-8000x12000.jpg — aus Tile-Pyramide
des kiepenkerl-Projekts (git.davoryn.de/calic/kiepenkerl) rekonstruiert.
Nicht im Repo (.gitignore), lokal erzeugen:
# Tiles holen (zoom 6, 1504 Tiles)
docker cp kiepenkerl:/app/dist-server/public/tiles/6 pipeline/source/aventurien-tiles-z6
# Bild zusammensetzen
npm run pipeline:assemble -- pipeline/source/aventurien-tiles-z6 pipeline/source/aventurien.jpg
# Auf exakte 8000×12000 cropppen
node -e "import('sharp').then(({default:s})=>s('pipeline/source/aventurien.jpg').extract({left:0,top:0,width:8000,height:12000}).jpeg({quality:92}).toFile('pipeline/source/aventurien-8000x12000.jpg').then(i=>console.log(i)))"
Skala
PIXELS_PER_MEILE = 8 (aus kiepenkerl src/engine/route-calc.ts)
| Skala | hexSize |
Hexes (8000×12000) |
|---|---|---|
| 10 Meilen/Hex | 40 px | ~17.800 |
| 5 Meilen/Hex | 20 px | ~71.000 |
Standard für diese Karte: 10 Meilen/Hex, hexSize=40.
Regionsaufteilung (axiale r-Koordinate)
Axiale Grenzen schneiden durch den Pixelraum diagonal (r-Isolinien sind im Bild von links-oben nach rechts-unten geneigt). Das ist korrekt und erwünscht — Grenzen sind konsistent und erweiterbar.
| Region | r-Bereich | Inhalt |
|---|---|---|
| Nord | r < 25 | Ifirns Ozean, Thorwal, Gjalskerland |
| Mitte | 25 ≤ r ≤ 90 | Mittelreich, Horasreich, Bornland, Raschtulswall, Khom |
| Süd | r > 90 | Al'Anfa, Südmeer-Inseln |
Jede Session kann eine Region hinzufügen, ohne die anderen zu berühren.
Map-Eintrag in der DB
# Einmalig anlegen (z.B. über API beim laufenden Server, oder direkt):
curl -X POST http://localhost:3001/api/maps \
-H 'Content-Type: application/json' \
-d '{"name":"Aventurien 10M/Hex","image_width":8000,"image_height":12000,
"hex_size":40,"origin_x":0,"origin_y":0,"min_zoom":0,"max_zoom":6}'
Workflow (Mitte-Region)
# 1. Submaps extrahieren (Mitte: r=25..90)
npm run pipeline:extract -- \
pipeline/source/aventurien-8000x12000.jpg <map-id> \
--region 0,25,133,90
# 2. Claude klassifiziert in dieser Session
# (submaps/<map-id>/classifications.json wird schrittweise gefüllt)
# 3. Ergebnisse importieren
npm run pipeline:import -- \
pipeline/submaps/<map-id>/classifications.json <map-id>
Entwicklung
npm run dev # Vite + Express parallel
npm run test # Vitest
npm run pipeline:assemble -- <tiles-dir> <output.jpg>
npm run pipeline:extract -- <image> <map-id> [--region q0,r0,q1,r1]
npm run pipeline:import -- <classifications.json> <map-id>