Phase 3: Universal VTT (.dd2vtt/.uvtt) import + export

- src/lib/vtt/uvtt.ts: pure, tested parser converting UVTT grid-unit geometry to
  image pixels (pixels_per_grid → gridSize), extracting the base64 image, walls
  (line_of_sight), doors (portals) and lights; plus toUvtt() for export.
- BattleMap gains walls/doors/lights (Dexie v11, backfilled). mapsRepo.createFromVtt.
- MapsPage "+ Add map" now accepts .dd2vtt/.uvtt/.df2vtt (text) and images, and a
  per-map "Export .uvtt" (image dims → map_size). downloadText() helper.
- Walls render faintly for the GM (CanvasView.walls / drawScene) so imported
  line-of-sight is visible; Phase 4 will turn walls into dynamic vision.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-08 14:51:56 +02:00
parent 1aff63f29c
commit 36f0ea66b3
12 changed files with 338 additions and 16 deletions
+15
View File
@@ -25,6 +25,7 @@ import {
type BattleMap,
} from '@/lib/schemas';
import { getSystem } from '@/lib/rules';
import type { ParsedUvtt } from '@/lib/vtt/uvtt';
function now(): string {
return new Date().toISOString();
@@ -306,6 +307,20 @@ export const mapsRepo = {
await db.maps.add(map);
return map;
},
/** Create a map from a parsed Universal VTT (image + grid + walls/doors/lights). */
async createFromVtt(campaignId: string, name: string, vtt: ParsedUvtt): Promise<BattleMap> {
const ts = now();
const map = battleMapSchema.parse({
id: newId(), campaignId, name, image: vtt.image, gridSize: vtt.gridSize,
showGrid: true, fogEnabled: false, revealed: [], tokens: [],
walls: vtt.walls.map((w) => ({ id: newId(), points: w.points })),
doors: vtt.doors.map((d) => ({ id: newId(), a: d.a, b: d.b, open: d.open })),
lights: vtt.lights.map((l) => ({ id: newId(), x: l.x, y: l.y, range: l.range, color: l.color, intensity: l.intensity })),
createdAt: ts, updatedAt: ts,
});
await db.maps.add(map);
return map;
},
get(id: string): Promise<BattleMap | undefined> {
return db.maps.get(id);
},