From 36414fb186f9a0884adbc7da0ef34619cf225c37 Mon Sep 17 00:00:00 2001 From: jjohare Date: Fri, 19 Jun 2026 09:22:15 +0100 Subject: [PATCH] refactor: remove vircadia multi-user XR integration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit End-to-end removal of the Vircadia integration: - delete contexts/Vircadia{,Bridges}Context, services/vircadia/*, bridges/{Bots,Graph}VircadiaBridge, components/settings/VircadiaSettings, scripts/init-vircadia-db.sql, vircadia-world/ - decouple importers: App.tsx (drop providers), quest3AutoDetector (keep Quest3 AR core, drop vircadia client), settings.ts, run-benchmarks.ts, vite-env.d.ts - drop the Vircadia integration CI job from client benchmarks.yml - genericize spatial-audio doc comments (TS + Rust); strip VITE_VIRCADIA_* env Docs prose (~25 files) still reference vircadia — separate follow-up. NOTE: not build-verified in CI here; please run client typecheck + cargo check. --- client/.env.example | 15 - client/.github/workflows/benchmarks.yml | 32 +- client/scripts/run-benchmarks.ts | 30 +- client/src/app/App.tsx | 18 +- .../components/settings/VircadiaSettings.tsx | 261 ------ .../src/contexts/VircadiaBridgesContext.tsx | 276 ------- client/src/contexts/VircadiaContext.tsx | 170 ---- .../src/features/settings/config/settings.ts | 8 - client/src/services/LiveKitVoiceService.ts | 16 +- client/src/services/VoiceOrchestrator.ts | 4 +- .../services/bridges/BotsVircadiaBridge.ts | 283 ------- .../services/bridges/GraphVircadiaBridge.ts | 355 -------- client/src/services/quest3AutoDetector.ts | 50 -- .../vircadia/CollaborativeGraphSync.ts | 724 ----------------- .../services/vircadia/EntitySyncManager.ts | 327 -------- .../services/vircadia/GraphEntityMapper.ts | 349 -------- .../vircadia/ThreeJSAvatarRenderer.ts | 429 ---------- .../services/vircadia/VircadiaClientCore.ts | 445 ----------- client/src/vite-env.d.ts | 6 - config/livekit.yaml | 2 +- .../visionclaw-domain/src/config/services.rs | 4 +- crates/visionclaw-domain/src/types/speech.rs | 2 +- scripts/init-vircadia-db.sql | 101 --- src/config/services.rs | 4 +- src/services/audio_router.rs | 6 +- src/types/speech.rs | 2 +- .../server/service/schemas/AI-SHACL.ttl | 756 ------------------ 27 files changed, 28 insertions(+), 4647 deletions(-) delete mode 100644 client/src/components/settings/VircadiaSettings.tsx delete mode 100644 client/src/contexts/VircadiaBridgesContext.tsx delete mode 100644 client/src/contexts/VircadiaContext.tsx delete mode 100644 client/src/services/bridges/BotsVircadiaBridge.ts delete mode 100644 client/src/services/bridges/GraphVircadiaBridge.ts delete mode 100644 client/src/services/vircadia/CollaborativeGraphSync.ts delete mode 100644 client/src/services/vircadia/EntitySyncManager.ts delete mode 100644 client/src/services/vircadia/GraphEntityMapper.ts delete mode 100644 client/src/services/vircadia/ThreeJSAvatarRenderer.ts delete mode 100644 client/src/services/vircadia/VircadiaClientCore.ts delete mode 100644 scripts/init-vircadia-db.sql delete mode 100644 vircadia-world/server/service/schemas/AI-SHACL.ttl diff --git a/client/.env.example b/client/.env.example index b0b8cbd68..a4e43edb7 100644 --- a/client/.env.example +++ b/client/.env.example @@ -46,30 +46,17 @@ VITE_DEV_POWER_USER_PUBKEY= VITE_DEV_MODE_AUTH=false # =========================================== -# Vircadia Multi-User XR Configuration # =========================================== -VITE_VIRCADIA_SERVER_URL=ws://localhost:3020/world/ws -VITE_VIRCADIA_AUTH_TOKEN=your-system-token-here -VITE_VIRCADIA_AUTH_PROVIDER=system -VITE_VIRCADIA_ENABLED=true -VITE_VIRCADIA_SYNC_GROUP=public.NORMAL -VITE_VIRCADIA_ENABLE_MULTI_USER=true -VITE_VIRCADIA_ENABLE_SPATIAL_AUDIO=false # =========================================== # Quest 3 XR Settings # =========================================== -VITE_QUEST3_AUTO_CONNECT_VIRCADIA=true VITE_QUEST3_ENABLE_HAND_TRACKING=true VITE_QUEST3_ENABLE_PASSTHROUGH=true # =========================================== # Performance Configuration # =========================================== -VITE_VIRCADIA_BATCH_SIZE=100 -VITE_VIRCADIA_SYNC_INTERVAL_MS=100 -VITE_VIRCADIA_MAX_RECONNECT_ATTEMPTS=5 -VITE_VIRCADIA_RECONNECT_DELAY_MS=5000 # Three.js Rendering Optimization VITE_INSTANCED_RENDERING=true @@ -80,8 +67,6 @@ VITE_MAX_RENDER_DISTANCE=50 # Logging & Debug # =========================================== VITE_LOG_LEVEL=info -VITE_VIRCADIA_DEBUG=false -VITE_VIRCADIA_SUPPRESS_LOGS=false # Console logging (can be toggled at runtime via Control Center) VITE_CONSOLE_LOGGING=true diff --git a/client/.github/workflows/benchmarks.yml b/client/.github/workflows/benchmarks.yml index 098c35e6b..8236a2973 100644 --- a/client/.github/workflows/benchmarks.yml +++ b/client/.github/workflows/benchmarks.yml @@ -59,38 +59,8 @@ jobs: benchmark-results/benchmark-*.json fi - integration: - runs-on: ubuntu-latest - timeout-minutes: 30 - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '20' - cache: 'npm' - - - name: Install dependencies - working-directory: client - run: npm ci - - - name: Run Vircadia integration tests - working-directory: client - run: npm run benchmark:integration - continue-on-error: true - - - name: Upload integration results - uses: actions/upload-artifact@v4 - with: - name: integration-results - path: client/benchmark-results/vircadia-*.md - retention-days: 30 - report: - needs: [benchmark, integration] + needs: [benchmark] runs-on: ubuntu-latest if: always() diff --git a/client/scripts/run-benchmarks.ts b/client/scripts/run-benchmarks.ts index 7b8d248d4..7bea9da3a 100755 --- a/client/scripts/run-benchmarks.ts +++ b/client/scripts/run-benchmarks.ts @@ -8,7 +8,6 @@ * - Multi-user load tests * - VR performance validation * - Network resilience tests - * - Vircadia integration tests */ import * as fs from 'fs'; @@ -34,11 +33,6 @@ import { DEFAULT_NETWORK_TEST_CONFIG, NetworkTestResult } from '../src/tests/network/LatencyTest'; -import { - VircadiaIntegrationTest, - DEFAULT_VIRCADIA_CONFIG, - VircadiaTestResult -} from '../src/tests/integration/VircadiaTest'; interface TestSuite { name: string; @@ -54,7 +48,6 @@ interface BenchmarkReport { load?: LoadTestResult[]; vr?: VRPerformanceResult; network?: NetworkTestResult[]; - vircadia?: VircadiaTestResult; }; summary: { totalTests: number; @@ -187,13 +180,6 @@ class BenchmarkRunner { }); } - // Vircadia test - if (results.vircadia) { - totalTests++; - if (results.vircadia.passed) passed++; - else failed++; - if (results.vircadia.issues.length > 0) warnings++; - } return { totalTests, passed, failed, warnings }; } @@ -251,13 +237,6 @@ class BenchmarkRunner { ); } - if (report.results.vircadia) { - const md = VircadiaIntegrationTest.generateReport(report.results.vircadia); - fs.writeFileSync( - path.join(this.outputDir, `vircadia-${timestamp}.md`), - md - ); - } } /** @@ -296,7 +275,6 @@ program .option('-l, --load', 'Run load tests') .option('-v, --vr', 'Run VR performance tests') .option('-n, --network', 'Run network resilience tests') - .option('-i, --integration', 'Run Vircadia integration tests') .option('-a, --all', 'Run all tests (default)', true) .option('-o, --output ', 'Output directory', './benchmark-results') .parse(process.argv); @@ -304,12 +282,11 @@ program const options = program.opts(); // Determine which tests to run -const runAll = options.all || (!options.performance && !options.load && !options.vr && !options.network && !options.integration); +const runAll = options.all || (!options.performance && !options.load && !options.vr && !options.network); const runPerformance = runAll || options.performance; const runLoad = runAll || options.load; const runVr = runAll || options.vr; const runNetwork = runAll || options.network; -const runIntegration = runAll || options.integration; // Create runner const runner = new BenchmarkRunner(options.output); @@ -335,11 +312,6 @@ runner.registerSuite('Network Resilience', async () => { return await networkTest.run(); }, runNetwork); -runner.registerSuite('Vircadia Integration', async () => { - const vircadiaTest = new VircadiaIntegrationTest(DEFAULT_VIRCADIA_CONFIG); - return await vircadiaTest.run(); -}, runIntegration); - // Run benchmarks runner.runAll() .then(() => process.exit(0)) diff --git a/client/src/app/App.tsx b/client/src/app/App.tsx index 7ab5b3306..f9e4bf24c 100644 --- a/client/src/app/App.tsx +++ b/client/src/app/App.tsx @@ -23,8 +23,6 @@ import { ConnectionWarning } from '../components/ConnectionWarning'; import { useAutoBalanceNotifications } from '../hooks/useAutoBalanceNotifications'; import ErrorBoundary from '../components/ErrorBoundary'; import { remoteLogger } from '../services/remoteLogger'; -import { VircadiaProvider } from '../contexts/VircadiaContext'; -import { VircadiaBridgesProvider } from '../contexts/VircadiaBridgesContext'; import { useNostrAuth } from '../hooks/useNostrAuth'; import { OnboardingWizard } from '../components/OnboardingWizard'; import { LoadingScreen } from '../components/LoadingScreen'; @@ -180,24 +178,20 @@ function App() { case 'initialized': return shouldUseImmersiveClient() ? ( - - }> - - - + }> + + ) : ( - - - + ); } }; return ( - + <> Skip to graph @@ -223,7 +217,7 @@ function App() { - + ); } diff --git a/client/src/components/settings/VircadiaSettings.tsx b/client/src/components/settings/VircadiaSettings.tsx deleted file mode 100644 index 501036a9c..000000000 --- a/client/src/components/settings/VircadiaSettings.tsx +++ /dev/null @@ -1,261 +0,0 @@ - - -import React, { useState, useEffect } from 'react'; -import { useVircadia } from '../../contexts/VircadiaContext'; -import { useVircadiaBridges } from '../../contexts/VircadiaBridgesContext'; -import { useSettingsStore } from '../../store/settingsStore'; -import { createLogger } from '../../utils/loggerConfig'; - -const logger = createLogger('VircadiaSettings'); - -export const VircadiaSettings: React.FC = () => { - const { client, connect, disconnect, connectionInfo, isConnected, isConnecting, error } = useVircadia(); - const { isInitialized: bridgesInitialized, activeUsers, error: bridgeError } = useVircadiaBridges(); - - const [vircadiaEnabled, setVircadiaEnabled] = useState(false); - const [serverUrl, setServerUrl] = useState('ws://vircadia-world-server:3020/world/ws'); - const [autoConnect, setAutoConnect] = useState(false); - - - useEffect(() => { - const settings = useSettingsStore.getState().settings; - const vircadiaConfig = settings?.vircadia; - - if (vircadiaConfig) { - setVircadiaEnabled(vircadiaConfig.enabled || false); - setServerUrl(vircadiaConfig.serverUrl || serverUrl); - setAutoConnect(vircadiaConfig.autoConnect || false); - } - }, []); - - - const saveSettings = () => { - useSettingsStore.getState().updateSettings((draft) => { - draft.vircadia = { - enabled: vircadiaEnabled, - serverUrl, - autoConnect - }; - }); - }; - - - const handleEnableToggle = async () => { - const newEnabled = !vircadiaEnabled; - setVircadiaEnabled(newEnabled); - - if (newEnabled) { - try { - await connect(); - logger.info('Vircadia enabled and connected'); - } catch (err) { - logger.error('Failed to connect to Vircadia:', err); - } - } else { - disconnect(); - logger.info('Vircadia disabled'); - } - - saveSettings(); - }; - - - const handleServerUrlChange = (e: React.ChangeEvent) => { - setServerUrl(e.target.value); - saveSettings(); - }; - - - const handleReconnect = async () => { - disconnect(); - setTimeout(async () => { - try { - await connect(); - } catch (err) { - logger.error('Failed to reconnect:', err); - } - }, 1000); - }; - - - const getStatusColor = () => { - if (isConnected) return 'text-green-500'; - if (isConnecting) return 'text-yellow-500'; - if (error) return 'text-red-500'; - return 'text-gray-500'; - }; - - const getStatusText = () => { - if (isConnected) return 'Connected'; - if (isConnecting) return 'Connecting...'; - if (error) return 'Error'; - return 'Disconnected'; - }; - - return ( -
-
-

Multi-User XR (Vircadia)

-

- Enable collaborative visualization with multiple users in real-time -

-
- - {} -
-
- -

Connect to Vircadia World Server for collaboration

-
- -
- - {} -
- - -

- Default: ws://vircadia-world-server:3020/world/ws (Docker network) -

-
- - {} -
-
-
-

Connection Status

-

{getStatusText()}

-
- {vircadiaEnabled && !isConnecting && ( - - )} -
- - {connectionInfo && ( -
-
- Agent ID: - {connectionInfo.agentId || 'N/A'} -
-
- Session ID: - {connectionInfo.sessionId || 'N/A'} -
- {connectionInfo.connectionDuration && ( -
- Connected For: - {Math.floor(connectionInfo.connectionDuration / 1000)}s -
- )} -
- )} - - {error && ( -
-

Error: {error.message}

-
- )} - - {bridgeError && ( -
-

Bridge Error: {bridgeError.message}

-
- )} -
- - {} - {bridgesInitialized && isConnected && ( -
-

Active Users ({activeUsers.length})

- {activeUsers.length === 0 ? ( -

No other users connected

- ) : ( -
- {activeUsers.map((user) => ( -
-
-
- {user.username} -
- {user.selectedNodes.length > 0 && ( - - {user.selectedNodes.length} nodes selected - - )} -
- ))} -
- )} -
- )} - - {} - {vircadiaEnabled && isConnected && ( -
-

Synchronization Status

-
-
- Bots Bridge: - - {bridgesInitialized ? '✓ Active' : 'Initializing...'} - -
-
- Graph Bridge: - - {bridgesInitialized ? '✓ Active' : 'Initializing...'} - -
-
-
- )} - - {} -
-

📘 How to Use Multi-User Mode

-
    -
  • • Enable toggle to connect to Vircadia server
  • -
  • • See other users' agent selections in real-time
  • -
  • • Add annotations visible to all users
  • -
  • • Hear spatial audio based on proximity
  • -
  • • Best experienced in VR with Meta Quest 3
  • -
-
- - {} -
-

🐳 Docker Setup

-

To enable Vircadia server, run:

