# 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//_.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//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 \ --region 0,25,133,90 # 2. Claude klassifiziert in dieser Session # (submaps//classifications.json wird schrittweise gefüllt) # 3. Ergebnisse importieren npm run pipeline:import -- \ pipeline/submaps//classifications.json ``` --- ## Entwicklung ```bash npm run dev # Vite + Express parallel npm run test # Vitest npm run pipeline:assemble -- npm run pipeline:extract -- [--region q0,r0,q1,r1] npm run pipeline:import -- ```