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
+19 -2
View File
@@ -36,6 +36,8 @@ export interface CanvasView {
revealed: string[];
tokens: CanvasToken[];
drawings: CanvasDrawing[];
/** sight-blocking walls, drawn faintly for the GM (not sent to players) */
walls?: { points: Point[] }[] | undefined;
}
export interface Overlay {
@@ -137,7 +139,7 @@ export function MapCanvas({ view, viewportHeight = '70vh', readOnly, playerFog,
drawScene(canvasRef.current, imgRef.current, size.w, size.h, {
vp, W, H, gridSize, cols, rows,
showGrid: view.showGrid, fogEnabled: view.fogEnabled, revealed: view.revealed,
playerFog: !!playerFog, drawings: view.drawings, overlay,
playerFog: !!playerFog, drawings: view.drawings, overlay, walls: view.walls,
});
});
return () => cancelAnimationFrame(raf);
@@ -228,7 +230,7 @@ interface SceneParams {
vp: Viewport;
W: number; H: number; gridSize: number; cols: number; rows: number;
showGrid: boolean; fogEnabled: boolean; revealed: string[]; playerFog: boolean;
drawings: CanvasDrawing[]; overlay?: Overlay | undefined;
drawings: CanvasDrawing[]; overlay?: Overlay | undefined; walls?: { points: Point[] }[] | undefined;
}
/** Draw the whole scene at device resolution with a camera transform. */
@@ -291,6 +293,21 @@ function drawScene(canvas: HTMLCanvasElement | null, img: HTMLImageElement | nul
cam();
for (const d of p.drawings) drawShape(ctx, d);
// Walls (GM only — faint, so imported line-of-sight is visible while editing)
if (p.walls?.length) {
cam();
ctx.strokeStyle = 'rgba(80,200,255,0.6)';
ctx.lineWidth = 2 / zoom;
ctx.lineJoin = 'round'; ctx.lineCap = 'round';
for (const w of p.walls) {
if (w.points.length < 2) continue;
ctx.beginPath();
ctx.moveTo(w.points[0]!.x, w.points[0]!.y);
for (const pt of w.points.slice(1)) ctx.lineTo(pt.x, pt.y);
ctx.stroke();
}
}
// Overlay (AoE/measure/draft/poly/brush)
const o = p.overlay;
if (o) {