/** * layout — Kachel-Geometrie + Kamera-Mathematik fuer den Workspace-Canvas. * * Welt-Koordinaten = Pixel bei Scale 1. Kacheln liegen auf einem festen 2x2- * Raster. Die "Welt-View" (Animated.View) ist exakt die Bounding-Box der gerade * sichtbaren Kacheln; Kinder werden relativ zu deren Ursprung positioniert. * * RN 0.73 kennt noch kein transformOrigin — Scale dreht um die View-MITTE. * Alle Kamera-Formeln rechnen deshalb mit Center-Origin: * screen = center + scale*(p - center) + translate * wobei center = (worldW/2, worldH/2) (die Welt-View sitzt bei screen 0,0). */ export type TileId = 'chat' | 'editor' | 'vnc' | 'preview'; export interface TileRect { x: number; y: number; w: number; h: number; } export interface TileDef { id: TileId; title: string; icon: string; } // Feste Kachelgroesse im Welt-Raster (Pixel bei Scale 1). const TILE_W = 1100; const TILE_H = 1500; const GAP = 140; /** Absolute Welt-Rects pro Kachel (2x2-Raster). */ export const TILE_RECTS: Record = { chat: { x: 0, y: 0, w: TILE_W, h: TILE_H }, editor: { x: TILE_W + GAP, y: 0, w: TILE_W, h: TILE_H }, vnc: { x: 0, y: TILE_H + GAP, w: TILE_W, h: TILE_H }, preview: { x: TILE_W + GAP, y: TILE_H + GAP, w: TILE_W, h: TILE_H }, }; export const TILE_META: Record = { chat: { id: 'chat', title: 'Chat', icon: '💬' }, editor: { id: 'editor', title: 'Editor', icon: '📝' }, vnc: { id: 'vnc', title: 'Desktop', icon: '🖥️' }, preview: { id: 'preview', title: 'Vorschau', icon: '🖼️' }, }; // Reihenfolge fuer stabiles Rendering. export const TILE_ORDER: TileId[] = ['chat', 'editor', 'vnc', 'preview']; export interface VisibilitySignals { kind: 'code' | 'chat'; hasCode: boolean; // schon ein code_file empfangen hasDesktop: boolean; // Desktop verfuegbar gemeldet } /** Welche Kacheln sind fuer den aktuellen Kontext sichtbar? * Chat ist immer da; Code-Projekt (explizit ODER durch ein Live-Signal) * blendet Editor/Desktop/Vorschau ein. */ export function visibleTilesFor(sig: VisibilitySignals): TileId[] { const isCode = sig.kind === 'code' || sig.hasCode || sig.hasDesktop; if (!isCode) return ['chat']; return ['chat', 'editor', 'vnc', 'preview']; } /** Bounding-Box mehrerer Kacheln. */ export function boundsOf(ids: TileId[]): TileRect { if (ids.length === 0) return { x: 0, y: 0, w: TILE_W, h: TILE_H }; let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; for (const id of ids) { const r = TILE_RECTS[id]; minX = Math.min(minX, r.x); minY = Math.min(minY, r.y); maxX = Math.max(maxX, r.x + r.w); maxY = Math.max(maxY, r.y + r.h); } return { x: minX, y: minY, w: maxX - minX, h: maxY - minY }; } /** Rect in Welt-View-lokale Koordinaten (relativ zur Bounds-Ecke) umrechnen. */ export function toLocal(rect: TileRect, bounds: TileRect): TileRect { return { x: rect.x - bounds.x, y: rect.y - bounds.y, w: rect.w, h: rect.h }; } export interface Camera { scale: number; tx: number; ty: number; } /** Kamera, die ein (lokales) Rect bildschirmfuellend zeigt (Fokus-Modus). */ export function focusCamera(localRect: TileRect, worldW: number, worldH: number, vw: number, vh: number): Camera { const s = Math.min(vw / localRect.w, vh / localRect.h); const pcx = localRect.x + localRect.w / 2; const pcy = localRect.y + localRect.h / 2; const tx = vw / 2 - worldW / 2 - s * (pcx - worldW / 2); const ty = vh / 2 - worldH / 2 - s * (pcy - worldH / 2); return { scale: s, tx, ty }; } /** Kamera, die die gesamte Welt zentriert einpasst (Uebersicht). */ export function overviewCamera(worldW: number, worldH: number, vw: number, vh: number, pad = 0.86): Camera { const s = Math.min(vw / worldW, vh / worldH) * pad; const tx = vw / 2 - worldW / 2; const ty = vh / 2 - worldH / 2; return { scale: s, tx, ty }; }