- - docker-compose -f docker-compose.yml -f docker-compose.vircadia.yml --profile dev up -d - -
-
- ); -}; diff --git a/client/src/contexts/VircadiaBridgesContext.tsx b/client/src/contexts/VircadiaBridgesContext.tsx deleted file mode 100644 index e1f0af82b..000000000 --- a/client/src/contexts/VircadiaBridgesContext.tsx +++ /dev/null @@ -1,276 +0,0 @@ - - -import React, { createContext, useContext, useEffect, useState, useCallback, useRef } from 'react'; -import { useVircadia } from './VircadiaContext'; -import { BotsVircadiaBridge } from '../services/bridges/BotsVircadiaBridge'; -import { GraphVircadiaBridge, type UserSelectionEvent, type AnnotationEvent } from '../services/bridges/GraphVircadiaBridge'; -import { EntitySyncManager } from '../services/vircadia/EntitySyncManager'; -// Note: AvatarManager removed - requires camera which context doesn't have -import { CollaborativeGraphSync } from '../services/vircadia/CollaborativeGraphSync'; -import { createLogger } from '../utils/loggerConfig'; -import type { BotsAgent, BotsEdge } from '../features/bots/types/BotsTypes'; - -const logger = createLogger('VircadiaBridgesContext'); - -interface VircadiaBridgesContextValue { - - botsBridge: BotsVircadiaBridge | null; - syncAgentsToVircadia: (agents: BotsAgent[], edges: BotsEdge[]) => void; - startBotsAutoSync: (getAgentsCallback: () => { agents: BotsAgent[]; edges: BotsEdge[] }) => void; - stopBotsAutoSync: () => void; - - - graphBridge: GraphVircadiaBridge | null; - syncGraphToVircadia: (nodes: any[], edges: any[]) => void; - broadcastSelection: (nodeIds: string[]) => void; - addAnnotation: (nodeId: string, text: string, position: { x: number; y: number; z: number }) => Promise; - removeAnnotation: (annotationId: string) => Promise; - activeUsers: Array<{ userId: string; username: string; selectedNodes: string[] }>; - annotations: AnnotationEvent[]; - - - isInitialized: boolean; - error: Error | null; -} - -const VircadiaBridgesContext = createContext(null); - -interface VircadiaBridgesProviderProps { - children: React.ReactNode; - scene?: any; - enableBotsBridge?: boolean; - enableGraphBridge?: boolean; -} - -export const VircadiaBridgesProvider: React.FC = ({ - children, - scene, - enableBotsBridge = true, - enableGraphBridge = true -}) => { - const { client, isConnected } = useVircadia(); - const [botsBridge, setBotsBridge] = useState(null); - const [graphBridge, setGraphBridge] = useState(null); - const botsBridgeRef = useRef(null); - const graphBridgeRef = useRef(null); - const [isInitialized, setIsInitialized] = useState(false); - const [error, setError] = useState(null); - const [activeUsers, setActiveUsers] = useState>([]); - const [annotations, setAnnotations] = useState([]); - - - useEffect(() => { - if (!client || !isConnected) { - setIsInitialized(false); - return; - } - - const initializeBridges = async () => { - try { - logger.info('Initializing Vircadia bridges...'); - - - if (enableBotsBridge) { - // EntitySyncManager takes (client, config?) - not scene - const entitySync = new EntitySyncManager(client, { - syncGroup: 'public.NORMAL', - enableRealTimePositions: true - }); - - // AvatarManager requires camera - pass null since we don't have it here - // BotsVircadiaBridge accepts null for avatars - const bBridge = new BotsVircadiaBridge(client, entitySync, null); - await bBridge.initialize(); - botsBridgeRef.current = bBridge; - setBotsBridge(bBridge); - - logger.info('BotsVircadiaBridge initialized'); - } - - - if (enableGraphBridge && scene) { - const collab = new CollaborativeGraphSync(scene, client); - const gBridge = new GraphVircadiaBridge(scene, client, collab); - await gBridge.initialize(); - - - gBridge.onRemoteSelection((event: UserSelectionEvent) => { - logger.debug('Remote selection:', event); - updateActiveUsers(); - }); - - gBridge.onAnnotation((event: AnnotationEvent) => { - logger.info('Remote annotation:', event); - updateAnnotations(); - }); - - graphBridgeRef.current = gBridge; - setGraphBridge(gBridge); - - logger.info('GraphVircadiaBridge initialized'); - } - - setIsInitialized(true); - setError(null); - } catch (err) { - const error = err instanceof Error ? err : new Error(String(err)); - logger.error('Failed to initialize bridges:', error); - setError(error); - setIsInitialized(false); - } - }; - - initializeBridges(); - - - return () => { - if (botsBridgeRef.current) { - botsBridgeRef.current.dispose(); - botsBridgeRef.current = null; - setBotsBridge(null); - } - if (graphBridgeRef.current) { - graphBridgeRef.current.dispose(); - graphBridgeRef.current = null; - setGraphBridge(null); - } - setIsInitialized(false); - }; - }, [client, isConnected, scene, enableBotsBridge, enableGraphBridge]); - - - const updateActiveUsers = useCallback(() => { - if (graphBridge) { - setActiveUsers(graphBridge.getActiveUsers()); - } - }, [graphBridge]); - - - const updateAnnotations = useCallback(() => { - if (graphBridge) { - setAnnotations(graphBridge.getAnnotations()); - } - }, [graphBridge]); - - - useEffect(() => { - if (!graphBridge) return; - - const interval = setInterval(updateActiveUsers, 2000); - return () => clearInterval(interval); - }, [graphBridge, updateActiveUsers]); - - - const syncAgentsToVircadia = useCallback((agents: BotsAgent[], edges: BotsEdge[]) => { - if (botsBridge) { - botsBridge.syncAgentsToVircadia(agents, edges); - } - }, [botsBridge]); - - const startBotsAutoSync = useCallback((getAgentsCallback: () => { agents: BotsAgent[]; edges: BotsEdge[] }) => { - if (botsBridge) { - botsBridge.startAutoSync(getAgentsCallback); - } - }, [botsBridge]); - - const stopBotsAutoSync = useCallback(() => { - if (botsBridge) { - botsBridge.stopAutoSync(); - } - }, [botsBridge]); - - - const syncGraphToVircadia = useCallback((nodes: any[], edges: any[]) => { - if (graphBridge) { - graphBridge.syncGraphToVircadia(nodes, edges); - } - }, [graphBridge]); - - const broadcastSelection = useCallback((nodeIds: string[]) => { - if (graphBridge) { - graphBridge.broadcastLocalSelection(nodeIds); - } - }, [graphBridge]); - - const addAnnotation = useCallback(async ( - nodeId: string, - text: string, - position: { x: number; y: number; z: number } - ): Promise => { - if (!graphBridge) { - throw new Error('Graph bridge not initialized'); - } - const id = await graphBridge.addAnnotation(nodeId, text, position); - updateAnnotations(); - return id; - }, [graphBridge, updateAnnotations]); - - const removeAnnotation = useCallback(async (annotationId: string): Promise => { - if (graphBridge) { - await graphBridge.removeAnnotation(annotationId); - updateAnnotations(); - } - }, [graphBridge, updateAnnotations]); - - const value: VircadiaBridgesContextValue = { - botsBridge, - syncAgentsToVircadia, - startBotsAutoSync, - stopBotsAutoSync, - graphBridge, - syncGraphToVircadia, - broadcastSelection, - addAnnotation, - removeAnnotation, - activeUsers, - annotations, - isInitialized, - error - }; - - return ( - - {children} - - ); -}; - - -export const useVircadiaBridges = (): VircadiaBridgesContextValue => { - const context = useContext(VircadiaBridgesContext); - if (!context) { - throw new Error('useVircadiaBridges must be used within VircadiaBridgesProvider'); - } - return context; -}; - - -export const useBotsBridge = () => { - const { botsBridge, syncAgentsToVircadia, startBotsAutoSync, stopBotsAutoSync, isInitialized } = useVircadiaBridges(); - return { botsBridge, syncAgentsToVircadia, startBotsAutoSync, stopBotsAutoSync, isInitialized }; -}; - - -export const useGraphBridge = () => { - const { - graphBridge, - syncGraphToVircadia, - broadcastSelection, - addAnnotation, - removeAnnotation, - activeUsers, - annotations, - isInitialized - } = useVircadiaBridges(); - - return { - graphBridge, - syncGraphToVircadia, - broadcastSelection, - addAnnotation, - removeAnnotation, - activeUsers, - annotations, - isInitialized - }; -}; diff --git a/client/src/contexts/VircadiaContext.tsx b/client/src/contexts/VircadiaContext.tsx deleted file mode 100644 index 68dd5222b..000000000 --- a/client/src/contexts/VircadiaContext.tsx +++ /dev/null @@ -1,170 +0,0 @@ -import React, { createContext, useContext, useEffect, useState, useCallback, useRef } from 'react'; -import { ClientCore, ClientCoreConfig, ClientCoreConnectionInfo } from '../services/vircadia/VircadiaClientCore'; -import { createLogger } from '../utils/loggerConfig'; - -const logger = createLogger('VircadiaContext'); - -interface VircadiaContextValue { - client: ClientCore | null; - connectionInfo: ClientCoreConnectionInfo | null; - isConnected: boolean; - isConnecting: boolean; - error: Error | null; - connect: () => Promise; - disconnect: () => void; -} - -const VircadiaContext = createContext(null); - -interface VircadiaProviderProps { - children: React.ReactNode; - config?: Partial; - autoConnect?: boolean; -} - -export const VircadiaProvider: React.FC = ({ - children, - config, - autoConnect = false -}) => { - const [client, setClient] = useState(null); - const [connectionInfo, setConnectionInfo] = useState(null); - const [error, setError] = useState(null); - const clientRef = useRef(null); - const configRef = useRef(config); - configRef.current = config; - - // Serialize config to a stable string so the effect only re-runs when - // the actual config values change, not on every render identity change. - const configKey = JSON.stringify(config ?? {}); - - useEffect(() => { - const mergedConfig: ClientCoreConfig = { - serverUrl: import.meta.env.VITE_VIRCADIA_SERVER_URL || 'ws://localhost:3020/world/ws', - authToken: import.meta.env.VITE_VIRCADIA_AUTH_TOKEN || '', - authProvider: import.meta.env.VITE_VIRCADIA_AUTH_PROVIDER || 'system', - reconnectAttempts: 5, - reconnectDelay: 5000, - debug: import.meta.env.DEV || false, - suppress: false, - ...configRef.current - }; - - logger.info('Initializing Vircadia client with config:', { - serverUrl: mergedConfig.serverUrl, - authProvider: mergedConfig.authProvider, - debug: mergedConfig.debug - }); - - const vircadiaClient = new ClientCore(mergedConfig); - setClient(vircadiaClient); - clientRef.current = vircadiaClient; - - const handleStatusChange = () => { - const info = vircadiaClient.Utilities.Connection.getConnectionInfo(); - setConnectionInfo(info); - logger.info('Connection status changed:', info.status); - }; - - vircadiaClient.Utilities.Connection.addEventListener('statusChange', handleStatusChange); - - return () => { - vircadiaClient.Utilities.Connection.removeEventListener('statusChange', handleStatusChange); - vircadiaClient.dispose(); - clientRef.current = null; - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [configKey]); - - - useEffect(() => { - if (autoConnect && client && !connectionInfo?.isConnected) { - connect(); - } - }, [autoConnect, client]); - - const connect = useCallback(async () => { - if (!client) { - const err = new Error('Vircadia client not initialized'); - setError(err); - logger.error('Cannot connect: client not initialized'); - return; - } - - try { - setError(null); - logger.info('Connecting to Vircadia server...'); - - const info = await client.Utilities.Connection.connect({ timeoutMs: 30000 }); - - setConnectionInfo(info); - logger.info('Connected to Vircadia server', { - agentId: info.agentId, - sessionId: info.sessionId, - duration: info.connectionDuration - }); - } catch (err) { - const error = err instanceof Error ? err : new Error(String(err)); - setError(error); - logger.error('Failed to connect to Vircadia server:', error); - } - }, [client]); - - const disconnect = useCallback(() => { - if (client) { - logger.info('Disconnecting from Vircadia server'); - client.Utilities.Connection.disconnect(); - setConnectionInfo(null); - } - }, [client]); - - const value: VircadiaContextValue = { - client, - connectionInfo, - isConnected: connectionInfo?.isConnected || false, - isConnecting: connectionInfo?.isConnecting || false, - error, - connect, - disconnect - }; - - return ( - - {children} - - ); -}; - -export const useVircadia = () => { - const context = useContext(VircadiaContext); - if (!context) { - throw new Error('useVircadia must be used within a VircadiaProvider'); - } - return context; -}; - -// Hook for Quest 3 XR integration -export const useVircadiaXR = () => { - const { client, isConnected, connectionInfo } = useVircadia(); - const [xrReady, setXRReady] = useState(false); - - useEffect(() => { - if (isConnected && client && connectionInfo?.agentId && connectionInfo?.sessionId) { - logger.info('Vircadia XR ready', { - agentId: connectionInfo.agentId, - sessionId: connectionInfo.sessionId - }); - setXRReady(true); - } else { - setXRReady(false); - } - }, [isConnected, client, connectionInfo]); - - return { - client, - isConnected, - xrReady, - agentId: connectionInfo?.agentId || null, - sessionId: connectionInfo?.sessionId || null - }; -}; diff --git a/client/src/features/settings/config/settings.ts b/client/src/features/settings/config/settings.ts index 210745539..7b0999a9a 100644 --- a/client/src/features/settings/config/settings.ts +++ b/client/src/features/settings/config/settings.ts @@ -721,13 +721,6 @@ export interface XRGPUSettings { }; } -// Vircadia integration settings -export interface VircadiaSettings { - enabled: boolean; - serverUrl: string; - autoConnect: boolean; -} - // Node filter settings for graph visualization export interface NodeFilterSettings { enabled: boolean; @@ -763,7 +756,6 @@ export interface Settings { performance?: PerformanceSettings; developer?: DeveloperSettings; qualityGates?: QualityGatesSettings; - vircadia?: VircadiaSettings; // Node filter settings for graph visualization nodeFilter?: NodeFilterSettings; // Client-side tweening for server-authoritative positions diff --git a/client/src/services/LiveKitVoiceService.ts b/client/src/services/LiveKitVoiceService.ts index 11a27219c..a8469efe4 100644 --- a/client/src/services/LiveKitVoiceService.ts +++ b/client/src/services/LiveKitVoiceService.ts @@ -2,13 +2,13 @@ * LiveKitVoiceService — WebRTC spatial voice chat via LiveKit SFU. * * Handles Plane 3 (user-to-user voice) and Plane 4 (agent spatial voice): - * - Connects to LiveKit room for the current Vircadia world + * - Connects to LiveKit room for the current XR world * - Publishes user microphone as a WebRTC audio track * - Subscribes to remote participants (other users + agent virtual participants) - * - Applies spatial audio panning based on Vircadia entity positions + * - Applies spatial audio panning based on XR entity positions * * Coordinate flow: - * Vircadia entity positions → CollaborativeGraphSync → this service → Web Audio panner + * XR entity positions → this service → Web Audio panner * * Audio format: Opus 48kHz mono throughout. */ @@ -26,7 +26,7 @@ export interface LiveKitConfig { roomName: string; /** Enable spatial audio panning */ spatialAudio: boolean; - /** Max distance for audio rolloff (Vircadia units) */ + /** Max distance for audio rolloff (XR units) */ maxDistance: number; } @@ -88,7 +88,7 @@ export class LiveKitVoiceService { /** * Connect to a LiveKit room for spatial voice chat. - * Call this after the user has joined a Vircadia world. + * Call this after the user has joined a XR world. */ async connect(config: LiveKitConfig): Promise { this.config = config; @@ -150,7 +150,7 @@ export class LiveKitVoiceService { // Set up audio context for spatial processing if (config.spatialAudio) { this.audioContext = new AudioContext({ sampleRate: 48000 }); - // Set listener position (will be updated from Vircadia) + // Set listener position (will be updated from XR) const listener = this.audioContext.listener; if (listener.positionX) { listener.positionX.value = 0; @@ -203,7 +203,7 @@ export class LiveKitVoiceService { } /** - * Update the listener's position (the local user's position in Vircadia world). + * Update the listener's position (the local user's position in XR world). * This drives the spatial audio panning for all remote participants. */ updateListenerPosition(position: SpatialPosition): void { @@ -221,7 +221,7 @@ export class LiveKitVoiceService { /** * Update a remote participant's spatial position. - * Called when Vircadia entity positions change (from CollaborativeGraphSync). + * Called when entity positions change. */ updateParticipantPosition(participantId: string, position: SpatialPosition): void { const participant = this.remoteParticipants.get(participantId); diff --git a/client/src/services/VoiceOrchestrator.ts b/client/src/services/VoiceOrchestrator.ts index af86d9185..f64b1f6e8 100644 --- a/client/src/services/VoiceOrchestrator.ts +++ b/client/src/services/VoiceOrchestrator.ts @@ -185,7 +185,7 @@ export class VoiceOrchestrator { /** * Update the local user's spatial position. - * Call this from the Vircadia presence sync loop. + * Call this from the XR presence sync loop. */ updateUserPosition(position: SpatialPosition): void { this.livekit.updateListenerPosition(position); @@ -193,7 +193,7 @@ export class VoiceOrchestrator { /** * Update a remote participant's (user or agent) spatial position. - * Call this when Vircadia entity positions change. + * Call this when XR entity positions change. */ updateRemotePosition(participantId: string, position: SpatialPosition): void { this.livekit.updateParticipantPosition(participantId, position); diff --git a/client/src/services/bridges/BotsVircadiaBridge.ts b/client/src/services/bridges/BotsVircadiaBridge.ts deleted file mode 100644 index 97803c7a8..000000000 --- a/client/src/services/bridges/BotsVircadiaBridge.ts +++ /dev/null @@ -1,283 +0,0 @@ - - -import { ClientCore } from '../vircadia/VircadiaClientCore'; -import { EntitySyncManager } from '../vircadia/EntitySyncManager'; -import { ThreeJSAvatarRenderer } from '../vircadia/ThreeJSAvatarRenderer'; -import type { BotsAgent, BotsEdge } from '../../features/bots/types/BotsTypes'; -import { createLogger } from '../../utils/loggerConfig'; - -const logger = createLogger('BotsVircadiaBridge'); - -export interface BridgeConfig { - syncPositions: boolean; - syncMetadata: boolean; - syncEdges: boolean; - updateInterval: number; - enableAvatars: boolean; -} - -export interface VircadiaEntity { - id: string; - type: string; - position: { x: number; y: number; z: number }; - rotation?: { x: number; y: number; z: number; w: number }; - scale?: { x: number; y: number; z: number }; - metadata?: Record; -} - -export class BotsVircadiaBridge { - private syncInterval: ReturnType | null = null; - private agentEntityMap = new Map(); - private lastSyncedAgents = new Map(); - private entityUpdateUnsubscribe: (() => void) | null = null; - private isActive = false; - - private defaultConfig: BridgeConfig = { - syncPositions: true, - syncMetadata: true, - syncEdges: true, - updateInterval: 100, - enableAvatars: true - }; - - constructor( - private client: ClientCore, - private entitySync: EntitySyncManager, - private avatars: ThreeJSAvatarRenderer | null, - config?: Partial - ) { - this.defaultConfig = { ...this.defaultConfig, ...config }; - } - - - async initialize(): Promise { - logger.info('Initializing BotsVircadiaBridge...'); - - if (!this.client.Utilities.Connection.getConnectionInfo().isConnected) { - throw new Error('Vircadia client must be connected before initializing bridge'); - } - - // EntitySyncManager exposes onEntityUpdate(callback) which returns - // an unsubscribe function. Entity deletion is handled internally via - // deleteEntity(). Subscribe to updates for the sync group. - this.entityUpdateUnsubscribe = this.entitySync.onEntityUpdate((entities) => { - entities.forEach(entity => { - this.handleVircadiaEntityUpdate(entity as unknown as VircadiaEntity); - }); - }); - - this.isActive = true; - logger.info('BotsVircadiaBridge initialized successfully'); - } - - - syncAgentsToVircadia(agents: BotsAgent[], edges: BotsEdge[]): void { - if (!this.isActive) return; - - try { - - agents.forEach(agent => { - this.syncAgentToEntity(agent); - }); - - - this.cleanupStaleEntities(agents); - - - if (this.defaultConfig.syncEdges) { - this.syncEdgesToVircadia(edges); - } - - logger.debug(`Synced ${agents.length} agents and ${edges.length} edges to Vircadia`); - } catch (error) { - logger.error('Failed to sync agents to Vircadia:', error); - } - } - - - private syncAgentToEntity(agent: BotsAgent): void { - - const lastSynced = this.lastSyncedAgents.get(agent.id); - if (lastSynced && this.isAgentUnchanged(agent, lastSynced)) { - return; - } - - const entityId = this.agentEntityMap.get(agent.id) || `agent-${agent.id}`; - - - const position = this.convertAgentPosition(agent.position || { x: 0, y: 0, z: 0 }); - - - const entityData: VircadiaEntity = { - id: entityId, - type: 'agent-avatar', - position, - scale: { x: 1, y: 1, z: 1 }, - metadata: this.defaultConfig.syncMetadata ? { - agentName: agent.name, - agentType: agent.type, - health: agent.health, - status: agent.status, - capabilities: agent.capabilities, - currentTask: agent.currentTask, - tokenUsage: agent.tokenUsage, - isActive: agent.status === 'active', - color: this.getAgentColor(agent) - } : undefined - }; - - // EntitySyncManager uses updateNodePosition for real-time position changes. - // Full entity sync (metadata) goes through pushGraphToVircadia in bulk. - if (entityData.position) { - this.entitySync.updateNodePosition(entityId, entityData.position); - } - - this.agentEntityMap.set(agent.id, entityId); - this.lastSyncedAgents.set(agent.id, structuredClone(agent)); - } - - - private convertAgentPosition(position: { x: number; y: number; z: number }): { x: number; y: number; z: number } { - - - return { - x: position.x * 10, - y: position.z * 10, - z: position.y * 10 - }; - } - - - private getAgentColor(agent: BotsAgent): string { - const typeColors: Record = { - 'researcher': '#4A90E2', - 'coder': '#50E3C2', - 'analyst': '#F5A623', - 'optimizer': '#D0021B', - 'coordinator': '#7ED321' - }; - - return typeColors[agent.type] || '#9013FE'; - } - - - private isAgentUnchanged(agent: BotsAgent, lastSynced: BotsAgent): boolean { - return ( - agent.position?.x === lastSynced.position?.x && - agent.position?.y === lastSynced.position?.y && - agent.position?.z === lastSynced.position?.z && - agent.health === lastSynced.health && - agent.status === lastSynced.status - ); - } - - - private cleanupStaleEntities(currentAgents: BotsAgent[]): void { - const currentAgentIds = new Set(currentAgents.map(a => a.id)); - const staleAgentIds: string[] = []; - - this.agentEntityMap.forEach((entityId, agentId) => { - if (!currentAgentIds.has(agentId)) { - staleAgentIds.push(agentId); - this.entitySync.deleteEntity(entityId).catch(err => { - logger.error(`Failed to delete stale entity ${entityId}:`, err); - }); - } - }); - - staleAgentIds.forEach(agentId => { - this.agentEntityMap.delete(agentId); - this.lastSyncedAgents.delete(agentId); - }); - - if (staleAgentIds.length > 0) { - logger.debug(`Cleaned up ${staleAgentIds.length} stale agent entities`); - } - } - - - private syncEdgesToVircadia(edges: BotsEdge[]): void { - - edges.forEach(edge => { - const entityId = `edge-${edge.source}-${edge.target}`; - const sourceEntity = this.agentEntityMap.get(edge.source); - const targetEntity = this.agentEntityMap.get(edge.target); - - if (sourceEntity && targetEntity) { - // Edge entities use updateNodePosition for placement; full metadata - // sync is handled via pushGraphToVircadia in bulk operations. - this.entitySync.updateNodePosition(entityId, { x: 0, y: 0, z: 0 }); - } - }); - } - - - private handleVircadiaEntityUpdate(entity: VircadiaEntity): void { - - if (entity.type !== 'agent-avatar') return; - - - const agentId = Array.from(this.agentEntityMap.entries()) - .find(([_, entityId]) => entityId === entity.id)?.[0]; - - if (!agentId) { - logger.debug('Received update for unknown agent entity:', entity.id); - return; - } - - - - logger.debug(`Entity ${entity.id} updated by another user`); - } - - - private handleVircadiaEntityDeleted(entityId: string): void { - - const agentId = Array.from(this.agentEntityMap.entries()) - .find(([_, eid]) => eid === entityId)?.[0]; - - if (agentId) { - this.agentEntityMap.delete(agentId); - this.lastSyncedAgents.delete(agentId); - logger.debug(`Agent entity ${entityId} deleted`); - } - } - - - startAutoSync( - getAgentsCallback: () => { agents: BotsAgent[]; edges: BotsEdge[] } - ): void { - if (this.syncInterval) { - this.stopAutoSync(); - } - - this.syncInterval = setInterval(() => { - const { agents, edges } = getAgentsCallback(); - this.syncAgentsToVircadia(agents, edges); - }, this.defaultConfig.updateInterval); - - logger.info(`Auto-sync started with ${this.defaultConfig.updateInterval}ms interval`); - } - - - stopAutoSync(): void { - if (this.syncInterval) { - clearInterval(this.syncInterval); - this.syncInterval = null; - logger.info('Auto-sync stopped'); - } - } - - - dispose(): void { - this.stopAutoSync(); - if (this.entityUpdateUnsubscribe) { - this.entityUpdateUnsubscribe(); - this.entityUpdateUnsubscribe = null; - } - this.isActive = false; - this.agentEntityMap.clear(); - this.lastSyncedAgents.clear(); - logger.info('BotsVircadiaBridge disposed'); - } -} diff --git a/client/src/services/bridges/GraphVircadiaBridge.ts b/client/src/services/bridges/GraphVircadiaBridge.ts deleted file mode 100644 index bea199390..000000000 --- a/client/src/services/bridges/GraphVircadiaBridge.ts +++ /dev/null @@ -1,355 +0,0 @@ -// Three.js migration verified: all scene, mesh, vector, and material APIs use THREE.* - -import * as THREE from 'three'; -import { ClientCore } from '../vircadia/VircadiaClientCore'; -import { CollaborativeGraphSync, type FilterState } from '../vircadia/CollaborativeGraphSync'; -import { EntitySyncManager } from '../vircadia/EntitySyncManager'; -import { GraphEntityMapper } from '../vircadia/GraphEntityMapper'; -import { createLogger } from '../../utils/loggerConfig'; - -const logger = createLogger('GraphVircadiaBridge'); - -export interface GraphNode { - id: string; - label: string; - position: { x: number; y: number; z: number }; - type?: string; - metadata?: Record; -} - -export interface GraphEdge { - source: string; - target: string; - type?: string; -} - -export interface UserSelectionEvent { - userId: string; - username: string; - nodeIds: string[]; -} - -export interface AnnotationEvent { - id: string; - userId: string; - username: string; - nodeId: string; - text: string; - position: { x: number; y: number; z: number }; -} - -export class GraphVircadiaBridge { - private nodeEntityMap = new Map(); - private nodePositionMap = new Map(); - private localSelectionCallback?: (nodeIds: string[]) => void; - private remoteSelectionCallback?: (event: UserSelectionEvent) => void; - private annotationCallback?: (event: AnnotationEvent) => void; - private isActive = false; - private entitySync: EntitySyncManager | null = null; - private mapper: GraphEntityMapper; - - constructor( - private scene: THREE.Scene, - private client: ClientCore, - private collab: CollaborativeGraphSync - ) { - this.mapper = new GraphEntityMapper({ - syncGroup: 'public.NORMAL', - loadPriority: 0, - createdBy: 'visionclaw-bridge' - }); - } - - - async initialize(): Promise { - logger.info('Initializing GraphVircadiaBridge...'); - - if (!this.client.Utilities.Connection.getConnectionInfo().isConnected) { - throw new Error('Vircadia client must be connected before initializing bridge'); - } - - await this.collab.initialize(); - - // Initialize EntitySyncManager for pushing graph entities to Vircadia - this.entitySync = new EntitySyncManager(this.client, { - syncGroup: 'public.NORMAL', - batchSize: 100, - syncIntervalMs: 100, - enableRealTimePositions: true, - }); - - // CollaborativeGraphSync has no EventEmitter interface. - // Remote selection / annotation / filter events are received via - // the binary WebSocket protocol and processed internally by - // CollaborativeGraphSync. The bridge reads state via polling - // (getActiveSelections, getAnnotations) instead. - - this.isActive = true; - logger.info('GraphVircadiaBridge initialized successfully'); - } - - - syncGraphToVircadia(nodes: GraphNode[], edges: GraphEdge[]): void { - if (!this.isActive) return; - - try { - - nodes.forEach(node => { - this.syncNodeToEntity(node); - }); - - - edges.forEach(edge => { - this.syncEdgeToEntity(edge); - }); - - logger.debug(`Synced ${nodes.length} nodes and ${edges.length} edges to Vircadia`); - } catch (error) { - logger.error('Failed to sync graph to Vircadia:', error); - } - } - - - private syncNodeToEntity(node: GraphNode): void { - const entityName = `node_${node.id}`; - this.nodeEntityMap.set(node.id, entityName); - this.nodePositionMap.set(node.id, node.position); - - if (!this.entitySync) { - logger.warn(`syncNodeToEntity(${entityName}): EntitySyncManager not initialized`); - return; - } - - // Map bridge GraphNode to mapper GraphNode format and create entity - const mapperNode = { - id: node.id, - label: node.label, - type: node.type, - x: node.position.x, - y: node.position.y, - z: node.position.z, - metadata: node.metadata, - }; - const entity = this.mapper.mapNodeToEntity(mapperNode); - - // Use EntitySyncManager's position update for real-time sync - this.entitySync.updateNodePosition(node.id, node.position); - - logger.debug(`Synced node ${node.id} to entity ${entityName}`); - } - - private syncEdgeToEntity(edge: GraphEdge): void { - const sourceEntityName = this.nodeEntityMap.get(edge.source); - const targetEntityName = this.nodeEntityMap.get(edge.target); - - if (!sourceEntityName || !targetEntityName) { - logger.debug(`syncEdgeToEntity(${edge.source}->${edge.target}): missing node entities, skipping`); - return; - } - - if (!this.entitySync) { - logger.warn(`syncEdgeToEntity(${edge.source}->${edge.target}): EntitySyncManager not initialized`); - return; - } - - // Map to mapper format using tracked node positions - const mapperEdge = { - id: `${edge.source}_${edge.target}`, - source: edge.source, - target: edge.target, - label: edge.type, - }; - this.mapper.mapEdgeToEntity(mapperEdge, this.nodePositionMap); - - logger.debug(`Synced edge ${edge.source}->${edge.target} to Vircadia`); - } - - - broadcastLocalSelection(nodeIds: string[]): void { - if (!this.isActive) return; - - try { - this.collab.selectNodes(nodeIds); - logger.debug(`Broadcasted selection of ${nodeIds.length} nodes`); - } catch (error) { - logger.error('Failed to broadcast selection:', error); - } - } - - - async addAnnotation( - nodeId: string, - text: string, - position: { x: number; y: number; z: number } - ): Promise { - if (!this.isActive) { - throw new Error('Bridge not active'); - } - - try { - const threePosition = new THREE.Vector3(position.x, position.y, position.z); - await this.collab.createAnnotation(nodeId, text, threePosition); - // createAnnotation generates the ID internally; retrieve the latest for this node - const nodeAnnotations = this.collab.getNodeAnnotations(nodeId); - const annotationId = nodeAnnotations.length > 0 - ? nodeAnnotations[nodeAnnotations.length - 1].id - : ''; - - logger.info(`Added annotation ${annotationId} to node ${nodeId}`); - return annotationId; - } catch (error) { - logger.error('Failed to add annotation:', error); - throw error; - } - } - - - async removeAnnotation(annotationId: string): Promise { - if (!this.isActive) return; - - try { - await this.collab.deleteAnnotation(annotationId); - logger.info(`Removed annotation ${annotationId}`); - } catch (error) { - logger.error('Failed to remove annotation:', error); - } - } - - - broadcastFilterState(filterState: { - searchQuery?: string; - categoryFilter?: string[]; - timeRange?: { start: number; end: number }; - customFilters?: Record; - }): void { - if (!this.isActive) return; - - try { - const collabFilterState: FilterState = { - searchQuery: filterState.searchQuery, - categoryFilter: filterState.categoryFilter, - timeRange: filterState.timeRange, - customFilters: filterState.customFilters, - }; - this.collab.updateFilterState(collabFilterState); - logger.debug('Broadcasted filter state'); - } catch (error) { - logger.error('Failed to broadcast filter state:', error); - } - } - - - private handleRemoteSelection(event: { - agentId: string; - username: string; - nodeIds: string[]; - }): void { - logger.debug(`Remote user ${event.username} selected ${event.nodeIds.length} nodes`); - - if (this.remoteSelectionCallback) { - this.remoteSelectionCallback({ - userId: event.agentId, - username: event.username, - nodeIds: event.nodeIds - }); - } - } - - - private handleRemoteAnnotation(annotation: { - id: string; - agentId: string; - username: string; - nodeId: string; - text: string; - position: { x: number; y: number; z: number }; - }): void { - logger.info(`Remote annotation added by ${annotation.username} on node ${annotation.nodeId}`); - - if (this.annotationCallback) { - this.annotationCallback({ - id: annotation.id, - userId: annotation.agentId, - username: annotation.username, - nodeId: annotation.nodeId, - text: annotation.text, - position: annotation.position - }); - } - } - - - private handleAnnotationRemoved(annotationId: string): void { - logger.debug(`Annotation ${annotationId} removed`); - } - - - private handleFilterStateChanged(event: { - agentId: string; - username: string; - filterState: any; - }): void { - logger.debug(`Remote user ${event.username} changed filter state`); - - } - - - onLocalSelection(callback: (nodeIds: string[]) => void): void { - this.localSelectionCallback = callback; - } - - - onRemoteSelection(callback: (event: UserSelectionEvent) => void): void { - this.remoteSelectionCallback = callback; - } - - - onAnnotation(callback: (event: AnnotationEvent) => void): void { - this.annotationCallback = callback; - } - - - getActiveUsers(): Array<{ - userId: string; - username: string; - selectedNodes: string[]; - }> { - if (!this.isActive) return []; - - return this.collab.getActiveSelections().map(selection => ({ - userId: selection.agentId, - username: selection.username, - selectedNodes: selection.nodeIds - })); - } - - - getAnnotations(): AnnotationEvent[] { - if (!this.isActive) return []; - - return this.collab.getAnnotations().map(ann => ({ - id: ann.id, - userId: ann.agentId, - username: ann.username, - nodeId: ann.nodeId, - text: ann.text, - position: ann.position - })); - } - - - dispose(): void { - this.isActive = false; - this.nodeEntityMap.clear(); - this.nodePositionMap.clear(); - this.localSelectionCallback = undefined; - this.remoteSelectionCallback = undefined; - this.annotationCallback = undefined; - if (this.entitySync) { - this.entitySync.dispose(); - this.entitySync = null; - } - this.collab.dispose(); - logger.info('GraphVircadiaBridge disposed'); - } -} diff --git a/client/src/services/quest3AutoDetector.ts b/client/src/services/quest3AutoDetector.ts index a06943c84..7fb8d5b0f 100644 --- a/client/src/services/quest3AutoDetector.ts +++ b/client/src/services/quest3AutoDetector.ts @@ -1,7 +1,6 @@ import { createLogger } from '../utils/loggerConfig'; import { usePlatformStore } from './platformManager'; import { useSettingsStore } from '../store/settingsStore'; -import { ClientCore } from './vircadia/VircadiaClientCore'; const logger = createLogger('Quest3AutoDetector'); @@ -16,7 +15,6 @@ export class Quest3AutoDetector { private static instance: Quest3AutoDetector; private detectionResult: Quest3DetectionResult | null = null; private autoStartAttempted: boolean = false; - private vircadiaClient: ClientCore | null = null; private constructor() {} @@ -144,7 +142,6 @@ export class Quest3AutoDetector { usePlatformStore.getState().setXRSessionState('active'); - await this.initializeVircadiaConnection(); return true; @@ -224,56 +221,9 @@ export class Quest3AutoDetector { } - private async initializeVircadiaConnection(): Promise { - try { - logger.info('Initializing Vircadia connection for Quest 3 XR...'); - - - this.vircadiaClient = new ClientCore({ - serverUrl: import.meta.env.VITE_VIRCADIA_SERVER_URL || 'ws://localhost:3020/world/ws', - authToken: import.meta.env.VITE_VIRCADIA_AUTH_TOKEN || 'system-token', - authProvider: import.meta.env.VITE_VIRCADIA_AUTH_PROVIDER || 'system', - reconnectAttempts: 5, - reconnectDelay: 5000, - debug: import.meta.env.DEV || false, - suppress: false - }); - - - const connectionInfo = await this.vircadiaClient.Utilities.Connection.connect({ - timeoutMs: 10000 - }); - - logger.info('Vircadia connected for Quest 3 XR', { - agentId: connectionInfo.agentId, - sessionId: connectionInfo.sessionId - }); - - } catch (error) { - logger.error('Failed to initialize Vircadia connection:', error); - - } - } - - - public getVircadiaClient(): ClientCore | null { - return this.vircadiaClient; - } - - - public disconnectVircadia(): void { - if (this.vircadiaClient) { - logger.info('Disconnecting Vircadia client'); - this.vircadiaClient.dispose(); - this.vircadiaClient = null; - } - } - - public resetDetection(): void { this.detectionResult = null; this.autoStartAttempted = false; - this.disconnectVircadia(); } } diff --git a/client/src/services/vircadia/CollaborativeGraphSync.ts b/client/src/services/vircadia/CollaborativeGraphSync.ts deleted file mode 100644 index a48410fbb..000000000 --- a/client/src/services/vircadia/CollaborativeGraphSync.ts +++ /dev/null @@ -1,724 +0,0 @@ -// Three.js migration verified: CanvasTexture for dynamic text, onBeforeRender for -// billboard/rotation animation, standard Object3D parent/child hierarchy. - -import * as THREE from 'three'; -import { ClientCore } from './VircadiaClientCore'; -import { EntitySyncManager } from './EntitySyncManager'; -import { GraphEntityMapper, VircadiaEntity } from './GraphEntityMapper'; -import { createLogger } from '../../utils/loggerConfig'; -import { BinaryWebSocketProtocol, MessageType, AgentPositionUpdate } from '../BinaryWebSocketProtocol'; - -const logger = createLogger('CollaborativeGraphSync'); - -export interface CollaborativeConfig { - highlightColor: THREE.Color; - annotationColor: THREE.Color; - selectionTimeout: number; - enableAnnotations: boolean; - enableFiltering: boolean; - enableVRPresence: boolean; -} - -export interface UserSelection { - agentId: string; - username: string; - nodeIds: string[]; - timestamp: number; - filterState?: FilterState; -} - -export interface FilterState { - searchQuery?: string; - categoryFilter?: string[]; - timeRange?: { start: number; end: number }; - customFilters?: Record; -} - -export interface GraphAnnotation { - id: string; - agentId: string; - username: string; - nodeId: string; - text: string; - position: { x: number; y: number; z: number }; - timestamp: number; -} - -export interface UserPresence { - userId: string; - username: string; - position: THREE.Vector3; - rotation: THREE.Quaternion; - headPosition?: THREE.Vector3; - headRotation?: THREE.Quaternion; - leftHandPosition?: THREE.Vector3; - leftHandRotation?: THREE.Quaternion; - rightHandPosition?: THREE.Vector3; - rightHandRotation?: THREE.Quaternion; - lastUpdate: number; -} - -export interface GraphOperation { - id: string; - type: 'node_move' | 'node_add' | 'node_delete' | 'edge_add' | 'edge_delete'; - userId: string; - nodeId?: string; - position?: { x: number; y: number; z: number }; - timestamp: number; - version: number; -} - -export class CollaborativeGraphSync { - private localAgentId: string | null = null; - private activeSelections = new Map(); - private annotations = new Map(); - private userPresence = new Map(); - private selectionHighlights = new Map(); - private annotationMeshes = new Map(); - private presenceMeshes = new Map(); - private localSelection: string[] = []; - private localFilterState: FilterState | null = null; - private operationVersion = 0; - - /** EntitySyncManager for bi-directional Vircadia sync */ - private entitySync: EntitySyncManager | null = null; - private entityUpdateUnsubscribe: (() => void) | null = null; - - private defaultConfig: CollaborativeConfig = { - highlightColor: new THREE.Color(0.2, 0.8, 0.3), - annotationColor: new THREE.Color(1.0, 0.8, 0.2), - selectionTimeout: 30000, - enableAnnotations: true, - enableFiltering: true, - enableVRPresence: true - }; - - private protocol = BinaryWebSocketProtocol.getInstance(); - private textDecoder = new TextDecoder(); - - constructor( - private scene: THREE.Scene, - private client: ClientCore, - config?: Partial - ) { - this.defaultConfig = { ...this.defaultConfig, ...config }; - this.setupConnectionListeners(); - this.initEntitySync(); - } - - async initialize(): Promise { - logger.info('Initializing collaborative graph sync...'); - - const info = this.client.Utilities.Connection.getConnectionInfo(); - if (info.agentId) { - this.localAgentId = info.agentId; - } - - await this.loadAnnotations(); - - // Register bi-directional entity update listener - if (this.entitySync) { - this.entityUpdateUnsubscribe = this.entitySync.onEntityUpdate((entities) => { - this.handleIncomingEntityUpdates(entities); - }); - logger.info('Bi-directional Vircadia entity sync registered'); - } - - logger.info('Collaborative sync initialized'); - } - - /** - * Initialize the EntitySyncManager for bi-directional Vircadia sync. - * Positions flow: server → binary protocol → client AND client → EntitySync → Vircadia - */ - private initEntitySync(): void { - try { - this.entitySync = new EntitySyncManager(this.client, { - syncGroup: 'public.NORMAL', - batchSize: 100, - syncIntervalMs: 100, - enableRealTimePositions: true, - }); - logger.info('EntitySyncManager initialized for bi-directional sync'); - } catch (err) { - logger.warn('Failed to initialize EntitySyncManager:', err); - } - } - - /** - * Forward a node position update to the Vircadia entity sync layer. - * Called from applyOperation when node_move operations arrive. - */ - public syncNodePositionToVircadia(nodeId: string, position: { x: number; y: number; z: number }): void { - if (this.entitySync) { - this.entitySync.updateNodePosition(nodeId, position); - } - } - - /** - * Handle incoming entity updates from Vircadia (server → client direction). - * Reconciles remote entity positions into the local scene graph. - */ - private handleIncomingEntityUpdates(entities: VircadiaEntity[]): void { - for (const entity of entities) { - const metadata = GraphEntityMapper.extractMetadata(entity); - if (!metadata) continue; - - if (metadata.entityType === 'node' && metadata.position) { - const nodeMesh = this.scene.getObjectByName(`node_${metadata.graphId}`); - if (nodeMesh) { - // Only apply if the position differs significantly (avoid jitter) - const dx = nodeMesh.position.x - metadata.position.x; - const dy = nodeMesh.position.y - metadata.position.y; - const dz = nodeMesh.position.z - metadata.position.z; - const distSq = dx * dx + dy * dy + dz * dz; - - if (distSq > 0.01) { // 0.1 unit threshold - nodeMesh.position.set( - metadata.position.x, - metadata.position.y, - metadata.position.z - ); - logger.debug(`[BiSync] Reconciled node ${metadata.graphId} from Vircadia entity`); - } - } - } - } - } - - /** - * Get the EntitySyncManager for external access (e.g. pushing full graph). - */ - public getEntitySync(): EntitySyncManager | null { - return this.entitySync; - } - - // Arrow function class properties for stable references (Fix 3 - bind leak) - private handleSyncUpdateEvent = async (): Promise => { - // Sync update handler - placeholder for event-driven sync processing - logger.debug('Sync update event received'); - }; - - private handleStatusChangeEvent = (): void => { - const info = this.client.Utilities.Connection.getConnectionInfo(); - if (info.isConnected && info.agentId) { - this.localAgentId = info.agentId; - } - }; - - private handleSyncUpdate(payload: ArrayBuffer): void { - const view = new DataView(payload); - let offset = 0; - - while (offset < payload.byteLength) { - const opType = view.getUint8(offset); - offset += 1; - - const userId = this.textDecoder.decode(payload.slice(offset, offset + 36)); - offset += 36; - - const nodeIdLength = view.getUint16(offset, true); - offset += 2; - - const nodeId = this.textDecoder.decode(payload.slice(offset, offset + nodeIdLength)); - offset += nodeIdLength; - - const operation: GraphOperation = { - id: `${userId}_${Date.now()}`, - type: this.getOperationType(opType), - userId, - nodeId, - timestamp: Date.now(), - version: this.operationVersion++ - }; - - if (opType === 0) { // node_move - operation.position = { - x: view.getFloat32(offset, true), - y: view.getFloat32(offset + 4, true), - z: view.getFloat32(offset + 8, true) - }; - offset += 12; - } - - this.applyOperation(operation); - } - } - - private handleAnnotationUpdate(payload: ArrayBuffer): void { - const text = this.textDecoder.decode(payload); - try { - const annotation: GraphAnnotation = JSON.parse(text); - this.annotations.set(annotation.id, annotation); - this.createAnnotationMesh(annotation); - } catch (error) { - logger.error('Failed to parse annotation:', error); - } - } - - private handleSelectionUpdate(payload: ArrayBuffer): void { - const text = this.textDecoder.decode(payload); - try { - const selection: UserSelection = JSON.parse(text); - this.activeSelections.set(selection.agentId, selection); - this.updateSelectionHighlight(selection); - } catch (error) { - logger.error('Failed to parse selection:', error); - } - } - - private getOperationType(opType: number): GraphOperation['type'] { - switch (opType) { - case 0: return 'node_move'; - case 1: return 'node_add'; - case 2: return 'node_delete'; - case 3: return 'edge_add'; - case 4: return 'edge_delete'; - default: return 'node_move'; - } - } - - private applyOperation(operation: GraphOperation): void { - // Server-authoritative position flow: - // 1. Server computes positions via GPU physics - // 2. Server broadcasts via binary protocol to all clients (desktop + VR + Vircadia) - // 3. Each client applies optimistic tweening toward server targets - // 4. Collaborative operations (e.g. node_move from another user) are applied - // as visual updates; the authoritative position comes from the server's - // next physics broadcast. - if (operation.type === 'node_move' && operation.position) { - const nodeMesh = this.scene.getObjectByName(`node_${operation.nodeId}`); - if (nodeMesh) { - nodeMesh.position.set( - operation.position.x, - operation.position.y, - operation.position.z - ); - } - // Forward position to Vircadia entity sync for bi-directional mirroring - if (operation.nodeId) { - this.syncNodePositionToVircadia(operation.nodeId, operation.position); - } - // Note: The graph data manager receives authoritative positions from the - // server via binary WebSocket protocol. This collaborative operation is - // an optimistic preview that will be reconciled on the next server tick. - } - - logger.debug(`Applied operation: ${operation.type} on node ${operation.nodeId}`); - } - - private resolveConflict(local: GraphOperation, remote: GraphOperation): GraphOperation { - // Operational transform: last-write-wins with user priority - if (remote.timestamp > local.timestamp) { - logger.debug(`Conflict resolved: using remote operation (${remote.userId})`); - return remote; - } else if (remote.timestamp === local.timestamp) { - // Use lexicographic ordering on userId for determinism - if (remote.userId > local.userId) { - return remote; - } - } - - logger.debug(`Conflict resolved: using local operation (${local.userId})`); - return local; - } - - async selectNodes(nodeIds: string[]): Promise { - if (!this.localAgentId) { - logger.warn('Cannot broadcast selection: no agent ID'); - return; - } - - this.localSelection = nodeIds; - - const selection: UserSelection = { - agentId: this.localAgentId, - username: 'Local User', - nodeIds, - timestamp: Date.now(), - filterState: this.localFilterState ?? undefined - }; - - // Selection is tracked locally; binary broadcast removed since - // getWebSocket() was non-functional (Fix 2). Use JSON query path - // or event system when WebSocket access is available. - this.activeSelections.set(this.localAgentId, selection); - this.updateSelectionHighlight(selection); - - logger.debug(`Selection broadcast: ${nodeIds.length} nodes`); - } - - async updateFilterState(filterState: FilterState): Promise { - if (!this.defaultConfig.enableFiltering) { - return; - } - - this.localFilterState = filterState; - await this.selectNodes(this.localSelection); - - logger.debug('Filter state broadcast:', filterState); - } - - async createAnnotation(nodeId: string, text: string, position: THREE.Vector3): Promise { - if (!this.defaultConfig.enableAnnotations || !this.localAgentId) { - return; - } - - const annotation: GraphAnnotation = { - id: `annotation_${this.localAgentId}_${Date.now()}`, - agentId: this.localAgentId, - username: 'Local User', - nodeId, - text, - position: { x: position.x, y: position.y, z: position.z }, - timestamp: Date.now() - }; - - this.createAnnotationMesh(annotation); - this.annotations.set(annotation.id, annotation); - - logger.info(`Annotation created: "${text}" on node ${nodeId}`); - } - - async loadAnnotations(): Promise { - if (!this.defaultConfig.enableAnnotations) { - return; - } - - logger.info('Annotations loading initiated'); - } - - private createAnnotationMesh(annotation: GraphAnnotation): void { - // Create canvas for text texture - const canvas = document.createElement('canvas'); - canvas.width = 512; - canvas.height = 128; - const ctx = canvas.getContext('2d')!; - - ctx.fillStyle = 'rgba(20, 20, 30, 0.85)'; - ctx.fillRect(0, 0, 512, 128); - - ctx.fillStyle = `#${this.defaultConfig.annotationColor.getHexString()}`; - ctx.font = 'bold 32px Arial'; - ctx.textAlign = 'center'; - ctx.fillText(annotation.text, 256, 50); - - ctx.fillStyle = 'rgba(255, 255, 255, 0.7)'; - ctx.font = '20px Arial'; - ctx.fillText(`- ${annotation.username}`, 256, 90); - - const texture = new THREE.CanvasTexture(canvas); - texture.needsUpdate = true; - - const geometry = new THREE.PlaneGeometry(0.5, 0.2); - const material = new THREE.MeshBasicMaterial({ - map: texture, - transparent: true, - side: THREE.DoubleSide - }); - - const plane = new THREE.Mesh(geometry, material); - plane.name = `${annotation.id}_mesh`; - plane.position.set( - annotation.position.x, - annotation.position.y, - annotation.position.z - ); - - // Billboard: face the camera every frame via onBeforeRender - plane.onBeforeRender = (_renderer, _scene, camera) => { - plane.quaternion.copy(camera.quaternion); - }; - - this.scene.add(plane); - - // Dispose old mesh/texture if replacing (Fix 4 - texture leak) - const existingMesh = this.annotationMeshes.get(annotation.id); - if (existingMesh) { - this.scene.remove(existingMesh); - this.disposeAnnotationMesh(existingMesh); - } - - this.annotationMeshes.set(annotation.id, plane); - - logger.debug(`Annotation mesh created: "${annotation.text}"`); - } - - private updateSelectionHighlight(selection: UserSelection): void { - const existingHighlights = this.selectionHighlights.get(selection.agentId); - if (existingHighlights) { - existingHighlights.forEach(mesh => { - this.scene.remove(mesh); - mesh.geometry.dispose(); - (mesh.material as THREE.Material).dispose(); - }); - } - - const highlights: THREE.Mesh[] = []; - - selection.nodeIds.forEach(nodeId => { - const nodeMesh = this.scene.getObjectByName(`node_${nodeId}`); - if (!nodeMesh) { - return; - } - - // Calculate bounding sphere - const box = new THREE.Box3().setFromObject(nodeMesh); - const sphere = new THREE.Sphere(); - box.getBoundingSphere(sphere); - - const geometry = new THREE.TorusGeometry( - sphere.radius * 1.25, - 0.02, - 16, - 32 - ); - - const hue = parseInt(selection.agentId.substring(0, 8), 16) % 360; - const color = new THREE.Color().setHSL(hue / 360, 0.8, 0.9); - - const material = new THREE.MeshBasicMaterial({ - color: color, - transparent: true, - opacity: 0.6 - }); - - const highlight = new THREE.Mesh(geometry, material); - highlight.name = `highlight_${selection.agentId}_${nodeId}`; - highlight.position.copy(nodeMesh.position); - highlight.position.y += sphere.radius; - - // Animate rotation each frame using onBeforeRender - const rotationSpeed = 0.02; - highlight.onBeforeRender = () => { - highlight.rotation.y += rotationSpeed; - }; - - this.scene.add(highlight); - highlights.push(highlight); - }); - - this.selectionHighlights.set(selection.agentId, highlights); - - logger.debug(`Updated highlight for ${selection.username}: ${selection.nodeIds.length} nodes`); - } - - private updatePresenceMesh(presence: UserPresence): void { - let mesh = this.presenceMeshes.get(presence.userId); - - if (!mesh) { - // Create user cursor/avatar - const geometry = new THREE.SphereGeometry(0.1, 16, 16); - - const hue = parseInt(presence.userId.substring(0, 8), 16) % 360; - const color = new THREE.Color().setHSL(hue / 360, 0.7, 0.8); - - const material = new THREE.MeshBasicMaterial({ - color: color, - transparent: true, - opacity: 0.8 - }); - - mesh = new THREE.Mesh(geometry, material); - mesh!.name = `presence_${presence.userId}`; - - // Add nameplate - const canvas = document.createElement('canvas'); - canvas.width = 256; - canvas.height = 64; - const ctx = canvas.getContext('2d')!; - - ctx.fillStyle = 'rgba(0,0,0,0.7)'; - ctx.fillRect(0, 0, 256, 64); - ctx.fillStyle = 'white'; - ctx.font = 'bold 24px Arial'; - ctx.textAlign = 'center'; - ctx.fillText(presence.username, 128, 40); - - const texture = new THREE.CanvasTexture(canvas); - const nameplateGeometry = new THREE.PlaneGeometry(0.5, 0.1); - const nameplateMaterial = new THREE.MeshBasicMaterial({ - map: texture, - transparent: true, - side: THREE.DoubleSide - }); - - const nameplate = new THREE.Mesh(nameplateGeometry, nameplateMaterial); - nameplate.name = `nameplate_${presence.userId}`; - nameplate.position.y = 0.2; - - // Billboard: face the camera every frame via onBeforeRender - nameplate.onBeforeRender = (_renderer, _scene, camera) => { - nameplate.quaternion.copy(camera.quaternion); - }; - - mesh!.add(nameplate); - this.scene.add(mesh!); - this.presenceMeshes.set(presence.userId, mesh!); - } - - // Update position - mesh!.position.copy(presence.position); - - // Update VR hands if available - if (this.defaultConfig.enableVRPresence) { - this.updateVRHandPresence(presence); - } - } - - private updateVRHandPresence(presence: UserPresence): void { - if (!presence.leftHandPosition && !presence.rightHandPosition) { - return; - } - - // Left hand - if (presence.leftHandPosition) { - let leftHand = this.scene.getObjectByName(`lefthand_${presence.userId}`) as THREE.Mesh; - if (!leftHand) { - const geometry = new THREE.SphereGeometry(0.025, 8, 8); - const material = new THREE.MeshBasicMaterial({ color: 0xff0000 }); - leftHand = new THREE.Mesh(geometry, material); - leftHand.name = `lefthand_${presence.userId}`; - this.scene.add(leftHand); - } - leftHand.position.copy(presence.leftHandPosition); - if (presence.leftHandRotation) { - leftHand.quaternion.copy(presence.leftHandRotation); - } - } - - // Right hand - if (presence.rightHandPosition) { - let rightHand = this.scene.getObjectByName(`righthand_${presence.userId}`) as THREE.Mesh; - if (!rightHand) { - const geometry = new THREE.SphereGeometry(0.025, 8, 8); - const material = new THREE.MeshBasicMaterial({ color: 0x0000ff }); - rightHand = new THREE.Mesh(geometry, material); - rightHand.name = `righthand_${presence.userId}`; - this.scene.add(rightHand); - } - rightHand.position.copy(presence.rightHandPosition); - if (presence.rightHandRotation) { - rightHand.quaternion.copy(presence.rightHandRotation); - } - } - } - - private setupConnectionListeners(): void { - // Use stable arrow function references for proper removal in dispose() (Fix 3) - this.client.Utilities.Connection.addEventListener('syncUpdate', this.handleSyncUpdateEvent); - this.client.Utilities.Connection.addEventListener('statusChange', this.handleStatusChangeEvent); - } - - async deleteAnnotation(annotationId: string): Promise { - const annotation = this.annotations.get(annotationId); - if (!annotation || annotation.agentId !== this.localAgentId) { - logger.warn('Cannot delete annotation from another user'); - return; - } - - const mesh = this.annotationMeshes.get(annotationId); - if (mesh) { - this.scene.remove(mesh); - this.disposeAnnotationMesh(mesh); // Fix 4 - dispose texture - this.annotationMeshes.delete(annotationId); - } - - this.annotations.delete(annotationId); - - logger.info(`Annotation deleted: ${annotationId}`); - } - - /** Dispose an annotation mesh including its CanvasTexture (Fix 4) */ - private disposeAnnotationMesh(mesh: THREE.Mesh): void { - mesh.geometry.dispose(); - const material = mesh.material as THREE.MeshBasicMaterial; - if (material.map) { - material.map.dispose(); - } - material.dispose(); - } - - /** Dispose a presence mesh including its nameplate CanvasTexture (Fix 4) */ - private disposePresenceMesh(mesh: THREE.Object3D): void { - if (mesh instanceof THREE.Mesh) { - mesh.geometry.dispose(); - (mesh.material as THREE.Material).dispose(); - } - // Dispose child nameplate textures - mesh.children.forEach(child => { - if (child instanceof THREE.Mesh) { - child.geometry.dispose(); - const mat = child.material as THREE.MeshBasicMaterial; - if (mat.map) { - mat.map.dispose(); - } - mat.dispose(); - } - }); - } - - getActiveSelections(): UserSelection[] { - return Array.from(this.activeSelections.values()); - } - - getAnnotations(): GraphAnnotation[] { - return Array.from(this.annotations.values()); - } - - getNodeAnnotations(nodeId: string): GraphAnnotation[] { - return Array.from(this.annotations.values()).filter(a => a.nodeId === nodeId); - } - - getUserPresence(): UserPresence[] { - return Array.from(this.userPresence.values()); - } - - dispose(): void { - logger.info('Disposing CollaborativeGraphSync'); - - // Unsubscribe entity update listener - if (this.entityUpdateUnsubscribe) { - this.entityUpdateUnsubscribe(); - this.entityUpdateUnsubscribe = null; - } - - // Dispose entity sync manager - if (this.entitySync) { - this.entitySync.dispose(); - this.entitySync = null; - } - - // Remove event listeners using stable references (Fix 3) - this.client.Utilities.Connection.removeEventListener('syncUpdate', this.handleSyncUpdateEvent); - this.client.Utilities.Connection.removeEventListener('statusChange', this.handleStatusChangeEvent); - - this.selectionHighlights.forEach(highlights => { - highlights.forEach(mesh => { - this.scene.remove(mesh); - mesh.geometry.dispose(); - (mesh.material as THREE.Material).dispose(); - }); - }); - this.selectionHighlights.clear(); - - // Dispose annotation meshes including textures (Fix 4) - this.annotationMeshes.forEach(mesh => { - this.scene.remove(mesh); - this.disposeAnnotationMesh(mesh); - }); - this.annotationMeshes.clear(); - - // Dispose presence meshes including nameplate textures (Fix 4) - this.presenceMeshes.forEach(mesh => { - // THREE.Mesh extends Object3D; cast needed due to @types/three dual-path resolution - this.scene.remove(mesh as never); - this.disposePresenceMesh(mesh as never); - }); - this.presenceMeshes.clear(); - - this.activeSelections.clear(); - this.annotations.clear(); - this.userPresence.clear(); - } -} diff --git a/client/src/services/vircadia/EntitySyncManager.ts b/client/src/services/vircadia/EntitySyncManager.ts deleted file mode 100644 index a5c563dd3..000000000 --- a/client/src/services/vircadia/EntitySyncManager.ts +++ /dev/null @@ -1,327 +0,0 @@ - - -import { ClientCore, QueryResult } from './VircadiaClientCore'; -import { GraphEntityMapper, GraphData, GraphNode, GraphEdge, VircadiaEntity } from './GraphEntityMapper'; -import { createLogger } from '../../utils/loggerConfig'; - -const logger = createLogger('EntitySyncManager'); - -export interface SyncConfig { - syncGroup: string; - batchSize: number; - syncIntervalMs: number; - enableRealTimePositions: boolean; -} - -export interface SyncStats { - totalEntities: number; - syncedNodes: number; - syncedEdges: number; - lastSyncTime: number; - pendingUpdates: number; - errors: number; -} - -export class EntitySyncManager { - private mapper: GraphEntityMapper; - private syncInterval: ReturnType | null = null; - private pendingPositionUpdates = new Map(); - private stats: SyncStats = { - totalEntities: 0, - syncedNodes: 0, - syncedEdges: 0, - lastSyncTime: 0, - pendingUpdates: 0, - errors: 0 - }; - - private config: SyncConfig = { - syncGroup: 'public.NORMAL', - batchSize: 100, - syncIntervalMs: 100, - enableRealTimePositions: true - }; - - constructor( - private client: ClientCore, - config?: Partial - ) { - this.config = { ...this.config, ...config }; - this.mapper = new GraphEntityMapper({ - syncGroup: this.config.syncGroup, - loadPriority: 0, - createdBy: 'visionclaw-xr' - }); - - - this.setupEventListeners(); - } - - private setupEventListeners(): void { - - this.client.Utilities.Connection.addEventListener('syncUpdate', () => { - logger.debug('Received sync update from server'); - - }); - - - this.client.Utilities.Connection.addEventListener('statusChange', () => { - const info = this.client.Utilities.Connection.getConnectionInfo(); - if (info.isConnected) { - logger.info('Connected - starting sync manager'); - this.startRealTimeSync(); - } else { - logger.warn('Disconnected - stopping sync manager'); - this.stopRealTimeSync(); - } - }); - } - - - async pushGraphToVircadia(graphData: GraphData): Promise { - logger.info(`Pushing graph to Vircadia: ${graphData.nodes.length} nodes, ${graphData.edges.length} edges`); - - const entities = this.mapper.mapGraphToEntities(graphData); - this.stats.totalEntities = entities.length; - - - for (let i = 0; i < entities.length; i += this.config.batchSize) { - const batch = entities.slice(i, i + this.config.batchSize); - await this.insertEntitiesBatch(batch); - } - - this.stats.syncedNodes = graphData.nodes.length; - this.stats.syncedEdges = graphData.edges.length; - this.stats.lastSyncTime = Date.now(); - - logger.info('Graph push complete', this.stats); - } - - - async pullGraphFromVircadia(): Promise { - logger.info('Pulling graph from Vircadia'); - - try { - const query = ` - SELECT * FROM entity.entities - WHERE group__sync = $1 - AND (general__entity_name LIKE 'node_%' - OR general__entity_name LIKE 'edge_%') - ORDER BY group__load_priority ASC - `; - - const result = await this.client.Utilities.Connection.query<{ result: VircadiaEntity[] }>({ - query, - parameters: [this.config.syncGroup], - timeoutMs: 30000 - }); - - if (!result || !result.result) { - logger.warn('No entities found in Vircadia'); - return { nodes: [], edges: [] }; - } - - const entities = result.result as unknown as VircadiaEntity[]; - const graphData = GraphEntityMapper.entitiesToGraph(entities); - - logger.info(`Pulled graph from Vircadia: ${graphData.nodes.length} nodes, ${graphData.edges.length} edges`); - return graphData; - - } catch (error) { - logger.error('Failed to pull graph from Vircadia:', error); - this.stats.errors++; - throw error; - } - } - - - updateNodePosition(nodeId: string, position: { x: number; y: number; z: number }): void { - if (!this.config.enableRealTimePositions) { - return; - } - - const entityName = `node_${nodeId}`; - this.pendingPositionUpdates.set(entityName, position); - this.stats.pendingUpdates = this.pendingPositionUpdates.size; - } - - - private async flushPositionUpdates(): Promise { - if (this.pendingPositionUpdates.size === 0) { - return; - } - - const updates = Array.from(this.pendingPositionUpdates.entries()); - this.pendingPositionUpdates.clear(); - - try { - const parameterizedUpdates = updates.map(([entityName, position]) => - this.mapper.generatePositionUpdateSQL(entityName, position) - ); - - for (const { query, parameters } of parameterizedUpdates) { - await this.client.Utilities.Connection.query({ - query, - parameters, - timeoutMs: 5000 - }); - } - - logger.debug(`Flushed ${updates.length} position updates to Vircadia`); - this.stats.pendingUpdates = 0; - - } catch (error) { - logger.error('Failed to flush position updates:', error); - this.stats.errors++; - - updates.forEach(([entityName, position]) => { - this.pendingPositionUpdates.set(entityName, position); - }); - } - } - - - private async insertEntitiesBatch(entities: VircadiaEntity[]): Promise { - try { - const { queries } = this.mapper.generateBatchInsertSQL(entities); - - for (const { query, parameters } of queries) { - await this.client.Utilities.Connection.query({ - query, - parameters, - timeoutMs: 10000 - }); - } - - logger.debug(`Inserted batch of ${entities.length} entities`); - - } catch (error) { - logger.error('Failed to insert entity batch:', error); - this.stats.errors++; - throw error; - } - } - - - private startRealTimeSync(): void { - if (this.syncInterval) { - return; - } - - logger.info(`Starting real-time sync (interval: ${this.config.syncIntervalMs}ms)`); - - this.syncInterval = setInterval(() => { - this.flushPositionUpdates(); - }, this.config.syncIntervalMs); - } - - - private stopRealTimeSync(): void { - if (this.syncInterval) { - clearInterval(this.syncInterval); - this.syncInterval = null; - logger.info('Stopped real-time sync'); - } - } - - - onEntityUpdate(callback: (entities: VircadiaEntity[]) => void): () => void { - const handler = async () => { - try { - - const query = ` - SELECT * FROM entity.entities - WHERE group__sync = $1 - AND general__updated_at > NOW() - INTERVAL '1 second' - ORDER BY general__updated_at DESC - `; - - const result = await this.client.Utilities.Connection.query<{ result: VircadiaEntity[] }>({ - query, - parameters: [this.config.syncGroup], - timeoutMs: 5000 - }); - - if (result?.result) { - const entities = result.result as unknown as VircadiaEntity[]; - callback(entities); - } - - } catch (error) { - logger.error('Failed to query entity updates:', error); - } - }; - - this.client.Utilities.Connection.addEventListener('syncUpdate', handler); - - - return () => { - this.client.Utilities.Connection.removeEventListener('syncUpdate', handler); - }; - } - - - async deleteEntity(entityName: string): Promise { - try { - const query = ` - DELETE FROM entity.entities - WHERE general__entity_name = $1 - `; - - await this.client.Utilities.Connection.query({ - query, - parameters: [entityName], - timeoutMs: 5000 - }); - - logger.debug(`Deleted entity: ${entityName}`); - - } catch (error) { - logger.error(`Failed to delete entity ${entityName}:`, error); - this.stats.errors++; - throw error; - } - } - - - async clearGraph(): Promise { - logger.warn('Clearing all graph entities from Vircadia'); - - try { - const query = ` - DELETE FROM entity.entities - WHERE group__sync = $1 - AND (general__entity_name LIKE 'node_%' OR general__entity_name LIKE 'edge_%') - `; - - await this.client.Utilities.Connection.query({ - query, - parameters: [this.config.syncGroup], - timeoutMs: 10000 - }); - - this.stats.totalEntities = 0; - this.stats.syncedNodes = 0; - this.stats.syncedEdges = 0; - - logger.info('Graph cleared from Vircadia'); - - } catch (error) { - logger.error('Failed to clear graph:', error); - this.stats.errors++; - throw error; - } - } - - - getStats(): SyncStats { - return { ...this.stats }; - } - - - dispose(): void { - this.stopRealTimeSync(); - this.pendingPositionUpdates.clear(); - logger.info('EntitySyncManager disposed'); - } -} diff --git a/client/src/services/vircadia/GraphEntityMapper.ts b/client/src/services/vircadia/GraphEntityMapper.ts deleted file mode 100644 index a98823104..000000000 --- a/client/src/services/vircadia/GraphEntityMapper.ts +++ /dev/null @@ -1,349 +0,0 @@ - - -import { createLogger } from '../../utils/loggerConfig'; - -const logger = createLogger('GraphEntityMapper'); - -// VisionClaw graph types -export interface GraphNode { - id: string; - label: string; - type?: string; - color?: string; - size?: number; - x?: number; - y?: number; - z?: number; - metadata?: Record; -} - -export interface GraphEdge { - id: string; - source: string; - target: string; - label?: string; - color?: string; - weight?: number; - metadata?: Record; -} - -export interface GraphData { - nodes: GraphNode[]; - edges: GraphEdge[]; -} - -// Vircadia entity types (from Vircadia schema) -export interface VircadiaEntity { - general__entity_name: string; - general__semantic_version: string; - general__created_by?: string; - general__updated_by?: string; - group__sync: string; - group__load_priority: number; - meta__data?: Record; -} - -export interface VircadiaEntityMetadata { - entityType: 'node' | 'edge'; - graphId: string; - position?: { x: number; y: number; z: number }; - rotation?: { x: number; y: number; z: number }; - scale?: { x: number; y: number; z: number }; - color?: string; - label?: string; - visualProperties?: Record; - sourceId?: string; - targetId?: string; -} - -export interface EntitySyncOptions { - syncGroup: string; - loadPriority: number; - createdBy: string; -} - -export class GraphEntityMapper { - private defaultOptions: EntitySyncOptions = { - syncGroup: 'public.NORMAL', - loadPriority: 0, - createdBy: 'visionclaw' - }; - - constructor(private options: Partial = {}) { - this.defaultOptions = { ...this.defaultOptions, ...options }; - } - - - mapNodeToEntity(node: GraphNode): VircadiaEntity { - const entityName = `node_${node.id}`; - - - const position = { - x: node.x ?? 0, - y: node.y ?? 0, - z: node.z ?? 0 - }; - - - const scale = { - x: node.size ?? 0.1, - y: node.size ?? 0.1, - z: node.size ?? 0.1 - }; - - const metadata: VircadiaEntityMetadata = { - entityType: 'node', - graphId: node.id, - position, - rotation: { x: 0, y: 0, z: 0 }, - scale, - color: node.color ?? '#3b82f6', - label: node.label, - visualProperties: { - type: node.type, - originalMetadata: node.metadata - } - }; - - const entity: VircadiaEntity = { - general__entity_name: entityName, - general__semantic_version: '1.0.0', - general__created_by: this.defaultOptions.createdBy, - group__sync: this.defaultOptions.syncGroup, - group__load_priority: this.defaultOptions.loadPriority, - meta__data: metadata as unknown as Record - }; - - logger.debug(`Mapped node ${node.id} to entity ${entityName}`, entity); - return entity; - } - - - mapEdgeToEntity(edge: GraphEdge, nodePositions: Map): VircadiaEntity { - const entityName = `edge_${edge.id}`; - - const sourcePos = nodePositions.get(edge.source); - const targetPos = nodePositions.get(edge.target); - - if (!sourcePos || !targetPos) { - logger.warn(`Cannot map edge ${edge.id}: missing node positions`, { - source: edge.source, - target: edge.target, - hasSource: !!sourcePos, - hasTarget: !!targetPos - }); - } - - const metadata: VircadiaEntityMetadata = { - entityType: 'edge', - graphId: edge.id, - sourceId: edge.source, - targetId: edge.target, - color: edge.color ?? '#6b7280', - label: edge.label, - position: sourcePos || { x: 0, y: 0, z: 0 }, - visualProperties: { - weight: edge.weight, - targetPosition: targetPos || { x: 0, y: 0, z: 0 }, - originalMetadata: edge.metadata - } - }; - - const entity: VircadiaEntity = { - general__entity_name: entityName, - general__semantic_version: '1.0.0', - general__created_by: this.defaultOptions.createdBy, - group__sync: this.defaultOptions.syncGroup, - group__load_priority: this.defaultOptions.loadPriority + 1, - meta__data: metadata as unknown as Record - }; - - logger.debug(`Mapped edge ${edge.id} to entity ${entityName}`, entity); - return entity; - } - - - mapGraphToEntities(graphData: GraphData): VircadiaEntity[] { - logger.info(`Mapping graph with ${graphData.nodes.length} nodes and ${graphData.edges.length} edges`); - - const entities: VircadiaEntity[] = []; - - - const nodePositions = new Map(); - graphData.nodes.forEach(node => { - nodePositions.set(node.id, { - x: node.x ?? 0, - y: node.y ?? 0, - z: node.z ?? 0 - }); - }); - - - graphData.nodes.forEach(node => { - entities.push(this.mapNodeToEntity(node)); - }); - - - graphData.edges.forEach(edge => { - entities.push(this.mapEdgeToEntity(edge, nodePositions)); - }); - - logger.info(`Mapped ${entities.length} total entities`); - return entities; - } - - - generateEntityInsertSQL(entity: VircadiaEntity): { query: string; parameters: unknown[] } { - const columns = [ - 'general__entity_name', - 'general__semantic_version', - 'general__created_by', - 'group__sync', - 'group__load_priority', - 'meta__data' - ]; - - const query = ` -INSERT INTO entity.entities (${columns.join(', ')}) -VALUES ($1, $2, $3, $4, $5, $6::jsonb) -ON CONFLICT (general__entity_name) -DO UPDATE SET - meta__data = EXCLUDED.meta__data, - general__updated_at = CURRENT_TIMESTAMP; - `.trim(); - - const parameters: unknown[] = [ - entity.general__entity_name, - entity.general__semantic_version, - entity.general__created_by, - entity.group__sync, - entity.group__load_priority, - JSON.stringify(entity.meta__data) - ]; - - return { query, parameters }; - } - - - generateBatchInsertSQL(entities: VircadiaEntity[]): { queries: { query: string; parameters: unknown[] }[] } { - const queries = entities.map(entity => this.generateEntityInsertSQL(entity)); - return { queries }; - } - - - static extractMetadata(entity: VircadiaEntity): VircadiaEntityMetadata | null { - if (!entity.meta__data) { - return null; - } - return entity.meta__data as unknown as VircadiaEntityMetadata; - } - - - static entityToGraphNode(entity: VircadiaEntity): GraphNode | null { - const metadata = GraphEntityMapper.extractMetadata(entity); - if (!metadata || metadata.entityType !== 'node') { - return null; - } - - const node: GraphNode = { - id: metadata.graphId, - label: metadata.label || metadata.graphId, - type: (metadata.visualProperties?.type as string) || 'default', - color: metadata.color, - size: metadata.scale?.x, - x: metadata.position?.x, - y: metadata.position?.y, - z: metadata.position?.z, - metadata: metadata.visualProperties?.originalMetadata as Record - }; - - return node; - } - - - static entityToGraphEdge(entity: VircadiaEntity): GraphEdge | null { - const metadata = GraphEntityMapper.extractMetadata(entity); - if (!metadata || metadata.entityType !== 'edge') { - return null; - } - - const edge: GraphEdge = { - id: metadata.graphId, - source: metadata.sourceId || '', - target: metadata.targetId || '', - label: metadata.label, - color: metadata.color, - weight: metadata.visualProperties?.weight as number, - metadata: metadata.visualProperties?.originalMetadata as Record - }; - - return edge; - } - - - static entitiesToGraph(entities: VircadiaEntity[]): GraphData { - const nodes: GraphNode[] = []; - const edges: GraphEdge[] = []; - - entities.forEach(entity => { - const node = GraphEntityMapper.entityToGraphNode(entity); - if (node) { - nodes.push(node); - return; - } - - const edge = GraphEntityMapper.entityToGraphEdge(entity); - if (edge) { - edges.push(edge); - } - }); - - logger.info(`Converted ${entities.length} entities to ${nodes.length} nodes and ${edges.length} edges`); - - return { nodes, edges }; - } - - - updateEntityPosition( - entity: VircadiaEntity, - position: { x: number; y: number; z: number } - ): VircadiaEntity { - const metadata = GraphEntityMapper.extractMetadata(entity); - if (!metadata) { - logger.warn(`Cannot update position: entity has no metadata`, entity); - return entity; - } - - metadata.position = position; - - return { - ...entity, - meta__data: metadata as unknown as Record - }; - } - - - generatePositionUpdateSQL( - entityName: string, - position: { x: number; y: number; z: number } - ): { query: string; parameters: unknown[] } { - const query = ` -UPDATE entity.entities -SET meta__data = jsonb_set( - jsonb_set( - jsonb_set( - meta__data, - '{position,x}', to_jsonb($1::numeric) - ), - '{position,y}', to_jsonb($2::numeric) - ), - '{position,z}', to_jsonb($3::numeric) -) -WHERE general__entity_name = $4; - `.trim(); - - const parameters: unknown[] = [position.x, position.y, position.z, entityName]; - - return { query, parameters }; - } -} diff --git a/client/src/services/vircadia/ThreeJSAvatarRenderer.ts b/client/src/services/vircadia/ThreeJSAvatarRenderer.ts deleted file mode 100644 index cd4c214f1..000000000 --- a/client/src/services/vircadia/ThreeJSAvatarRenderer.ts +++ /dev/null @@ -1,429 +0,0 @@ -import * as THREE from 'three'; -import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'; -import { ClientCore } from './VircadiaClientCore'; -import { createLogger } from '../../utils/loggerConfig'; - -const logger = createLogger('ThreeJSAvatarRenderer'); - -export interface AvatarConfig { - modelUrl: string; - scale: number; - showNameplate: boolean; - nameplateDistance: number; - enableAnimations: boolean; -} - -export interface UserAvatar { - agentId: string; - username: string; - position: THREE.Vector3; - rotation: THREE.Quaternion; - mesh?: THREE.Object3D; - nameplate?: THREE.Sprite; - mixer?: THREE.AnimationMixer; - animations?: THREE.AnimationClip[]; -} - -export class ThreeJSAvatarRenderer { - private avatars = new Map(); - private localAgentId: string | null = null; - private updateInterval: ReturnType | null = null; - private gltfLoader = new GLTFLoader(); - - private defaultConfig: AvatarConfig = { - modelUrl: '/assets/avatars/default-avatar.glb', - scale: 1.0, - showNameplate: true, - nameplateDistance: 10.0, - enableAnimations: true - }; - - constructor( - private scene: THREE.Scene, - private client: ClientCore, - private camera: THREE.Camera, - config?: Partial - ) { - this.defaultConfig = { ...this.defaultConfig, ...config }; - this.setupConnectionListeners(); - } - - // Fix 2: Arrow function class properties for proper listener removal - private handleStatusChange = (): void => { - const info = this.client.Utilities.Connection.getConnectionInfo(); - if (info.isConnected && info.agentId) { - this.localAgentId = info.agentId; - logger.info(`Local agent ID: ${this.localAgentId}`); - } - }; - - private handleSyncUpdate = async (): Promise => { - await this.fetchRemoteAvatars(); - }; - - private setupConnectionListeners(): void { - this.client.Utilities.Connection.addEventListener('statusChange', this.handleStatusChange); - this.client.Utilities.Connection.addEventListener('syncUpdate', this.handleSyncUpdate); - } - - async createLocalAvatar(username: string): Promise { - if (!this.localAgentId) { - logger.warn('Cannot create local avatar: no agent ID'); - return; - } - - logger.info(`Creating local avatar for ${username}`); - - const avatar: UserAvatar = { - agentId: this.localAgentId, - username, - position: this.camera.position.clone(), - rotation: new THREE.Quaternion() - }; - - this.avatars.set(this.localAgentId, avatar); - this.startPositionBroadcast(); - await this.syncAvatarToVircadia(avatar); - } - - async loadRemoteAvatar(agentId: string, username: string): Promise { - if (this.avatars.has(agentId)) { - return; - } - - logger.info(`Loading remote avatar for ${username} (${agentId})`); - - try { - const gltf = await this.gltfLoader.loadAsync(this.defaultConfig.modelUrl); - const model = gltf.scene; - - model.name = `avatar_${agentId}`; - model.scale.setScalar(this.defaultConfig.scale); - this.scene.add(model); - - // Setup animations - let mixer: THREE.AnimationMixer | undefined; - if (this.defaultConfig.enableAnimations && gltf.animations.length > 0) { - mixer = new THREE.AnimationMixer(model); - const idleClip = gltf.animations.find(clip => - clip.name.toLowerCase().includes('idle') - ); - if (idleClip) { - mixer.clipAction(idleClip).play(); - } - } - - // Create nameplate - let nameplate: THREE.Sprite | undefined; - if (this.defaultConfig.showNameplate) { - nameplate = this.createNameplate(username); - nameplate.position.y = 2.2; - model.add(nameplate); - } - - // Fix 4: Proper type assertion instead of @ts-ignore - const avatar: UserAvatar = { - agentId, - username, - position: new THREE.Vector3(), - rotation: new THREE.Quaternion(), - mesh: model as unknown as THREE.Object3D, - nameplate, - mixer, - animations: gltf.animations - }; - - this.avatars.set(agentId, avatar); - logger.info(`Remote avatar loaded: ${username}`); - - } catch (error) { - logger.error(`Failed to load avatar for ${username}:`, error); - } - } - - private createNameplate(username: string): THREE.Sprite { - const canvas = document.createElement('canvas'); - canvas.width = 512; - canvas.height = 128; - - const ctx = canvas.getContext('2d')!; - ctx.fillStyle = 'rgba(0, 0, 0, 0.7)'; - ctx.fillRect(0, 0, 512, 128); - ctx.fillStyle = 'white'; - ctx.font = 'bold 56px Arial'; - ctx.textAlign = 'center'; - ctx.fillText(username, 256, 80); - - const texture = new THREE.CanvasTexture(canvas); - const material = new THREE.SpriteMaterial({ - map: texture, - transparent: true, - opacity: 0.9 - }); - - const sprite = new THREE.Sprite(material); - sprite.scale.set(2, 0.5, 1); - - return sprite; - } - - updateAvatarPosition(agentId: string, position: THREE.Vector3, rotation?: THREE.Quaternion): void { - const avatar = this.avatars.get(agentId); - if (!avatar) { - logger.warn(`Cannot update avatar: ${agentId} not found`); - return; - } - - avatar.position.copy(position); - if (rotation) { - avatar.rotation.copy(rotation); - } - - if (avatar.mesh) { - avatar.mesh.position.copy(position); - if (rotation) { - avatar.mesh.quaternion.copy(rotation); - } - } - - // Update nameplate visibility based on distance - if (avatar.nameplate && this.camera) { - const distance = this.camera.position.distanceTo(position); - avatar.nameplate.visible = distance <= this.defaultConfig.nameplateDistance; - } - } - - private startPositionBroadcast(): void { - if (this.updateInterval) return; - - logger.info('Starting avatar position broadcast'); - - this.updateInterval = setInterval(async () => { - if (!this.localAgentId || !this.camera) return; - - const localAvatar = this.avatars.get(this.localAgentId); - if (!localAvatar) return; - - localAvatar.position.copy(this.camera.position); - this.camera.getWorldQuaternion(localAvatar.rotation); - - await this.broadcastAvatarUpdate(localAvatar); - }, 100); - } - - // Fix 1: Parameterized SQL query - private async broadcastAvatarUpdate(avatar: UserAvatar): Promise { - try { - const query = ` - UPDATE entity.entities - SET meta__data = jsonb_set( - jsonb_set( - jsonb_set(meta__data, '{position}', $1::jsonb), - '{rotation}', $2::jsonb - ), - '{timestamp}', $3::text::jsonb - ) - WHERE general__entity_name = $4 - `; - - await this.client.Utilities.Connection.query({ - query, - parameters: [ - JSON.stringify({ x: avatar.position.x, y: avatar.position.y, z: avatar.position.z }), - JSON.stringify({ x: avatar.rotation.x, y: avatar.rotation.y, z: avatar.rotation.z, w: avatar.rotation.w }), - String(Date.now()), - `avatar_${avatar.agentId}` - ], - timeoutMs: 1000 - }); - } catch (error) { - logger.debug('Failed to broadcast avatar update:', error); - } - } - - // Fix 1: Parameterized SQL query - private async fetchRemoteAvatars(): Promise { - try { - const query = ` - SELECT * FROM entity.entities - WHERE general__entity_name LIKE $1 - AND general__entity_name != $2 - `; - - const result = await this.client.Utilities.Connection.query<{ result: any[] }>({ - query, - parameters: [ - 'avatar_%', - `avatar_${this.localAgentId}` - ], - timeoutMs: 5000 - }); - - if (!result?.result) return; - - interface AvatarEntity { - general__entity_name: string; - meta__data: { - username?: string; - position?: { x: number; y: number; z: number }; - rotation?: { x: number; y: number; z: number; w: number }; - }; - } - - const entities = (result as unknown as { result: AvatarEntity[] }).result; - for (const entity of entities) { - const agentId = entity.general__entity_name.replace('avatar_', ''); - const metadata = entity.meta__data; - - if (!metadata?.username) continue; - - if (!this.avatars.has(agentId)) { - await this.loadRemoteAvatar(agentId, metadata.username); - } - - if (metadata.position) { - const position = new THREE.Vector3( - metadata.position.x, - metadata.position.y, - metadata.position.z - ); - - let rotation: THREE.Quaternion | undefined; - if (metadata.rotation) { - rotation = new THREE.Quaternion( - metadata.rotation.x, - metadata.rotation.y, - metadata.rotation.z, - metadata.rotation.w - ); - } - - this.updateAvatarPosition(agentId, position, rotation); - } - } - } catch (error) { - logger.error('Failed to fetch remote avatars:', error); - } - } - - // Fix 1: Parameterized SQL query - private async syncAvatarToVircadia(avatar: UserAvatar): Promise { - try { - const query = ` - INSERT INTO entity.entities ( - general__entity_name, - general__semantic_version, - general__created_by, - group__sync, - group__load_priority, - meta__data - ) VALUES ( - $1, $2, $3, $4, $5, $6::jsonb - ) - ON CONFLICT (general__entity_name) - DO UPDATE SET meta__data = EXCLUDED.meta__data - `; - - await this.client.Utilities.Connection.query({ - query, - parameters: [ - `avatar_${avatar.agentId}`, - '1.0.0', - 'visionclaw-xr', - 'public.NORMAL', - 100, - JSON.stringify({ - entityType: 'avatar', - username: avatar.username, - position: { - x: avatar.position.x, - y: avatar.position.y, - z: avatar.position.z - }, - rotation: { - x: avatar.rotation.x, - y: avatar.rotation.y, - z: avatar.rotation.z, - w: avatar.rotation.w - } - }) - ], - timeoutMs: 5000 - }); - - logger.info(`Avatar synced to Vircadia: ${avatar.username}`); - } catch (error) { - logger.error('Failed to sync avatar to Vircadia:', error); - } - } - - update(deltaTime: number): void { - // Update animation mixers - this.avatars.forEach(avatar => { - if (avatar.mixer) { - avatar.mixer.update(deltaTime); - } - }); - } - - removeAvatar(agentId: string): void { - const avatar = this.avatars.get(agentId); - if (!avatar) return; - - logger.info(`Removing avatar: ${avatar.username}`); - - if (avatar.mesh) { - // Fix 4: Proper type assertion instead of @ts-ignore - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- @types/three dual-path resolution (build/ vs src/) creates incompatible Object3D types - this.scene.remove(avatar.mesh as unknown as THREE.Object3D); - avatar.mesh.traverse(child => { - if ((child as THREE.Mesh).geometry) { - (child as THREE.Mesh).geometry.dispose(); - } - if ((child as THREE.Mesh).material) { - const material = (child as THREE.Mesh).material; - if (Array.isArray(material)) { - material.forEach(m => m.dispose()); - } else { - material.dispose(); - } - } - }); - } - - // Fix 3: Dispose nameplate sprite textures to prevent texture leak - if (avatar.nameplate) { - avatar.nameplate.material.map?.dispose(); - avatar.nameplate.material.dispose(); - } - - this.avatars.delete(agentId); - } - - getAvatars(): UserAvatar[] { - return Array.from(this.avatars.values()); - } - - getAvatarCount(): number { - return this.avatars.size; - } - - dispose(): void { - logger.info('Disposing ThreeJSAvatarRenderer'); - - if (this.updateInterval) { - clearInterval(this.updateInterval); - this.updateInterval = null; - } - - // Fix 2: Remove event listeners to prevent leaks - this.client.Utilities.Connection.removeEventListener('statusChange', this.handleStatusChange); - this.client.Utilities.Connection.removeEventListener('syncUpdate', this.handleSyncUpdate); - - this.avatars.forEach((_, agentId) => { - this.removeAvatar(agentId); - }); - - this.avatars.clear(); - } -} diff --git a/client/src/services/vircadia/VircadiaClientCore.ts b/client/src/services/vircadia/VircadiaClientCore.ts deleted file mode 100644 index 894d786b9..000000000 --- a/client/src/services/vircadia/VircadiaClientCore.ts +++ /dev/null @@ -1,445 +0,0 @@ -import { createLogger } from '../../utils/loggerConfig'; - -const logger = createLogger('VircadiaClient'); - -export type ClientCoreConnectionState = - | "connected" - | "connecting" - | "reconnecting" - | "disconnected"; - -export interface ClientCoreConnectionInfo { - status: ClientCoreConnectionState; - isConnected: boolean; - isConnecting: boolean; - isReconnecting: boolean; - connectionDuration?: number; - reconnectAttempts: number; - pendingRequests: Array<{ - requestId: string; - elapsedMs: number; - }>; - agentId: string | null; - sessionId: string | null; -} - -export type ClientCoreConnectionEventListener = () => void; - -export interface ClientCoreConfig { - serverUrl: string; - authToken: string; - authProvider: string; - reconnectAttempts?: number; - reconnectDelay?: number; - debug?: boolean; - suppress?: boolean; -} - -export interface QueryOptions { - query: string; - parameters?: unknown[]; - timeoutMs?: number; -} - -export interface QueryResult { - success: boolean; - result?: T; - errorMessage?: string; - timestamp: number; -} - -// WebSocket message types from Vircadia schema -export enum MessageType { - GENERAL_ERROR_RESPONSE = "GENERAL_ERROR_RESPONSE", - QUERY_REQUEST = "QUERY_REQUEST", - QUERY_RESPONSE = "QUERY_RESPONSE", - SYNC_GROUP_UPDATES_RESPONSE = "SYNC_GROUP_UPDATES_RESPONSE", - TICK_NOTIFICATION_RESPONSE = "TICK_NOTIFICATION_RESPONSE", - SESSION_INFO_RESPONSE = "SESSION_INFO_RESPONSE" -} - -export interface WebSocketMessage { - type: MessageType; - timestamp: number; - requestId?: string; - errorMessage?: string | null; -} - -export interface QueryRequest extends WebSocketMessage { - type: MessageType.QUERY_REQUEST; - query: string; - parameters: unknown[]; -} - -export interface QueryResponse extends WebSocketMessage { - type: MessageType.QUERY_RESPONSE; - result?: T; -} - -export interface SessionInfoResponse extends WebSocketMessage { - type: MessageType.SESSION_INFO_RESPONSE; - agentId: string; - sessionId: string; -} - -const debugLog = (config: ClientCoreConfig, message: string, ...args: unknown[]) => { - if (config.debug && !config.suppress) { - logger.debug(`${message}`, ...args); - } -}; - -const debugError = (config: ClientCoreConfig, message: string, ...args: unknown[]) => { - if (!config.suppress) { - logger.error(`${message}`, ...args); - } -}; - -class CoreConnectionManager { - private ws: WebSocket | null = null; - private reconnectTimer: ReturnType | null = null; - private heartbeatTimer: ReturnType | null = null; - private reconnectCount = 0; - private lastHeartbeatTime: number = 0; - private pendingRequests = new Map< - string, - { - resolve: (value: unknown) => void; - reject: (reason: unknown) => void; - timeout: ReturnType; - } - >(); - private eventListeners = new Map>(); - private lastStatus: ClientCoreConnectionState = "disconnected"; - private connectionStartTime: number | null = null; - private connectionPromise: Promise | null = null; - private agentId: string | null = null; - private sessionId: string | null = null; - private readonly HEARTBEAT_INTERVAL_MS = 30000; - private readonly HEARTBEAT_TIMEOUT_MS = 10000; - - constructor(private config: ClientCoreConfig) {} - - addEventListener(event: string, listener: ClientCoreConnectionEventListener): void { - if (!this.eventListeners.has(event)) { - this.eventListeners.set(event, new Set()); - } - this.eventListeners.get(event)?.add(listener); - } - - removeEventListener(event: string, listener: ClientCoreConnectionEventListener): void { - this.eventListeners.get(event)?.delete(listener); - } - - private emit(event: string): void { - this.eventListeners.get(event)?.forEach(listener => listener()); - } - - private updateStatus(newStatus: ClientCoreConnectionState): void { - if (this.lastStatus !== newStatus) { - this.lastStatus = newStatus; - this.emit("statusChange"); - debugLog(this.config, `Connection status changed to: ${newStatus}`); - } - } - - connect(options?: { timeoutMs?: number }): Promise { - if (this.connectionPromise) { - return this.connectionPromise; - } - - this.connectionPromise = new Promise((resolve, reject) => { - const timeoutMs = options?.timeoutMs || 30000; - const timeoutTimer = setTimeout(() => { - this.disconnect(); - reject(new Error("Connection timeout")); - }, timeoutMs); - - try { - this.updateStatus("connecting"); - this.connectionStartTime = Date.now(); - - const url = new URL(this.config.serverUrl); - - debugLog(this.config, `Connecting to: ${url.toString()}`); - - this.ws = new WebSocket(url.toString()); - - this.ws.onopen = () => { - clearTimeout(timeoutTimer); - // Send auth credentials as first message instead of URL params - this.ws?.send(JSON.stringify({ - type: 'auth', - token: this.config.authToken, - provider: this.config.authProvider - })); - this.updateStatus("connected"); - this.reconnectCount = 0; - this.startHeartbeat(); - debugLog(this.config, "WebSocket connected"); - resolve(this.getConnectionInfo()); - }; - - this.ws.onmessage = (event) => { - this.handleMessage(event.data); - }; - - this.ws.onerror = (error) => { - clearTimeout(timeoutTimer); - debugError(this.config, "WebSocket error:", error); - this.connectionPromise = null; - reject(error); - }; - - this.ws.onclose = () => { - clearTimeout(timeoutTimer); - this.updateStatus("disconnected"); - this.handleReconnection(); - }; - - } catch (error) { - clearTimeout(timeoutTimer); - this.connectionPromise = null; - debugError(this.config, "Connection error:", error); - reject(error); - } - }); - - return this.connectionPromise; - } - - private handleMessage(data: string): void { - try { - const message: WebSocketMessage = JSON.parse(data); - debugLog(this.config, "Received message:", message.type); - - switch (message.type) { - case MessageType.SESSION_INFO_RESPONSE: { - const sessionInfo = message as SessionInfoResponse; - this.agentId = sessionInfo.agentId; - this.sessionId = sessionInfo.sessionId; - debugLog(this.config, `Session info: agentId=${this.agentId}, sessionId=${this.sessionId}`); - break; - } - - case MessageType.QUERY_RESPONSE: { - const response = message as QueryResponse; - if (response.requestId) { - const pending = this.pendingRequests.get(response.requestId); - if (pending) { - clearTimeout(pending.timeout); - this.pendingRequests.delete(response.requestId); - - if (response.errorMessage) { - pending.reject(new Error(response.errorMessage)); - } else { - pending.resolve(response); - } - } - } - break; - } - - case MessageType.SYNC_GROUP_UPDATES_RESPONSE: - this.emit("syncUpdate"); - break; - - case MessageType.TICK_NOTIFICATION_RESPONSE: - this.emit("tick"); - break; - - case MessageType.GENERAL_ERROR_RESPONSE: - debugError(this.config, "Server error:", message.errorMessage); - this.emit("error"); - break; - } - } catch (error) { - debugError(this.config, "Failed to parse message:", error); - } - } - - private handleReconnection(): void { - if (this.reconnectCount >= (this.config.reconnectAttempts || 5)) { - debugLog(this.config, "Max reconnection attempts reached"); - this.connectionPromise = null; - return; - } - - this.updateStatus("reconnecting"); - this.reconnectCount++; - - const delay = this.config.reconnectDelay || 5000; - debugLog(this.config, `Reconnecting in ${delay}ms (attempt ${this.reconnectCount})`); - - this.reconnectTimer = setTimeout(() => { - this.connectionPromise = null; - this.connect(); - }, delay); - } - - disconnect(): void { - this.stopHeartbeat(); - - if (this.reconnectTimer) { - clearTimeout(this.reconnectTimer); - this.reconnectTimer = null; - } - - this.pendingRequests.forEach(({ timeout }) => clearTimeout(timeout)); - this.pendingRequests.clear(); - - if (this.ws) { - this.ws.close(); - this.ws = null; - } - - this.updateStatus("disconnected"); - this.connectionPromise = null; - this.agentId = null; - this.sessionId = null; - } - - - private startHeartbeat(): void { - this.stopHeartbeat(); - this.lastHeartbeatTime = Date.now(); - - this.heartbeatTimer = setInterval(() => { - if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { - this.stopHeartbeat(); - return; - } - - const timeSinceLastHeartbeat = Date.now() - this.lastHeartbeatTime; - - - if (timeSinceLastHeartbeat > this.HEARTBEAT_INTERVAL_MS + this.HEARTBEAT_TIMEOUT_MS) { - debugLog(this.config, "Heartbeat timeout - connection appears stale"); - this.stopHeartbeat(); - this.ws?.close(); - return; - } - - - this.query({ query: "SELECT 1 as heartbeat", timeoutMs: this.HEARTBEAT_TIMEOUT_MS }) - .then(() => { - this.lastHeartbeatTime = Date.now(); - debugLog(this.config, "Heartbeat successful"); - }) - .catch((error) => { - debugError(this.config, "Heartbeat failed:", error); - this.stopHeartbeat(); - this.ws?.close(); - }); - - }, this.HEARTBEAT_INTERVAL_MS); - - debugLog(this.config, "Heartbeat started"); - } - - - private stopHeartbeat(): void { - if (this.heartbeatTimer) { - clearInterval(this.heartbeatTimer); - this.heartbeatTimer = null; - debugLog(this.config, "Heartbeat stopped"); - } - } - - query(options: QueryOptions): Promise> { - return new Promise((resolve, reject) => { - if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { - reject(new Error("WebSocket not connected")); - return; - } - - const requestId = crypto.randomUUID(); - const timeoutMs = options.timeoutMs || 10000; - - const timeout = setTimeout(() => { - this.pendingRequests.delete(requestId); - reject(new Error("Query timeout")); - }, timeoutMs); - - this.pendingRequests.set(requestId, { resolve: resolve as (value: unknown) => void, reject, timeout }); - - const request: QueryRequest = { - type: MessageType.QUERY_REQUEST, - timestamp: Date.now(), - requestId, - query: options.query, - parameters: options.parameters || [], - errorMessage: null - }; - - debugLog(this.config, `Sending query: ${options.query.substring(0, 100)}...`); - this.ws.send(JSON.stringify(request)); - }); - } - - getConnectionInfo(): ClientCoreConnectionInfo { - const now = Date.now(); - return { - status: this.lastStatus, - isConnected: this.lastStatus === "connected", - isConnecting: this.lastStatus === "connecting", - isReconnecting: this.lastStatus === "reconnecting", - connectionDuration: this.connectionStartTime ? now - this.connectionStartTime : undefined, - reconnectAttempts: this.reconnectCount, - pendingRequests: Array.from(this.pendingRequests.entries()).map(([requestId]) => ({ - requestId, - elapsedMs: 0 - })), - agentId: this.agentId, - sessionId: this.sessionId - }; - } -} - -export class ClientCore { - private connectionManager: CoreConnectionManager; - private _utilities: { - Connection: { - connect: (options?: { timeoutMs?: number }) => Promise; - disconnect: () => void; - query: (options: QueryOptions) => Promise>; - getConnectionInfo: () => ClientCoreConnectionInfo; - addEventListener: (event: string, listener: ClientCoreConnectionEventListener) => void; - removeEventListener: (event: string, listener: ClientCoreConnectionEventListener) => void; - }; - } | null = null; - - constructor(config: ClientCoreConfig) { - this.connectionManager = new CoreConnectionManager(config); - } - - get Utilities() { - if (!this._utilities) { - this._utilities = { - Connection: { - connect: (options?: { timeoutMs?: number }) => - this.connectionManager.connect(options), - - disconnect: () => - this.connectionManager.disconnect(), - - query: (options: QueryOptions) => - this.connectionManager.query(options), - - getConnectionInfo: () => - this.connectionManager.getConnectionInfo(), - - addEventListener: (event: string, listener: ClientCoreConnectionEventListener) => - this.connectionManager.addEventListener(event, listener), - - removeEventListener: (event: string, listener: ClientCoreConnectionEventListener) => - this.connectionManager.removeEventListener(event, listener) - } - }; - } - return this._utilities; - } - - dispose(): void { - this.connectionManager.disconnect(); - } -} diff --git a/client/src/vite-env.d.ts b/client/src/vite-env.d.ts index 806f5bfbb..0197ad6ba 100644 --- a/client/src/vite-env.d.ts +++ b/client/src/vite-env.d.ts @@ -9,12 +9,6 @@ interface ImportMetaEnv { readonly VITE_LOG_LEVEL?: string; readonly VITE_ENABLE_ANALYTICS?: string; readonly VITE_ENABLE_TELEMETRY?: string; - readonly VITE_VIRCADIA_SERVER_URL?: string; - readonly VITE_VIRCADIA_AUTH_TOKEN?: string; - readonly VITE_VIRCADIA_AUTH_PROVIDER?: string; - readonly VITE_VIRCADIA_ENABLED?: string; - readonly VITE_VIRCADIA_ENABLE_MULTI_USER?: string; - readonly VITE_VIRCADIA_ENABLE_SPATIAL_AUDIO?: string; readonly VITE_QUEST3_ENABLE_HAND_TRACKING?: string; readonly VITE_INSTANCED_RENDERING?: string; readonly VITE_JSS_URL?: string; diff --git a/config/livekit.yaml b/config/livekit.yaml index c1aee0568..015b5f34d 100644 --- a/config/livekit.yaml +++ b/config/livekit.yaml @@ -17,7 +17,7 @@ keys: room: # Default room settings for VisionClaw voice empty_timeout: 300 # 5 min before closing empty rooms - max_participants: 50 # Matches Vircadia max users + max_participants: 50 # Max concurrent users auto_create: true audio: diff --git a/crates/visionclaw-domain/src/config/services.rs b/crates/visionclaw-domain/src/config/services.rs index 07ea3c8ba..e92adca14 100644 --- a/crates/visionclaw-domain/src/config/services.rs +++ b/crates/visionclaw-domain/src/config/services.rs @@ -159,10 +159,10 @@ pub struct LiveKitSettings { /// Room name template (default: "visionclaw-{world_id}") #[serde(skip_serializing_if = "Option::is_none", alias = "room_prefix")] pub room_prefix: Option, - /// Enable spatial audio based on Vircadia entity positions + /// Enable spatial audio based on XR entity positions #[serde(default = "default_true", alias = "spatial_audio")] pub spatial_audio: bool, - /// Max distance (in Vircadia units) before audio falls to zero + /// Max distance (in XR units) before audio falls to zero #[serde(default = "default_spatial_max_distance", alias = "spatial_max_distance")] pub spatial_max_distance: f32, } diff --git a/crates/visionclaw-domain/src/types/speech.rs b/crates/visionclaw-domain/src/types/speech.rs index ba381dcf1..7094cc953 100644 --- a/crates/visionclaw-domain/src/types/speech.rs +++ b/crates/visionclaw-domain/src/types/speech.rs @@ -144,7 +144,7 @@ impl Default for TranscriptionOptions { pub struct AgentSpatialInfo { /// Agent identifier pub agent_id: String, - /// 3D position in Vircadia world coordinates + /// 3D position in XR world coordinates pub position: [f32; 3], /// Owner user ID (for private fallback) pub owner_user_id: String, diff --git a/scripts/init-vircadia-db.sql b/scripts/init-vircadia-db.sql deleted file mode 100644 index 0ae4e6c94..000000000 --- a/scripts/init-vircadia-db.sql +++ /dev/null @@ -1,101 +0,0 @@ --- Initialize Vircadia World Server Database --- This script runs on first PostgreSQL startup - --- Create Vircadia database (if not exists) -SELECT 'CREATE DATABASE vircadia_world' -WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'vircadia_world')\gexec - --- Connect to vircadia_world database -\c vircadia_world; - --- Enable required extensions -CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; -CREATE EXTENSION IF NOT EXISTS "postgis"; - --- Worlds table -CREATE TABLE IF NOT EXISTS worlds ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), - name VARCHAR(255) NOT NULL, - description TEXT, - owner_id VARCHAR(255), - max_users INTEGER DEFAULT 50, - created_at TIMESTAMP DEFAULT NOW(), - updated_at TIMESTAMP DEFAULT NOW() -); - --- Entities table (3D objects in world) -CREATE TABLE IF NOT EXISTS entities ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), - world_id UUID REFERENCES worlds(id) ON DELETE CASCADE, - entity_type VARCHAR(50) NOT NULL, - name VARCHAR(255), - position_x DOUBLE PRECISION DEFAULT 0, - position_y DOUBLE PRECISION DEFAULT 0, - position_z DOUBLE PRECISION DEFAULT 0, - rotation_x DOUBLE PRECISION DEFAULT 0, - rotation_y DOUBLE PRECISION DEFAULT 0, - rotation_z DOUBLE PRECISION DEFAULT 0, - rotation_w DOUBLE PRECISION DEFAULT 1, - scale_x DOUBLE PRECISION DEFAULT 1, - scale_y DOUBLE PRECISION DEFAULT 1, - scale_z DOUBLE PRECISION DEFAULT 1, - metadata JSONB, - owner_id VARCHAR(255), - created_at TIMESTAMP DEFAULT NOW(), - updated_at TIMESTAMP DEFAULT NOW() -); - --- Spatial index for entities -CREATE INDEX IF NOT EXISTS entities_spatial_idx ON entities USING gist ( - cube( - ARRAY[position_x, position_y, position_z], - ARRAY[position_x, position_y, position_z] - ) -); - --- Sessions table (user connections) -CREATE TABLE IF NOT EXISTS sessions ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), - world_id UUID REFERENCES worlds(id) ON DELETE CASCADE, - agent_id VARCHAR(255) NOT NULL, - username VARCHAR(255), - connected_at TIMESTAMP DEFAULT NOW(), - last_seen_at TIMESTAMP DEFAULT NOW(), - metadata JSONB -); - --- Annotations table (collaborative notes) -CREATE TABLE IF NOT EXISTS annotations ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), - world_id UUID REFERENCES worlds(id) ON DELETE CASCADE, - entity_id UUID REFERENCES entities(id) ON DELETE CASCADE, - agent_id VARCHAR(255) NOT NULL, - username VARCHAR(255), - text TEXT NOT NULL, - position_x DOUBLE PRECISION, - position_y DOUBLE PRECISION, - position_z DOUBLE PRECISION, - created_at TIMESTAMP DEFAULT NOW() -); - --- Insert default world -INSERT INTO worlds (id, name, description, owner_id) -VALUES ( - '00000000-0000-0000-0000-000000000001', - 'VisionClaw World', - 'Default multi-user world for VisionClaw agent swarm visualization', - 'system' -) ON CONFLICT DO NOTHING; - --- Create indexes -CREATE INDEX IF NOT EXISTS idx_entities_world_id ON entities(world_id); -CREATE INDEX IF NOT EXISTS idx_entities_type ON entities(entity_type); -CREATE INDEX IF NOT EXISTS idx_sessions_world_id ON sessions(world_id); -CREATE INDEX IF NOT EXISTS idx_sessions_agent_id ON sessions(agent_id); -CREATE INDEX IF NOT EXISTS idx_annotations_world_id ON annotations(world_id); -CREATE INDEX IF NOT EXISTS idx_annotations_entity_id ON annotations(entity_id); - --- Grant permissions -GRANT ALL PRIVILEGES ON DATABASE vircadia_world TO visionclaw; -GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO visionclaw; -GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO visionclaw; diff --git a/src/config/services.rs b/src/config/services.rs index 07ea3c8ba..e92adca14 100644 --- a/src/config/services.rs +++ b/src/config/services.rs @@ -159,10 +159,10 @@ pub struct LiveKitSettings { /// Room name template (default: "visionclaw-{world_id}") #[serde(skip_serializing_if = "Option::is_none", alias = "room_prefix")] pub room_prefix: Option, - /// Enable spatial audio based on Vircadia entity positions + /// Enable spatial audio based on XR entity positions #[serde(default = "default_true", alias = "spatial_audio")] pub spatial_audio: bool, - /// Max distance (in Vircadia units) before audio falls to zero + /// Max distance (in XR units) before audio falls to zero #[serde(default = "default_spatial_max_distance", alias = "spatial_max_distance")] pub spatial_max_distance: f32, } diff --git a/src/services/audio_router.rs b/src/services/audio_router.rs index d36311dc1..bb3bb6afc 100644 --- a/src/services/audio_router.rs +++ b/src/services/audio_router.rs @@ -31,7 +31,7 @@ pub struct UserVoiceSession { pub ptt_active: bool, /// LiveKit participant ID for spatial audio pub livekit_participant_id: Option, - /// User's 3D position in the Vircadia world (for spatial audio) + /// User's 3D position in the XR world (for spatial audio) pub spatial_position: [f32; 3], } @@ -45,7 +45,7 @@ pub struct AgentVoiceIdentity { pub voice_id: String, /// Speech speed multiplier pub speed: f32, - /// Agent's 3D position in Vircadia world + /// Agent's 3D position in XR world pub position: [f32; 3], /// Whether voice is public (all users hear spatially) or private (owner only) pub public_voice: bool, @@ -287,7 +287,7 @@ impl AudioRouter { .collect() } - /// Update user's spatial position (for Vircadia presence sync) + /// Update user's spatial position (for XR presence sync) pub async fn update_user_position(&self, user_id: &str, position: [f32; 3]) { let mut sessions = self.sessions.write().await; if let Some(session) = sessions.get_mut(user_id) { diff --git a/src/types/speech.rs b/src/types/speech.rs index ba381dcf1..7094cc953 100644 --- a/src/types/speech.rs +++ b/src/types/speech.rs @@ -144,7 +144,7 @@ impl Default for TranscriptionOptions { pub struct AgentSpatialInfo { /// Agent identifier pub agent_id: String, - /// 3D position in Vircadia world coordinates + /// 3D position in XR world coordinates pub position: [f32; 3], /// Owner user ID (for private fallback) pub owner_user_id: String, diff --git a/vircadia-world/server/service/schemas/AI-SHACL.ttl b/vircadia-world/server/service/schemas/AI-SHACL.ttl deleted file mode 100644 index cbe88f8f9..000000000 --- a/vircadia-world/server/service/schemas/AI-SHACL.ttl +++ /dev/null @@ -1,756 +0,0 @@ -@prefix sh: . -@prefix rdf: . -@prefix rdfs: . -@prefix owl: . -@prefix xsd: . -@prefix dcterms: . -@prefix skos: . -@prefix ai: . - -# -# AI Grounded Ontology - SHACL Validation Shapes -# -# This file defines comprehensive validation constraints for the AI Grounded Ontology -# to ensure quality, conformance, and consistency across all AI concept definitions. -# -# Author: SHACL Shapes Author Agent -# Date: 2025-10-27 -# Version: 1.0.0 -# Specification: W3C SHACL (https://www.w3.org/TR/shacl/) -# - -# ============================================================================== -# SHAPE 1: Core AI Concept Shape -# ============================================================================== -# Purpose: Validates that every AI concept has the required structural elements: -# - Unique term identifier -# - Preferred term label -# - Comprehensive definition (50-500 words) -# - Authoritative source citations (minimum 3) -# - Parent class relationships -# ============================================================================== - -ai:AIConcept-Shape - a sh:NodeShape ; - sh:targetClass owl:Class ; - sh:targetSubjectsOf rdfs:subClassOf ; - rdfs:label "AI Concept Structural Conformance Shape"@en-GB ; - rdfs:comment "Ensures all AI concepts comply with the grounded ontology structural requirements."@en-GB ; - - # Every AI concept must have exactly one term identifier - sh:property [ - sh:path ai:term-id ; - sh:minCount 1 ; - sh:maxCount 1 ; - sh:datatype xsd:string ; - sh:pattern "^AI-[0-9]{4}$" ; - sh:message "Every AI concept must have exactly one term identifier in format AI-NNNN"@en-GB ; - ] ; - - # Every AI concept must have exactly one preferred term - sh:property [ - sh:path skos:prefLabel ; - sh:minCount 1 ; - sh:maxCount 1 ; - sh:datatype xsd:string ; - sh:minLength 2 ; - sh:maxLength 100 ; - sh:message "Every AI concept must have exactly one preferred term (2-100 characters)"@en-GB ; - ] ; - - # Every AI concept must have exactly one definition - sh:property [ - sh:path skos:definition ; - sh:minCount 1 ; - sh:maxCount 1 ; - sh:datatype xsd:string ; - sh:minLength 50 ; - sh:maxLength 5000 ; - sh:message "Every AI concept must have exactly one definition (50-5000 characters, approximately 50-500 words)"@en-GB ; - ] ; - - # Every AI concept must have at least 3 authoritative sources - sh:property [ - sh:path dcterms:source ; - sh:minCount 3 ; - sh:datatype xsd:string ; - sh:message "Every AI concept must have at least 3 authoritative source citations"@en-GB ; - ] ; - - # Every AI concept must have at least one parent class - sh:property [ - sh:path rdfs:subClassOf ; - sh:minCount 1 ; - sh:nodeKind sh:IRI ; - sh:message "Every AI concept must have at least one parent class relationship"@en-GB ; - ] ; - - # Every AI concept should have alternative labels - sh:property [ - sh:path skos:altLabel ; - sh:minCount 0 ; - sh:datatype xsd:string ; - sh:severity sh:Warning ; - sh:message "Consider adding alternative labels for improved discoverability"@en-GB ; - ] . - -# ============================================================================== -# SHAPE 2: Definition Quality Shape -# ============================================================================== -# Purpose: Validates definition content quality -# - Word count validation (50-500 words) -# - Ensures definition contains key structural elements -# - Checks for proper citation integration -# ============================================================================== - -ai:DefinitionQuality-Shape - a sh:NodeShape ; - sh:targetClass owl:Class ; - rdfs:label "AI Concept Definition Quality Shape"@en-GB ; - rdfs:comment "Ensures definitions meet quality standards for clarity and comprehensiveness."@en-GB ; - - # Definition must not contain common placeholder text - sh:property [ - sh:path skos:definition ; - sh:not [ - sh:pattern "(?i)(TODO|FIXME|TBD|XXX|placeholder)" ; - ] ; - sh:message "Definition must not contain placeholder text (TODO, FIXME, TBD, etc.)"@en-GB ; - ] ; - - # Definition should reference key terminology - sh:property [ - sh:path skos:definition ; - sh:pattern ".*" ; - sh:severity sh:Info ; - sh:message "Ensure definition uses precise technical terminology from authoritative sources"@en-GB ; - ] . - -# ============================================================================== -# SHAPE 3: Citation Format Validation Shape -# ============================================================================== -# Purpose: Validates that citations follow the required format: -# - [Standard/Document Name] Section X.Y.Z, [URL], Last accessed: YYYY-MM-DD -# - DOI format for academic papers -# - Valid permalink patterns -# ============================================================================== - -ai:CitationFormat-Shape - a sh:NodeShape ; - sh:targetClass owl:Class ; - rdfs:label "Citation Format Validation Shape"@en-GB ; - rdfs:comment "Ensures all source citations follow the required format for traceability and verification."@en-GB ; - - # Citation must follow standard format pattern - sh:property [ - sh:path dcterms:source ; - sh:pattern "^\\[.+\\].*,\\s*(https?://|doi:).+,\\s*Last accessed:\\s*[0-9]{4}-[0-9]{2}-[0-9]{2}$|^doi:[0-9]{2}\\.[0-9]{4,}/.+" ; - sh:message "Citation must follow format: [Source Name] Section, [URL], Last accessed: YYYY-MM-DD OR doi:XX.XXXX/..."@en-GB ; - ] ; - - # URL in citation must be valid - sh:property [ - sh:path dcterms:source ; - sh:pattern ".*(https?://[^\\s,]+).*" ; - sh:message "Citation must contain a valid HTTP/HTTPS URL or DOI"@en-GB ; - ] . - -# ============================================================================== -# SHAPE 4: Standards Alignment Shape -# ============================================================================== -# Purpose: Validates alignment with international standards: -# - ISO/IEC standard references -# - NIST framework mappings -# - EU AI Act classifications -# - OECD dimension assignments -# ============================================================================== - -ai:StandardsAlignment-Shape - a sh:NodeShape ; - sh:targetClass owl:Class ; - rdfs:label "Standards Alignment Validation Shape"@en-GB ; - rdfs:comment "Ensures AI concepts are properly aligned with international standards and frameworks."@en-GB ; - - # ISO/IEC standard alignment (when applicable) - sh:property [ - sh:path ai:iso-iec-standard ; - sh:datatype xsd:string ; - sh:pattern "^ISO/IEC\\s+[0-9]+(:[0-9]{4})?(-[0-9]+)?(:[0-9]{4})?$" ; - sh:severity sh:Info ; - sh:message "ISO/IEC standard reference should follow format: ISO/IEC NNNNN:YYYY or ISO/IEC NNNNN-N:YYYY"@en-GB ; - ] ; - - # NIST framework mapping (when applicable) - sh:property [ - sh:path ai:nist-framework ; - sh:datatype xsd:string ; - sh:in ( - "GOVERN" - "MAP" - "MEASURE" - "MANAGE" - ) ; - sh:severity sh:Info ; - sh:message "NIST AI RMF function should be one of: GOVERN, MAP, MEASURE, MANAGE"@en-GB ; - ] ; - - # EU AI Act classification (when applicable) - sh:property [ - sh:path ai:eu-ai-act-risk-level ; - sh:datatype xsd:string ; - sh:in ( - "Unacceptable Risk" - "High Risk" - "Limited Risk" - "Minimal Risk" - ) ; - sh:severity sh:Info ; - sh:message "EU AI Act risk level should be one of: Unacceptable Risk, High Risk, Limited Risk, Minimal Risk"@en-GB ; - ] ; - - # OECD AI Principles dimension - sh:property [ - sh:path ai:oecd-dimension ; - sh:datatype xsd:string ; - sh:in ( - "Inclusive Growth" - "Sustainable Development" - "Well-being" - "Human-centred Values" - "Fairness" - "Transparency" - "Explainability" - "Robustness" - "Security" - "Safety" - "Accountability" - ) ; - sh:severity sh:Info ; - sh:message "OECD dimension should align with OECD AI Principles framework"@en-GB ; - ] . - -# ============================================================================== -# SHAPE 5: Object Property Validation Shape -# ============================================================================== -# Purpose: Validates object properties (relationships between concepts): -# - Must have valid domain and range -# - Must have proper labels and descriptions -# - Cardinality constraints -# ============================================================================== - -ai:ObjectProperty-Shape - a sh:NodeShape ; - sh:targetClass owl:ObjectProperty ; - rdfs:label "Object Property Validation Shape"@en-GB ; - rdfs:comment "Ensures object properties have valid domains, ranges, and documentation."@en-GB ; - - # Object property must have a label - sh:property [ - sh:path rdfs:label ; - sh:minCount 1 ; - sh:datatype xsd:string ; - sh:message "Object property must have at least one label"@en-GB ; - ] ; - - # Object property must have a comment/definition - sh:property [ - sh:path rdfs:comment ; - sh:minCount 1 ; - sh:datatype xsd:string ; - sh:minLength 20 ; - sh:message "Object property must have a descriptive comment (minimum 20 characters)"@en-GB ; - ] ; - - # Object property should have a domain - sh:property [ - sh:path rdfs:domain ; - sh:minCount 1 ; - sh:nodeKind sh:IRI ; - sh:severity sh:Warning ; - sh:message "Object property should specify at least one domain class"@en-GB ; - ] ; - - # Object property should have a range - sh:property [ - sh:path rdfs:range ; - sh:minCount 1 ; - sh:nodeKind sh:IRI ; - sh:severity sh:Warning ; - sh:message "Object property should specify at least one range class"@en-GB ; - ] . - -# ============================================================================== -# SHAPE 6: Data Property Validation Shape -# ============================================================================== -# Purpose: Validates data properties (attributes with literal values): -# - Must have valid datatype -# - Must have proper documentation -# - Domain specification -# ============================================================================== - -ai:DataProperty-Shape - a sh:NodeShape ; - sh:targetClass owl:DatatypeProperty ; - rdfs:label "Data Property Validation Shape"@en-GB ; - rdfs:comment "Ensures data properties have valid datatypes and documentation."@en-GB ; - - # Data property must have a label - sh:property [ - sh:path rdfs:label ; - sh:minCount 1 ; - sh:datatype xsd:string ; - sh:message "Data property must have at least one label"@en-GB ; - ] ; - - # Data property must have a comment/definition - sh:property [ - sh:path rdfs:comment ; - sh:minCount 1 ; - sh:datatype xsd:string ; - sh:minLength 20 ; - sh:message "Data property must have a descriptive comment (minimum 20 characters)"@en-GB ; - ] ; - - # Data property must have a range datatype - sh:property [ - sh:path rdfs:range ; - sh:minCount 1 ; - sh:nodeKind sh:IRI ; - sh:message "Data property must specify a valid XSD datatype as range"@en-GB ; - ] ; - - # Data property should have a domain - sh:property [ - sh:path rdfs:domain ; - sh:minCount 1 ; - sh:nodeKind sh:IRI ; - sh:severity sh:Warning ; - sh:message "Data property should specify at least one domain class"@en-GB ; - ] . - -# ============================================================================== -# SHAPE 7: OWL Axiom Requirements Shape -# ============================================================================== -# Purpose: Validates that concepts include proper OWL axioms: -# - Class declarations must be present -# - Axioms must be syntactically valid (checked via pattern) -# - Functional syntax requirement -# ============================================================================== - -ai:OWLAxiom-Shape - a sh:NodeShape ; - sh:targetClass owl:Class ; - rdfs:label "OWL Axiom Requirements Shape"@en-GB ; - rdfs:comment "Ensures every AI concept includes valid OWL axioms in functional syntax."@en-GB ; - - # Every class must have OWL axiom documentation - sh:property [ - sh:path ai:owl-axiom ; - sh:minCount 1 ; - sh:datatype xsd:string ; - sh:message "Every AI concept must include OWL functional syntax axioms"@en-GB ; - ] ; - - # OWL axiom must contain Declaration statement - sh:property [ - sh:path ai:owl-axiom ; - sh:pattern "(?s).*Declaration\\s*\\(\\s*Class\\s*\\(.*" ; - sh:message "OWL axiom must include a Declaration(Class(...)) statement"@en-GB ; - ] ; - - # OWL axiom should be properly formatted - sh:property [ - sh:path ai:owl-axiom ; - sh:pattern "(?s).*SubClassOf\\s*\\(.*" ; - sh:severity sh:Warning ; - sh:message "OWL axiom should include SubClassOf relationships for proper hierarchy"@en-GB ; - ] . - -# ============================================================================== -# SHAPE 8: Metadata Completeness Shape -# ============================================================================== -# Purpose: Validates comprehensive metadata: -# - Creation and modification dates -# - Creator/contributor information -# - Version information -# - License information -# ============================================================================== - -ai:MetadataCompleteness-Shape - a sh:NodeShape ; - sh:targetClass owl:Class ; - rdfs:label "Metadata Completeness Shape"@en-GB ; - rdfs:comment "Ensures AI concepts include comprehensive metadata for provenance and governance."@en-GB ; - - # Creation date - sh:property [ - sh:path dcterms:created ; - sh:maxCount 1 ; - sh:datatype xsd:dateTime ; - sh:severity sh:Warning ; - sh:message "Consider adding creation date (dcterms:created) for provenance tracking"@en-GB ; - ] ; - - # Modification date - sh:property [ - sh:path dcterms:modified ; - sh:maxCount 1 ; - sh:datatype xsd:dateTime ; - sh:severity sh:Warning ; - sh:message "Consider adding last modification date (dcterms:modified) for version control"@en-GB ; - ] ; - - # Creator information - sh:property [ - sh:path dcterms:creator ; - sh:datatype xsd:string ; - sh:severity sh:Info ; - sh:message "Consider adding creator information for attribution"@en-GB ; - ] ; - - # License information - sh:property [ - sh:path dcterms:license ; - sh:maxCount 1 ; - sh:nodeKind sh:IRI ; - sh:severity sh:Info ; - sh:message "Consider specifying license information for usage rights"@en-GB ; - ] . - -# ============================================================================== -# SHAPE 9: Taxonomy Hierarchy Validation Shape -# ============================================================================== -# Purpose: Validates proper taxonomy structure: -# - No circular inheritance -# - Maximum depth constraints -# - Proper categorisation -# ============================================================================== - -ai:TaxonomyHierarchy-Shape - a sh:NodeShape ; - sh:targetClass owl:Class ; - rdfs:label "Taxonomy Hierarchy Validation Shape"@en-GB ; - rdfs:comment "Ensures AI concepts maintain a valid and coherent taxonomic hierarchy."@en-GB ; - - # Class must not be its own parent (prevents direct circular inheritance) - sh:sparql [ - sh:message "Class must not be its own parent (circular inheritance detected)"@en-GB ; - sh:prefixes [ - sh:prefix "rdfs" ; - sh:namespace "http://www.w3.org/2000/01/rdf-schema#"^^xsd:anyURI ; - ] ; - sh:select """ - SELECT $this - WHERE { - $this rdfs:subClassOf $this . - } - """ ; - ] ; - - # Top-level concepts should have explicit categorisation - sh:property [ - sh:path ai:category ; - sh:datatype xsd:string ; - sh:in ( - "Core AI Concepts" - "Machine Learning" - "Natural Language Processing" - "Computer Vision" - "Robotics" - "Knowledge Representation" - "AI Ethics & Governance" - "AI Safety & Security" - "AI Applications" - ) ; - sh:severity sh:Info ; - sh:message "Consider assigning concept to a primary AI category for organisation"@en-GB ; - ] . - -# ============================================================================== -# SHAPE 10: Cross-Reference Validation Shape -# ============================================================================== -# Purpose: Validates cross-references and related concepts: -# - Related terms must exist -# - See also references must be valid IRIs -# - Narrow/broader term relationships -# ============================================================================== - -ai:CrossReference-Shape - a sh:NodeShape ; - sh:targetClass owl:Class ; - rdfs:label "Cross-Reference Validation Shape"@en-GB ; - rdfs:comment "Ensures cross-references and related concept links are valid and meaningful."@en-GB ; - - # Related concepts should be valid IRIs - sh:property [ - sh:path skos:related ; - sh:nodeKind sh:IRI ; - sh:severity sh:Info ; - sh:message "Related concepts should reference valid ontology IRIs"@en-GB ; - ] ; - - # Broader concepts should be valid IRIs - sh:property [ - sh:path skos:broader ; - sh:nodeKind sh:IRI ; - sh:severity sh:Info ; - sh:message "Broader concepts should reference valid ontology IRIs"@en-GB ; - ] ; - - # Narrower concepts should be valid IRIs - sh:property [ - sh:path skos:narrower ; - sh:nodeKind sh:IRI ; - sh:severity sh:Info ; - sh:message "Narrower concepts should reference valid ontology IRIs"@en-GB ; - ] ; - - # See also references must be valid IRIs or URLs - sh:property [ - sh:path rdfs:seeAlso ; - sh:nodeKind sh:IRI ; - sh:severity sh:Info ; - sh:message "See also references should be valid IRIs or URLs"@en-GB ; - ] . - -# ============================================================================== -# SHAPE 11: Example and Usage Shape -# ============================================================================== -# Purpose: Validates that concepts include practical examples: -# - Usage examples -# - Application scenarios -# - Code snippets (where applicable) -# ============================================================================== - -ai:ExampleUsage-Shape - a sh:NodeShape ; - sh:targetClass owl:Class ; - rdfs:label "Example and Usage Validation Shape"@en-GB ; - rdfs:comment "Encourages inclusion of practical examples and usage scenarios for clarity."@en-GB ; - - # Example usage - sh:property [ - sh:path skos:example ; - sh:datatype xsd:string ; - sh:minLength 20 ; - sh:severity sh:Info ; - sh:message "Consider adding concrete examples to illustrate concept usage"@en-GB ; - ] ; - - # Scope note for context - sh:property [ - sh:path skos:scopeNote ; - sh:datatype xsd:string ; - sh:minLength 20 ; - sh:severity sh:Info ; - sh:message "Consider adding scope notes to clarify intended usage and boundaries"@en-GB ; - ] . - -# ============================================================================== -# SHAPE 12: Multilingual Support Shape -# ============================================================================== -# Purpose: Validates multilingual labels and definitions: -# - Language tags must be valid -# - Core content should have English (en-GB) version -# ============================================================================== - -ai:MultilingualSupport-Shape - a sh:NodeShape ; - sh:targetClass owl:Class ; - rdfs:label "Multilingual Support Validation Shape"@en-GB ; - rdfs:comment "Ensures proper language tagging for international accessibility."@en-GB ; - - # Preferred label must exist in UK English - sh:property [ - sh:path skos:prefLabel ; - sh:minCount 1 ; - sh:languageIn ("en-GB" "en") ; - sh:message "Preferred label must exist in UK English (en-GB) or English (en)"@en-GB ; - ] ; - - # Definition must exist in UK English - sh:property [ - sh:path skos:definition ; - sh:minCount 1 ; - sh:languageIn ("en-GB" "en") ; - sh:message "Definition must exist in UK English (en-GB) or English (en)"@en-GB ; - ] ; - - # If additional language versions exist, they must have valid language tags - sh:property [ - sh:path skos:prefLabel ; - sh:languageIn ("en" "en-GB" "en-US" "fr" "de" "es" "it" "ja" "zh" "ar" "ru") ; - sh:severity sh:Info ; - sh:message "Language tags should use standard BCP 47 language codes"@en-GB ; - ] . - -# ============================================================================== -# SHAPE 13: Deprecation and Versioning Shape -# ============================================================================== -# Purpose: Validates proper deprecation and versioning: -# - Deprecated concepts must have replacement information -# - Version information should be tracked -# ============================================================================== - -ai:DeprecationVersioning-Shape - a sh:NodeShape ; - sh:targetClass owl:DeprecatedClass ; - rdfs:label "Deprecation and Versioning Shape"@en-GB ; - rdfs:comment "Ensures deprecated concepts provide clear migration paths and version information."@en-GB ; - - # Deprecated concept must have replacement information - sh:property [ - sh:path dcterms:isReplacedBy ; - sh:minCount 1 ; - sh:nodeKind sh:IRI ; - sh:message "Deprecated concept must specify replacement concept (dcterms:isReplacedBy)"@en-GB ; - ] ; - - # Deprecated concept should have deprecation note - sh:property [ - sh:path owl:deprecated ; - sh:hasValue true ; - sh:message "Deprecated concept must have owl:deprecated set to true"@en-GB ; - ] ; - - # Version information - sh:property [ - sh:path owl:versionInfo ; - sh:datatype xsd:string ; - sh:severity sh:Warning ; - sh:message "Consider adding version information for deprecated concepts"@en-GB ; - ] . - -# ============================================================================== -# SHAPE 14: High-Risk AI System Shape -# ============================================================================== -# Purpose: Special validation for high-risk AI systems per EU AI Act: -# - Enhanced documentation requirements -# - Risk assessment information -# - Compliance markers -# ============================================================================== - -ai:HighRiskAISystem-Shape - a sh:NodeShape ; - sh:target [ - a sh:SPARQLTarget ; - sh:prefixes [ - sh:prefix "ai" ; - sh:namespace "http://metaverse-ontology.org/ai-grounded#"^^xsd:anyURI ; - ] ; - sh:select """ - SELECT ?this - WHERE { - ?this ai:eu-ai-act-risk-level "High Risk" . - } - """ ; - ] ; - rdfs:label "High-Risk AI System Validation Shape"@en-GB ; - rdfs:comment "Applies enhanced validation requirements for high-risk AI systems per EU AI Act."@en-GB ; - - # High-risk concepts must have risk assessment documentation - sh:property [ - sh:path ai:risk-assessment ; - sh:minCount 1 ; - sh:datatype xsd:string ; - sh:minLength 100 ; - sh:message "High-risk AI concepts must include comprehensive risk assessment documentation (minimum 100 characters)"@en-GB ; - ] ; - - # High-risk concepts must have mitigation strategies - sh:property [ - sh:path ai:risk-mitigation ; - sh:minCount 1 ; - sh:datatype xsd:string ; - sh:minLength 50 ; - sh:message "High-risk AI concepts must document risk mitigation strategies"@en-GB ; - ] ; - - # High-risk concepts must have compliance documentation - sh:property [ - sh:path ai:compliance-requirements ; - sh:minCount 1 ; - sh:datatype xsd:string ; - sh:message "High-risk AI concepts must document specific compliance requirements"@en-GB ; - ] ; - - # Enhanced source citation requirement (minimum 5 for high-risk) - sh:property [ - sh:path dcterms:source ; - sh:minCount 5 ; - sh:message "High-risk AI concepts must have at least 5 authoritative sources"@en-GB ; - ] . - -# ============================================================================== -# SHAPE 15: Technical Specification Shape -# ============================================================================== -# Purpose: Validates technical specifications for implementable concepts: -# - Algorithm specifications -# - Performance metrics -# - Implementation constraints -# ============================================================================== - -ai:TechnicalSpecification-Shape - a sh:NodeShape ; - sh:targetClass owl:Class ; - rdfs:label "Technical Specification Validation Shape"@en-GB ; - rdfs:comment "Validates technical specifications for implementable AI concepts."@en-GB ; - - # Algorithm specification (when applicable) - sh:property [ - sh:path ai:algorithm-specification ; - sh:datatype xsd:string ; - sh:severity sh:Info ; - sh:message "Consider adding algorithm specification for implementable concepts"@en-GB ; - ] ; - - # Performance metrics (when applicable) - sh:property [ - sh:path ai:performance-metrics ; - sh:datatype xsd:string ; - sh:severity sh:Info ; - sh:message "Consider documenting expected performance metrics for measurable concepts"@en-GB ; - ] ; - - # Computational complexity (when applicable) - sh:property [ - sh:path ai:computational-complexity ; - sh:datatype xsd:string ; - sh:pattern "^O\\([^)]+\\)$" ; - sh:severity sh:Info ; - sh:message "Computational complexity should use Big-O notation: O(n), O(log n), etc."@en-GB ; - ] . - -# ============================================================================== -# End of AI Grounded Ontology SHACL Shapes -# ============================================================================== -# -# Usage Instructions: -# ------------------- -# 1. Load this file into a SHACL validator (e.g., Apache Jena, TopBraid, pySHACL) -# 2. Validate ontology files against these shapes: -# - Command line: shacl validate --shapes AI-SHACL.ttl --data your-ontology.ttl -# - Python: pyshacl.validate(data_graph, shacl_graph=shapes_graph) -# 3. Review validation reports for violations, warnings, and informational messages -# 4. Severity levels: -# - sh:Violation: Must be fixed (structural requirements) -# - sh:Warning: Should be addressed (best practices) -# - sh:Info: Optional enhancements (quality improvements) -# -# Validation Report Example: -# -------------------------- -# The validator will produce reports with: -# - Focus node: The subject being validated -# - Result path: The property being checked -# - Message: Human-readable explanation -# - Severity: Violation, Warning, or Info -# - Source shape: Which shape detected the issue -# -# Maintenance: -# ------------ -# - Update shapes as ontology requirements evolve -# - Add new shapes for emerging AI standards -# - Version control all changes to validation rules -# - Test shapes against sample valid and invalid data -# -# ==============================================================================