/** * Unity WebGL Bridge * Communication layer between React and Unity WebGL */ import { LearningNode, AOIManifest } from '@/app/geo-lernwelt/types' // Message types from Unity to React export type UnityMessageType = | 'nodeSelected' | 'terrainLoaded' | 'cameraPosition' | 'error' | 'ready' | 'progress' export interface UnityMessage { type: UnityMessageType nodeId?: string position?: { x: number; y: number; z: number } message?: string progress?: number } // Message handler type export type UnityMessageHandler = (message: UnityMessage) => void // Unity instance interface export interface UnityInstance { SendMessage: (objectName: string, methodName: string, value?: string | number) => void Quit: () => Promise } // Unity loader configuration export interface UnityLoaderConfig { dataUrl: string frameworkUrl: string codeUrl: string streamingAssetsUrl: string companyName: string productName: string productVersion: string } /** * Unity Bridge class for managing communication */ export class UnityBridge { private instance: UnityInstance | null = null private messageHandlers: UnityMessageHandler[] = [] private isReady: boolean = false constructor() { // Set up global message handler if (typeof window !== 'undefined') { // eslint-disable-next-line @typescript-eslint/no-explicit-any (window as any).handleUnityMessage = this.handleMessage.bind(this) } } /** * Set the Unity instance (called after Unity loads) */ setInstance(instance: UnityInstance) { this.instance = instance this.isReady = true } /** * Check if Unity is ready */ ready(): boolean { return this.isReady && this.instance !== null } /** * Register a message handler */ onMessage(handler: UnityMessageHandler): () => void { this.messageHandlers.push(handler) // Return unsubscribe function return () => { const index = this.messageHandlers.indexOf(handler) if (index > -1) { this.messageHandlers.splice(index, 1) } } } /** * Handle incoming message from Unity */ private handleMessage(messageJson: string) { try { const message: UnityMessage = JSON.parse(messageJson) // Notify all handlers this.messageHandlers.forEach((handler) => { try { handler(message) } catch (e) { console.error('Error in Unity message handler:', e) } }) } catch (e) { console.error('Failed to parse Unity message:', e) } } /** * Send a message to Unity */ sendMessage(objectName: string, methodName: string, value?: string | number) { if (!this.instance) { console.warn('Unity instance not ready, message queued') return false } try { this.instance.SendMessage(objectName, methodName, value) return true } catch (e) { console.error('Error sending message to Unity:', e) return false } } // ============================================ // Terrain Commands // ============================================ /** * Load terrain from manifest */ loadTerrain(manifestUrl: string) { return this.sendMessage('TerrainManager', 'LoadManifest', manifestUrl) } /** * Set terrain exaggeration (vertical scale) */ setTerrainExaggeration(exaggeration: number) { return this.sendMessage('TerrainManager', 'SetExaggeration', exaggeration) } /** * Toggle terrain wireframe mode */ toggleWireframe(enabled: boolean) { return this.sendMessage('TerrainManager', 'SetWireframe', enabled ? 1 : 0) } // ============================================ // Camera Commands // ============================================ /** * Focus camera on a learning node */ focusOnNode(nodeId: string) { return this.sendMessage('CameraController', 'FocusOnNode', nodeId) } /** * Focus camera on a position */ focusOnPosition(x: number, y: number, z: number) { return this.sendMessage( 'CameraController', 'FocusOnPosition', JSON.stringify({ x, y, z }) ) } /** * Set camera mode */ setCameraMode(mode: 'orbit' | 'fly' | 'firstPerson') { return this.sendMessage('CameraController', 'SetMode', mode) } /** * Reset camera to default view */ resetCamera() { return this.sendMessage('CameraController', 'Reset') } // ============================================ // Learning Node Commands // ============================================ /** * Load learning nodes */ loadNodes(nodes: LearningNode[]) { return this.sendMessage( 'LearningNodeManager', 'LoadNodes', JSON.stringify(nodes) ) } /** * Add a single learning node */ addNode(node: LearningNode) { return this.sendMessage( 'LearningNodeManager', 'AddNode', JSON.stringify(node) ) } /** * Remove a learning node */ removeNode(nodeId: string) { return this.sendMessage('LearningNodeManager', 'RemoveNode', nodeId) } /** * Update a learning node */ updateNode(node: LearningNode) { return this.sendMessage( 'LearningNodeManager', 'UpdateNode', JSON.stringify(node) ) } /** * Highlight a learning node */ highlightNode(nodeId: string, highlight: boolean) { return this.sendMessage( 'LearningNodeManager', 'HighlightNode', JSON.stringify({ nodeId, highlight }) ) } /** * Show/hide all nodes */ setNodesVisible(visible: boolean) { return this.sendMessage('LearningNodeManager', 'SetVisible', visible ? 1 : 0) } // ============================================ // UI Commands // ============================================ /** * Toggle UI visibility */ toggleUI(visible: boolean) { return this.sendMessage('UIManager', 'SetVisible', visible ? 1 : 0) } /** * Show toast message */ showToast(message: string) { return this.sendMessage('UIManager', 'ShowToast', message) } /** * Set language */ setLanguage(lang: 'de' | 'en') { return this.sendMessage('UIManager', 'SetLanguage', lang) } // ============================================ // Screenshot & Recording // ============================================ /** * Take a screenshot */ takeScreenshot(): Promise { return new Promise((resolve, reject) => { const handler = (message: UnityMessage) => { if (message.type === 'error') { reject(new Error(message.message)) return true } // Would need custom message type for screenshot result return false } const unsubscribe = this.onMessage(handler) this.sendMessage('ScreenshotManager', 'TakeScreenshot') // Timeout after 5 seconds setTimeout(() => { unsubscribe() reject(new Error('Screenshot timeout')) }, 5000) }) } // ============================================ // Lifecycle // ============================================ /** * Quit Unity */ async quit() { if (this.instance) { await this.instance.Quit() this.instance = null this.isReady = false } } /** * Cleanup */ destroy() { this.messageHandlers = [] if (typeof window !== 'undefined') { // eslint-disable-next-line @typescript-eslint/no-explicit-any delete (window as any).handleUnityMessage } } } // Singleton instance let bridgeInstance: UnityBridge | null = null /** * Get or create the Unity bridge instance */ export function getUnityBridge(): UnityBridge { if (!bridgeInstance) { bridgeInstance = new UnityBridge() } return bridgeInstance } /** * Create Unity loader configuration */ export function createLoaderConfig(buildPath: string): UnityLoaderConfig { return { dataUrl: `${buildPath}/WebGL.data`, frameworkUrl: `${buildPath}/WebGL.framework.js`, codeUrl: `${buildPath}/WebGL.wasm`, streamingAssetsUrl: `${buildPath}/StreamingAssets`, companyName: 'BreakPilot', productName: 'GeoEdu Lernwelt', productVersion: '1.0.0', } }