Der Chat-Tab hostet ab jetzt den Workspace-Canvas (eine ChatScreen-Instanz,
kein Doppel-Mount). Zwei Ebenen:
- Welt-Ebene (reanimated scale/translate): leichte Thumbnail-Kacheln, 2-Finger
Pinch-Zoom + 2-Finger-Pan nur in der Uebersicht (gesture-handler).
- Identity-Content-Ebene (Scale 1): schwere Inhalte (ChatScreen + kommende
WebViews) immer gemountet, nur die fokussierte per display sichtbar → Touch/
Keyboard bleiben korrekt, nichts remountet beim Fokuswechsel.
Tap auf Kachel = Fokus (voll interaktiv), "⤢ Uebersicht"/Hardware-Back = zurueck
zur Landkarte. Bei nur einer Kachel (reiner Chat) ist diese dauerhaft fokussiert
→ verhaelt sich exakt wie der bisherige Vollbild-Chat.
Neue Dateien unter android/src/workspace/: layout.ts (Kamera-Mathe, Center-
Origin fuer RN 0.73), WorkspaceCanvas.tsx, WorkspaceScreen.tsx, Tile.tsx,
tiles/{ChatTile,CodeEditorTile,VncTile,PreviewTile}.tsx. Editor/VNC sind noch
Platzhalter (Commit 5/7). tsc clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
96 lines
3.8 KiB
TypeScript
96 lines
3.8 KiB
TypeScript
/**
|
|
* 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<TileId, TileRect> = {
|
|
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<TileId, TileDef> = {
|
|
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 };
|
|
}
|