/** * Formatierungs- und Mapping-Helfer der Weboberfläche. * Zeitkritische Werte werden tabellarisch monospaced dargestellt (§17.6). */ import type { LayerStatus, LayerType } from './types'; /** Klassennamen kompakt zusammenführen; leere/false-Teile entfallen. */ export function cx(...parts: ReadonlyArray): string { return parts.filter((p): p is string => typeof p === 'string' && p.length > 0).join(' '); } export function clamp(value: number, min: number, max: number): number { return Math.min(max, Math.max(min, value)); } /** Sekunden → „M:SS.t“ (tabellarisch, Zehntelsekunden). */ export function formatTimecode(totalSec: number): string { const safe = Number.isFinite(totalSec) && totalSec >= 0 ? totalSec : 0; const minutes = Math.floor(safe / 60); const seconds = Math.floor(safe % 60); const tenth = Math.floor((safe % 1) * 10); return `${minutes}:${seconds.toString().padStart(2, '0')}.${tenth}`; } /** Uhrzeit HH:MM:SS (lokal). */ export function formatClock(date: Date): string { const h = date.getHours().toString().padStart(2, '0'); const m = date.getMinutes().toString().padStart(2, '0'); const s = date.getSeconds().toString().padStart(2, '0'); return `${h}:${m}:${s}`; } export function formatTimeMs(ms: number): string { return formatClock(new Date(ms)); } /** FPS mit zwei Dezimalstellen (§17.2); null = keine Messung. */ export function formatFps(v: number | null): string { return v === null ? '—' : v.toFixed(2); } /** Deutsche Zahleneingabe: „12,5“ → 12.5; null wenn nicht parsebar. */ export function parseNumberInput(raw: string): number | null { const trimmed = raw.trim().replace(',', '.'); if (trimmed === '') return null; const parsed = Number(trimmed); return Number.isFinite(parsed) ? parsed : null; } export function errorMessage(err: unknown): string { if (err instanceof Error) return err.message; return 'unbekannter Fehler'; } /** Opacity: Engine speichert 0..1, die UI zeigt 0..100. */ export function engineToOpacity(v: number): number { return clamp(Math.round(v * 100), 0, 100); } export function opacityToEngine(v: number): number { return clamp(v, 0, 100) / 100; } /** Text-Label je Zustand – Status nie allein über Farbe (§17.6). */ export const STATUS_LABEL: Record = { active: 'Aktiv', paused: 'Pausiert', preloaded: 'Vorgeladen', warning: 'Warnung', error: 'Fehler', offline: 'Offline', out_of_sync: 'Nicht synchron', }; /** Farbtöne je Zustand (ergänzt durch Glyphe/Text, nie allein). */ export const STATUS_TONE: Record = { active: 'st-active', paused: 'st-paused', preloaded: 'st-preloaded', warning: 'st-warning', error: 'st-error', offline: 'st-offline', out_of_sync: 'st-out-of-sync', }; export const TYPE_LABEL: Record = { media: 'Media', generator: 'Generator', adjustment: 'Adjustment', group: 'Group', }; export const BLEND_MODES: readonly string[] = [ 'Normal', 'Add', 'Screen', 'Multiply', 'Dissolve', 'Lighten', ]; export const LOOP_MODES: readonly string[] = ['Loop', 'Once', 'Ping-Pong', 'Hold']; /** Parameterpfad §10.2 (packages/parameter_engine/hms_parameter/paths.py). */ export function layerOpacityPath(compositionId: string, layerId: string): string { return `composition/${compositionId}/layer/${layerId}/opacity`; }