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>
This commit is contained in:
191
CLAUDE.md
Normal file
191
CLAUDE.md
Normal file
@@ -0,0 +1,191 @@
|
||||
# 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>
|
||||
```
|
||||
Reference in New Issue
Block a user