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:
Benjamin Admin
2026-02-09 09:51:32 +01:00
parent f7487ee240
commit bfdaf63ba9
2009 changed files with 749983 additions and 1731 deletions

View File

@@ -0,0 +1,276 @@
'use client'
import {
createContext,
useContext,
useReducer,
useCallback,
ReactNode,
} from 'react'
import {
AOIResponse,
AOITheme,
AOIQuality,
Difficulty,
GeoJSONPolygon,
LearningNode,
GeoLernweltState,
} from '@/app/geo-lernwelt/types'
// Initial state
const initialState: GeoLernweltState = {
currentAOI: null,
drawnPolygon: null,
selectedTheme: 'topographie',
quality: 'medium',
difficulty: 'mittel',
learningNodes: [],
selectedNode: null,
isDrawing: false,
isLoading: false,
error: null,
unityReady: false,
unityProgress: 0,
}
// Action types
type GeoAction =
| { type: 'SET_AOI'; payload: AOIResponse | null }
| { type: 'SET_POLYGON'; payload: GeoJSONPolygon | null }
| { type: 'SET_THEME'; payload: AOITheme }
| { type: 'SET_QUALITY'; payload: AOIQuality }
| { type: 'SET_DIFFICULTY'; payload: Difficulty }
| { type: 'SET_LEARNING_NODES'; payload: LearningNode[] }
| { type: 'ADD_LEARNING_NODE'; payload: LearningNode }
| { type: 'UPDATE_LEARNING_NODE'; payload: LearningNode }
| { type: 'REMOVE_LEARNING_NODE'; payload: string }
| { type: 'SELECT_NODE'; payload: LearningNode | null }
| { type: 'SET_DRAWING'; payload: boolean }
| { type: 'SET_LOADING'; payload: boolean }
| { type: 'SET_ERROR'; payload: string | null }
| { type: 'SET_UNITY_READY'; payload: boolean }
| { type: 'SET_UNITY_PROGRESS'; payload: number }
| { type: 'RESET' }
// Reducer
function geoReducer(state: GeoLernweltState, action: GeoAction): GeoLernweltState {
switch (action.type) {
case 'SET_AOI':
return { ...state, currentAOI: action.payload }
case 'SET_POLYGON':
return { ...state, drawnPolygon: action.payload }
case 'SET_THEME':
return { ...state, selectedTheme: action.payload }
case 'SET_QUALITY':
return { ...state, quality: action.payload }
case 'SET_DIFFICULTY':
return { ...state, difficulty: action.payload }
case 'SET_LEARNING_NODES':
return { ...state, learningNodes: action.payload }
case 'ADD_LEARNING_NODE':
return {
...state,
learningNodes: [...state.learningNodes, action.payload],
}
case 'UPDATE_LEARNING_NODE':
return {
...state,
learningNodes: state.learningNodes.map((node) =>
node.id === action.payload.id ? action.payload : node
),
}
case 'REMOVE_LEARNING_NODE':
return {
...state,
learningNodes: state.learningNodes.filter(
(node) => node.id !== action.payload
),
selectedNode:
state.selectedNode?.id === action.payload ? null : state.selectedNode,
}
case 'SELECT_NODE':
return { ...state, selectedNode: action.payload }
case 'SET_DRAWING':
return { ...state, isDrawing: action.payload }
case 'SET_LOADING':
return { ...state, isLoading: action.payload }
case 'SET_ERROR':
return { ...state, error: action.payload }
case 'SET_UNITY_READY':
return { ...state, unityReady: action.payload }
case 'SET_UNITY_PROGRESS':
return { ...state, unityProgress: action.payload }
case 'RESET':
return initialState
default:
return state
}
}
// Context types
interface GeoContextValue {
state: GeoLernweltState
dispatch: React.Dispatch<GeoAction>
// Convenience actions
setAOI: (aoi: AOIResponse | null) => void
setPolygon: (polygon: GeoJSONPolygon | null) => void
setTheme: (theme: AOITheme) => void
setQuality: (quality: AOIQuality) => void
setDifficulty: (difficulty: Difficulty) => void
setLearningNodes: (nodes: LearningNode[]) => void
addNode: (node: LearningNode) => void
updateNode: (node: LearningNode) => void
removeNode: (nodeId: string) => void
selectNode: (node: LearningNode | null) => void
setDrawing: (drawing: boolean) => void
setLoading: (loading: boolean) => void
setError: (error: string | null) => void
setUnityReady: (ready: boolean) => void
setUnityProgress: (progress: number) => void
reset: () => void
}
// Create context
const GeoContext = createContext<GeoContextValue | null>(null)
// Provider component
export function GeoProvider({ children }: { children: ReactNode }) {
const [state, dispatch] = useReducer(geoReducer, initialState)
// Convenience action creators
const setAOI = useCallback(
(aoi: AOIResponse | null) => dispatch({ type: 'SET_AOI', payload: aoi }),
[]
)
const setPolygon = useCallback(
(polygon: GeoJSONPolygon | null) =>
dispatch({ type: 'SET_POLYGON', payload: polygon }),
[]
)
const setTheme = useCallback(
(theme: AOITheme) => dispatch({ type: 'SET_THEME', payload: theme }),
[]
)
const setQuality = useCallback(
(quality: AOIQuality) => dispatch({ type: 'SET_QUALITY', payload: quality }),
[]
)
const setDifficulty = useCallback(
(difficulty: Difficulty) =>
dispatch({ type: 'SET_DIFFICULTY', payload: difficulty }),
[]
)
const setLearningNodes = useCallback(
(nodes: LearningNode[]) =>
dispatch({ type: 'SET_LEARNING_NODES', payload: nodes }),
[]
)
const addNode = useCallback(
(node: LearningNode) => dispatch({ type: 'ADD_LEARNING_NODE', payload: node }),
[]
)
const updateNode = useCallback(
(node: LearningNode) =>
dispatch({ type: 'UPDATE_LEARNING_NODE', payload: node }),
[]
)
const removeNode = useCallback(
(nodeId: string) =>
dispatch({ type: 'REMOVE_LEARNING_NODE', payload: nodeId }),
[]
)
const selectNode = useCallback(
(node: LearningNode | null) => dispatch({ type: 'SELECT_NODE', payload: node }),
[]
)
const setDrawing = useCallback(
(drawing: boolean) => dispatch({ type: 'SET_DRAWING', payload: drawing }),
[]
)
const setLoading = useCallback(
(loading: boolean) => dispatch({ type: 'SET_LOADING', payload: loading }),
[]
)
const setError = useCallback(
(error: string | null) => dispatch({ type: 'SET_ERROR', payload: error }),
[]
)
const setUnityReady = useCallback(
(ready: boolean) => dispatch({ type: 'SET_UNITY_READY', payload: ready }),
[]
)
const setUnityProgress = useCallback(
(progress: number) => dispatch({ type: 'SET_UNITY_PROGRESS', payload: progress }),
[]
)
const reset = useCallback(() => dispatch({ type: 'RESET' }), [])
const value: GeoContextValue = {
state,
dispatch,
setAOI,
setPolygon,
setTheme,
setQuality,
setDifficulty,
setLearningNodes,
addNode,
updateNode,
removeNode,
selectNode,
setDrawing,
setLoading,
setError,
setUnityReady,
setUnityProgress,
reset,
}
return <GeoContext.Provider value={value}>{children}</GeoContext.Provider>
}
// Hook to use context
export function useGeo() {
const context = useContext(GeoContext)
if (!context) {
throw new Error('useGeo must be used within a GeoProvider')
}
return context
}
// Hook for just the state (read-only)
export function useGeoState() {
const { state } = useGeo()
return state
}

