fix: Restore all files lost during destructive rebase
A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.
This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).
Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
359
studio-v2/lib/geo-lernwelt/unityBridge.ts
Normal file
359
studio-v2/lib/geo-lernwelt/unityBridge.ts
Normal file
@@ -0,0 +1,359 @@
|
||||
/**
|
||||
* 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',
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user