Files
hexifyer/CLAUDE.md
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

192 lines
6.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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).
```typescript
// 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:
1. Das annotierte Bild — für visuelle Mustererkennung (Texturen, Formen, Schatten)
2. 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:
```bash
# 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
```bash
# 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)
```bash
# 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
```bash
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>
```