View File

@@ -0,0 +1,29 @@
/**
* GeoEdu Service - Library Exports
*/
// Context
export { GeoProvider, useGeo, useGeoState } from './GeoContext'
// Map Styles
export {
createMapStyle,
createFallbackStyle,
createDarkStyle,
createTerrainStyle,
GERMANY_BOUNDS,
GERMANY_CENTER,
MAINAU_CENTER,
MAINAU_BOUNDS,
} from './mapStyles'
// Unity Bridge
export {
UnityBridge,
getUnityBridge,
createLoaderConfig,
type UnityInstance,
type UnityMessage,
type UnityMessageHandler,
type UnityLoaderConfig,
} from './unityBridge'

View File

@@ -0,0 +1,369 @@
/**
* MapLibre Style Configurations for GeoEdu Service
* Styles for displaying OSM data with terrain
*/
import { MapStyle, MapLayer } from '@/app/geo-lernwelt/types'
// Germany bounds
export const GERMANY_BOUNDS: [[number, number], [number, number]] = [
[5.87, 47.27],
[15.04, 55.06],
]
// Default center (Germany)
export const GERMANY_CENTER: [number, number] = [10.45, 51.16]
// Mainau island (demo location)
export const MAINAU_CENTER: [number, number] = [9.1925, 47.7085]
export const MAINAU_BOUNDS: [[number, number], [number, number]] = [
[9.185, 47.705],
[9.200, 47.712],
]
/**
* Create a MapLibre style for the self-hosted tile server
*/
export function createMapStyle(geoServiceUrl: string): MapStyle {
return {
version: 8,
name: 'GeoEdu Germany',
metadata: {
description: 'Self-hosted OSM tiles for DSGVO-compliant education',
attribution: '© OpenStreetMap contributors',
},
sources: {
osm: {
type: 'vector',
tiles: [`${geoServiceUrl}/api/v1/tiles/{z}/{x}/{y}.pbf`],
minzoom: 0,
maxzoom: 14,
attribution: '© OpenStreetMap contributors (ODbL)',
},
terrain: {
type: 'raster-dem',
tiles: [`${geoServiceUrl}/api/v1/terrain/{z}/{x}/{y}.png`],
tileSize: 256,
attribution: '© Copernicus DEM GLO-30',
},
hillshade: {
type: 'raster',
tiles: [`${geoServiceUrl}/api/v1/terrain/hillshade/{z}/{x}/{y}.png`],
tileSize: 256,
},
},
layers: [
// Background
{
id: 'background',
type: 'background',
paint: { 'background-color': '#f8f4f0' },
},
// Hillshade
{
id: 'hillshade',
type: 'raster',
source: 'hillshade',
paint: { 'raster-opacity': 0.3 },
},
// Water areas
{
id: 'water',
type: 'fill',
source: 'osm',
'source-layer': 'water',
paint: { 'fill-color': '#a0c8f0' },
},
// Parks
{
id: 'landuse-park',
type: 'fill',
source: 'osm',
'source-layer': 'landuse',
filter: ['==', 'class', 'park'],
paint: { 'fill-color': '#c8e6c8', 'fill-opacity': 0.5 },
},
// Forest
{
id: 'landuse-forest',
type: 'fill',
source: 'osm',
'source-layer': 'landuse',
filter: ['==', 'class', 'wood'],
paint: { 'fill-color': '#94d294', 'fill-opacity': 0.5 },
},
// Buildings
{
id: 'building',
type: 'fill',
source: 'osm',
'source-layer': 'building',
minzoom: 13,
paint: {
'fill-color': '#d9d0c9',
'fill-opacity': 0.8,
},
},
// Building outlines
{
id: 'building-outline',
type: 'line',
source: 'osm',
'source-layer': 'building',
minzoom: 13,
paint: {
'line-color': '#b8a89a',
'line-width': 1,
},
},
// Minor roads
{
id: 'road-minor',
type: 'line',
source: 'osm',
'source-layer': 'transportation',
filter: ['all', ['==', '$type', 'LineString'], ['in', 'class', 'minor', 'service']],
paint: {
'line-color': '#ffffff',
'line-width': ['interpolate', ['linear'], ['zoom'], 10, 0.5, 14, 2],
},
},
// Secondary roads
{
id: 'road-secondary',
type: 'line',
source: 'osm',
'source-layer': 'transportation',
filter: ['all', ['==', '$type', 'LineString'], ['in', 'class', 'secondary', 'tertiary']],
paint: {
'line-color': '#ffc107',
'line-width': ['interpolate', ['linear'], ['zoom'], 8, 1, 14, 4],
},
},
// Primary roads
{
id: 'road-primary',
type: 'line',
source: 'osm',
'source-layer': 'transportation',
filter: ['all', ['==', '$type', 'LineString'], ['==', 'class', 'primary']],
paint: {
'line-color': '#ff9800',
'line-width': ['interpolate', ['linear'], ['zoom'], 6, 1, 14, 6],
},
},
// Highways
{
id: 'road-highway',
type: 'line',
source: 'osm',
'source-layer': 'transportation',
filter: ['all', ['==', '$type', 'LineString'], ['==', 'class', 'motorway']],
paint: {
'line-color': '#ff6f00',
'line-width': ['interpolate', ['linear'], ['zoom'], 4, 1, 14, 8],
},
},
// Railways
{
id: 'railway',
type: 'line',
source: 'osm',
'source-layer': 'transportation',
filter: ['==', 'class', 'rail'],
paint: {
'line-color': '#555555',
'line-width': 2,
'line-dasharray': [3, 3],
},
},
// Water lines (rivers, streams)
{
id: 'waterway',
type: 'line',
source: 'osm',
'source-layer': 'waterway',
paint: {
'line-color': '#a0c8f0',
'line-width': ['interpolate', ['linear'], ['zoom'], 10, 1, 14, 3],
},
},
// Place labels
{
id: 'place-label-city',
type: 'symbol',
source: 'osm',
'source-layer': 'place',
filter: ['==', 'class', 'city'],
layout: {
'text-field': '{name}',
'text-font': ['Open Sans Bold'],
'text-size': 16,
},
paint: {
'text-color': '#333333',
'text-halo-color': '#ffffff',
'text-halo-width': 2,
},
},
{
id: 'place-label-town',
type: 'symbol',
source: 'osm',
'source-layer': 'place',
filter: ['==', 'class', 'town'],
minzoom: 8,
layout: {
'text-field': '{name}',
'text-font': ['Open Sans Semibold'],
'text-size': 14,
},
paint: {
'text-color': '#444444',
'text-halo-color': '#ffffff',
'text-halo-width': 1.5,
},
},
{
id: 'place-label-village',
type: 'symbol',
source: 'osm',
'source-layer': 'place',
filter: ['==', 'class', 'village'],
minzoom: 10,
layout: {
'text-field': '{name}',
'text-font': ['Open Sans Regular'],
'text-size': 12,
},
paint: {
'text-color': '#555555',
'text-halo-color': '#ffffff',
'text-halo-width': 1,
},
},
],
terrain: {
source: 'terrain',
exaggeration: 1.5,
},
}
}
/**
* Fallback style using OSM raster tiles (when self-hosted tiles not available)
*/
export function createFallbackStyle(): MapStyle {
return {
version: 8,
name: 'OSM Fallback',
metadata: {
description: 'Fallback style using public OSM raster tiles',
attribution: '© OpenStreetMap contributors',
},
sources: {
osm: {
type: 'raster',
tiles: ['https://tile.openstreetmap.org/{z}/{x}/{y}.png'],
tileSize: 256,
attribution: '© OpenStreetMap contributors',
},
},
layers: [
{
id: 'osm-tiles',
type: 'raster',
source: 'osm',
minzoom: 0,
maxzoom: 19,
},
],
}
}
/**
* Dark mode variant of the map style
*/
export function createDarkStyle(geoServiceUrl: string): MapStyle {
const baseStyle = createMapStyle(geoServiceUrl)
return {
...baseStyle,
name: 'GeoEdu Germany (Dark)',
layers: baseStyle.layers.map((layer: MapLayer) => {
// Adjust colors for dark mode
if (layer.id === 'background') {
return { ...layer, paint: { 'background-color': '#1a1a2e' } }
}
if (layer.id === 'water') {
return { ...layer, paint: { 'fill-color': '#1e3a5f' } }
}
if (layer.type === 'symbol') {
return {
...layer,
paint: {
...layer.paint,
'text-color': '#e0e0e0',
'text-halo-color': '#1a1a2e',
},
}
}
return layer
}),
}
}
/**
* Terrain-focused style (for topography theme)
*/
export function createTerrainStyle(geoServiceUrl: string): MapStyle {
const baseStyle = createMapStyle(geoServiceUrl)
// Add contour lines source and layer
return {
...baseStyle,
name: 'GeoEdu Terrain',
sources: {
...baseStyle.sources,
contours: {
type: 'vector',
tiles: [`${geoServiceUrl}/api/v1/terrain/contours/{z}/{x}/{y}.pbf`],
maxzoom: 14,
},
},
layers: [
...baseStyle.layers,
{
id: 'contour-lines',
type: 'line',
source: 'contours',
'source-layer': 'contour',
minzoom: 10,
paint: {
'line-color': '#8b4513',
'line-width': ['match', ['get', 'index'], 5, 1.5, 0.5],
'line-opacity': 0.5,
},
},
{
id: 'contour-labels',
type: 'symbol',
source: 'contours',
'source-layer': 'contour',
minzoom: 12,
filter: ['==', ['%', ['get', 'ele'], 50], 0],
layout: {
'text-field': '{ele}m',
'text-font': ['Open Sans Regular'],
'text-size': 10,
'symbol-placement': 'line',
},
paint: {
'text-color': '#8b4513',
'text-halo-color': '#ffffff',
'text-halo-width': 1,
},
},
],
}
}

View 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',
}
}