export const MOTION_PROVIDER_BROWSER = "browser_camera"; export const MOTION_PROVIDER_AGENT = "motion_agent"; export const DEFAULT_MOTION_PROVIDER = MOTION_PROVIDER_BROWSER; export const MOTION_PROVIDERS = new Set([ MOTION_PROVIDER_BROWSER, MOTION_PROVIDER_AGENT, ]); export const MOTION_GESTURES = new Set([ "rotate_left", "rotate_right", "rotate_up", "rotate_down", "zoom_in", "zoom_out", "focus_prev", "focus_next", "layer_prev", "layer_next", "confirm", ]); export function clamp01(value, fallback = 1) { const number = Number(value); if (!Number.isFinite(number)) return fallback; return Math.max(0, Math.min(1, number)); } export function normalizeMotionProvider(value, fallback = DEFAULT_MOTION_PROVIDER) { const normalized = String(value || "").trim(); if (normalized === "browser") return MOTION_PROVIDER_BROWSER; if (normalized === "agent") return MOTION_PROVIDER_AGENT; return MOTION_PROVIDERS.has(normalized) ? normalized : fallback; } export function normalizeGestureMessage(raw, fallbackSource = "motion-provider") { if (!raw || typeof raw !== "object") return null; if (raw.type !== "gesture") return null; const gesture = String(raw.gesture || "").trim(); if (!MOTION_GESTURES.has(gesture)) return null; return { type: "gesture", gesture, phase: raw.phase || "discrete", confidence: clamp01(raw.confidence, 0), intensity: clamp01(raw.intensity, 1), timestampMs: Number(raw.timestamp_ms || raw.timestampMs || Date.now()), seq: Number(raw.seq || 0), source: raw.source || fallbackSource, mode: raw.mode || "single", protocolVersion: raw.protocol_version || raw.protocolVersion || "motion.v1", cameraId: raw.camera_id || raw.cameraId || "unknown", inputMode: raw.input_mode || raw.inputMode || raw.mode || "single", fusion: raw.fusion && typeof raw.fusion === "object" ? raw.fusion : null, payload: raw.payload && typeof raw.payload === "object" ? raw.payload : {}, }; } export function normalizeSkeletonMessage(raw, fallbackSource = "motion-provider") { if (!raw || typeof raw !== "object" || raw.type !== "skeleton") return null; const joints = Array.isArray(raw.joints) ? raw.joints .map((joint) => { const id = String(joint?.id || "").trim(); const x = Number(joint?.x); const y = Number(joint?.y); if (!id || !Number.isFinite(x) || !Number.isFinite(y)) return null; return { id, x: clamp01(x, 0), y: clamp01(y, 0), confidence: clamp01(joint?.confidence, 1), }; }) .filter(Boolean) : []; const bones = Array.isArray(raw.bones) ? raw.bones .map((bone) => { if (!Array.isArray(bone) || bone.length < 2) return null; const from = String(bone[0] || "").trim(); const to = String(bone[1] || "").trim(); return from && to ? [from, to] : null; }) .filter(Boolean) : []; return { type: "skeleton", timestampMs: Number(raw.timestamp_ms || raw.timestampMs || Date.now()), source: raw.source || fallbackSource, mode: raw.mode || "single", cameraId: raw.camera_id || raw.cameraId || "unknown", matchedGesture: raw.matched_gesture || raw.matchedGesture || null, confidence: clamp01(raw.confidence, 0), joints, bones, }; }