Files
Benjamin Boenisch 5a31f52310 Initial commit: breakpilot-lehrer - Lehrer KI Platform
Services: Admin-Lehrer, Backend-Lehrer, Studio v2, Website,
Klausur-Service, School-Service, Voice-Service, Geo-Service,
BreakPilot Drive, Agent-Core

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 23:47:26 +01:00

360 lines
8.0 KiB
TypeScript

/**
* 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<void>
}
// 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<string> {
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',
}
}