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>
360 lines
8.0 KiB
TypeScript
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',
|
|
}
|
|
}
|