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.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
-#
-# ==============================================================================