|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class VRController { |
|
|
constructor(scene, camera, renderer) { |
|
|
this.scene = scene; |
|
|
this.camera = camera; |
|
|
this.renderer = renderer; |
|
|
this.isInitialized = false; |
|
|
this.isSessionActive = false; |
|
|
this.session = null; |
|
|
this.referenceSpace = null; |
|
|
this.controllers = []; |
|
|
this.laserPointers = []; |
|
|
this.currentTarget = null; |
|
|
|
|
|
|
|
|
this.config = { |
|
|
sessionType: 'immersive-vr', |
|
|
requiredFeatures: ['local-floor'], |
|
|
optionalFeatures: ['hand-tracking', 'eye-tracking'], |
|
|
maxControllers: 2, |
|
|
laserPointerLength: 10, |
|
|
hapticFeedback: true, |
|
|
saveSessions: true |
|
|
}; |
|
|
|
|
|
|
|
|
this.mockControllers = this.createMockControllers(); |
|
|
|
|
|
console.log('🥽 تم تهيئة VR Controller'); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async initialize() { |
|
|
try { |
|
|
console.log('🔧 بدء تهيئة VR...'); |
|
|
|
|
|
|
|
|
if (!navigator.xr) { |
|
|
throw new Error('WebXR غير مدعوم في هذا المتصفح'); |
|
|
} |
|
|
|
|
|
|
|
|
const isVRAvailable = await navigator.xr.isSessionSupported(this.config.sessionType); |
|
|
if (!isVRAvailable) { |
|
|
throw new Error('VR غير متاح على هذا الجهاز'); |
|
|
} |
|
|
|
|
|
|
|
|
this.createControllers(); |
|
|
|
|
|
|
|
|
if (this.config.optionalFeatures.includes('eye-tracking')) { |
|
|
await this.initializeEyeTracking(); |
|
|
} |
|
|
|
|
|
this.isInitialized = true; |
|
|
console.log('✅ تم تهيئة VR بنجاح'); |
|
|
|
|
|
return true; |
|
|
|
|
|
} catch (error) { |
|
|
console.error('❌ خطأ في تهيئة VR:', error); |
|
|
throw error; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
createControllers() { |
|
|
for (let i = 0; i < this.config.maxControllers; i++) { |
|
|
const controller = { |
|
|
id: `controller_${i}`, |
|
|
handedness: i === 0 ? 'left' : 'right', |
|
|
gamepad: { |
|
|
axes: [0, 0, 0, 0], |
|
|
buttons: [ |
|
|
{ pressed: false, value: 0 }, |
|
|
{ pressed: false, value: 0 }, |
|
|
{ pressed: false, value: 0 }, |
|
|
{ pressed: false, value: 0 }, |
|
|
{ pressed: false, value: 0 }, |
|
|
{ pressed: false, value: 0 } |
|
|
], |
|
|
hapticActuators: [ |
|
|
{ |
|
|
pulse: (intensity, duration) => { |
|
|
console.log(`📳 اهتزاز جهاز التحكم ${i}: ${intensity}% لمدة ${duration}ms`); |
|
|
return Promise.resolve(); |
|
|
} |
|
|
} |
|
|
] |
|
|
}, |
|
|
connected: true, |
|
|
pose: { |
|
|
position: { x: 0, y: 1.5, z: 0 }, |
|
|
orientation: { x: 0, y: 0, z: 0, w: 1 } |
|
|
} |
|
|
}; |
|
|
|
|
|
this.controllers.push(controller); |
|
|
this.createLaserPointer(controller); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
createLaserPointer(controller) { |
|
|
const geometry = new THREE.CylinderGeometry(0.002, 0.002, this.config.laserPointerLength, 8); |
|
|
const material = new THREE.MeshBasicMaterial({ |
|
|
color: 0x22d3ee, |
|
|
transparent: true, |
|
|
opacity: 0.8, |
|
|
side: THREE.DoubleSide |
|
|
}); |
|
|
|
|
|
const laser = new THREE.Mesh(geometry, material); |
|
|
laser.rotation.x = Math.PI / 2; |
|
|
laser.visible = false; |
|
|
|
|
|
|
|
|
const glowGeometry = new THREE.SphereGeometry(0.01, 8, 8); |
|
|
const glowMaterial = new THREE.MeshBasicMaterial({ |
|
|
color: 0x22d3ee, |
|
|
transparent: true, |
|
|
opacity: 0.9, |
|
|
blending: THREE.AdditiveBlending |
|
|
}); |
|
|
|
|
|
const glow = new THREE.Mesh(glowGeometry, glowMaterial); |
|
|
laser.add(glow); |
|
|
|
|
|
this.scene.add(laser); |
|
|
this.laserPointers.push({ |
|
|
mesh: laser, |
|
|
controller: controller, |
|
|
visible: false |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async initializeEyeTracking() { |
|
|
console.log('👁️ تهيئة تتبع العين...'); |
|
|
|
|
|
try { |
|
|
|
|
|
this.eyeTrackingData = { |
|
|
leftEye: { x: 0, y: 0, z: -1 }, |
|
|
rightEye: { x: 0, y: 0, z: -1 }, |
|
|
combinedGaze: { x: 0, y: 0, z: -1 } |
|
|
}; |
|
|
|
|
|
|
|
|
this.updateEyeTracking(); |
|
|
|
|
|
console.log('✅ تم تهيئة تتبع العين'); |
|
|
|
|
|
} catch (error) { |
|
|
console.warn('⚠️ تعذر تهيئة تتبع العين:', error); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async startSession() { |
|
|
if (!this.isInitialized) { |
|
|
throw new Error('VR غير مهيأ بعد'); |
|
|
} |
|
|
|
|
|
try { |
|
|
console.log('🚀 بدء جلسة VR...'); |
|
|
|
|
|
|
|
|
this.session = await navigator.xr.requestSession(this.config.sessionType, { |
|
|
requiredFeatures: this.config.requiredFeatures, |
|
|
optionalFeatures: this.config.optionalFeatures |
|
|
}); |
|
|
|
|
|
|
|
|
this.bindSessionEvents(); |
|
|
|
|
|
|
|
|
this.renderer.xr.enabled = true; |
|
|
this.renderer.xr.setReferenceSpaceType('local-floor'); |
|
|
await this.renderer.xr.setSession(this.session); |
|
|
|
|
|
this.isSessionActive = true; |
|
|
this.startSessionLoop(); |
|
|
|
|
|
console.log('✅ تم بدء جلسة VR بنجاح'); |
|
|
this.emit('session_start'); |
|
|
|
|
|
} catch (error) { |
|
|
console.error('❌ خطأ في بدء جلسة VR:', error); |
|
|
throw error; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async endSession() { |
|
|
if (!this.session || !this.isSessionActive) { |
|
|
return; |
|
|
} |
|
|
|
|
|
try { |
|
|
console.log('🛑 إنهاء جلسة VR...'); |
|
|
|
|
|
this.isSessionActive = false; |
|
|
|
|
|
if (this.session) { |
|
|
await this.session.end(); |
|
|
this.session = null; |
|
|
} |
|
|
|
|
|
|
|
|
this.renderer.xr.enabled = false; |
|
|
|
|
|
|
|
|
this.laserPointers.forEach(pointer => { |
|
|
pointer.visible = false; |
|
|
pointer.mesh.visible = false; |
|
|
}); |
|
|
|
|
|
console.log('✅ تم إنهاء جلسة VR'); |
|
|
this.emit('session_end'); |
|
|
|
|
|
} catch (error) { |
|
|
console.error('❌ خطأ في إنهاء جلسة VR:', error); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
bindSessionEvents() { |
|
|
this.session.addEventListener('end', () => { |
|
|
this.isSessionActive = false; |
|
|
this.emit('session_end'); |
|
|
}); |
|
|
|
|
|
this.session.addEventListener('inputsourceschange', (event) => { |
|
|
this.handleInputSourcesChange(event); |
|
|
}); |
|
|
|
|
|
this.session.addEventListener('selectstart', (event) => { |
|
|
this.handleSelectStart(event); |
|
|
}); |
|
|
|
|
|
this.session.addEventListener('selectend', (event) => { |
|
|
this.handleSelectEnd(event); |
|
|
}); |
|
|
|
|
|
this.session.addEventListener('squeezestart', (event) => { |
|
|
this.handleSqueezeStart(event); |
|
|
}); |
|
|
|
|
|
this.session.addEventListener('squeezeend', (event) => { |
|
|
this.handleSqueezeEnd(event); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
handleInputSourcesChange(event) { |
|
|
const { added, removed } = event; |
|
|
|
|
|
added.forEach(inputSource => { |
|
|
console.log('➕ إضافة مصدر إدخال:', inputSource.handedness); |
|
|
this.handleInputSourceAdded(inputSource); |
|
|
}); |
|
|
|
|
|
removed.forEach(inputSource => { |
|
|
console.log('➖ إزالة مصدر إدخال:', inputSource.handedness); |
|
|
this.handleInputSourceRemoved(inputSource); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
handleInputSourceAdded(inputSource) { |
|
|
|
|
|
const controller = this.controllers.find(c => c.handedness === inputSource.handedness); |
|
|
if (controller) { |
|
|
controller.inputSource = inputSource; |
|
|
controller.connected = true; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
handleInputSourceRemoved(inputSource) { |
|
|
const controller = this.controllers.find(c => c.inputSource === inputSource); |
|
|
if (controller) { |
|
|
controller.inputSource = null; |
|
|
controller.connected = false; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
handleSelectStart(event) { |
|
|
const inputSource = event.inputSource; |
|
|
const controller = this.controllers.find(c => c.inputSource === inputSource); |
|
|
|
|
|
if (controller) { |
|
|
console.log('🎯 بدء الاختيار:', controller.handedness); |
|
|
|
|
|
|
|
|
if (this.config.hapticFeedback) { |
|
|
this.triggerHaptic(controller, 0.5, 100); |
|
|
} |
|
|
|
|
|
|
|
|
this.handleControllerSelection(controller, event); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
handleSelectEnd(event) { |
|
|
const inputSource = event.inputSource; |
|
|
const controller = this.controllers.find(c => c.inputSource === inputSource); |
|
|
|
|
|
if (controller) { |
|
|
console.log('🎯 نهاية الاختيار:', controller.handedness); |
|
|
this.currentTarget = null; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
handleSqueezeStart(event) { |
|
|
const inputSource = event.inputSource; |
|
|
const controller = this.controllers.find(c => c.inputSource === inputSource); |
|
|
|
|
|
if (controller) { |
|
|
console.log('✊ بدء الإمساك:', controller.handedness); |
|
|
|
|
|
|
|
|
if (this.config.hapticFeedback) { |
|
|
this.triggerHaptic(controller, 0.8, 200); |
|
|
} |
|
|
|
|
|
this.emit('controller_squeeze', controller); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
handleSqueezeEnd(event) { |
|
|
const inputSource = event.inputSource; |
|
|
const controller = this.controllers.find(c => c.inputSource === inputSource); |
|
|
|
|
|
if (controller) { |
|
|
console.log('✊ نهاية الإمساك:', controller.handedness); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
handleControllerSelection(controller, event) { |
|
|
|
|
|
const intersections = this.raycast(controller); |
|
|
|
|
|
if (intersections.length > 0) { |
|
|
const target = intersections[0].object; |
|
|
this.currentTarget = target; |
|
|
|
|
|
console.log('🎯 تم تحديد هدف:', target.userData); |
|
|
|
|
|
|
|
|
this.emit('controller_select', { |
|
|
controller: controller, |
|
|
target: target, |
|
|
intersection: intersections[0] |
|
|
}); |
|
|
|
|
|
|
|
|
this.showSelectionEffect(target); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
raycast(controller) { |
|
|
|
|
|
const origin = new THREE.Vector3( |
|
|
controller.pose.position.x, |
|
|
controller.pose.position.y, |
|
|
controller.pose.position.z |
|
|
); |
|
|
|
|
|
const direction = new THREE.Vector3(0, 0, -1); |
|
|
direction.applyQuaternion(new THREE.Quaternion( |
|
|
controller.pose.orientation.x, |
|
|
controller.pose.orientation.y, |
|
|
controller.pose.orientation.z, |
|
|
controller.pose.orientation.w |
|
|
)); |
|
|
|
|
|
const raycaster = new THREE.Raycaster(origin, direction); |
|
|
return raycaster.intersectObjects(this.scene.children, true); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async triggerHaptic(controller, intensity = 0.5, duration = 100) { |
|
|
try { |
|
|
const hapticActuator = controller.gamepad?.hapticActuators?.[0]; |
|
|
if (hapticActuator) { |
|
|
await hapticActuator.pulse(intensity, duration); |
|
|
} |
|
|
} catch (error) { |
|
|
console.warn('تعذر تشغيل الاهتزاز:', error); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
showSelectionEffect(target) { |
|
|
|
|
|
const glowGeometry = new THREE.SphereGeometry(0.1, 16, 16); |
|
|
const glowMaterial = new THREE.MeshBasicMaterial({ |
|
|
color: 0x22d3ee, |
|
|
transparent: true, |
|
|
opacity: 0.8, |
|
|
blending: THREE.AdditiveBlending |
|
|
}); |
|
|
|
|
|
const glow = new THREE.Mesh(glowGeometry, glowMaterial); |
|
|
glow.position.copy(target.position); |
|
|
|
|
|
this.scene.add(glow); |
|
|
|
|
|
|
|
|
setTimeout(() => { |
|
|
this.scene.remove(glow); |
|
|
}, 2000); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
startSessionLoop() { |
|
|
const animate = () => { |
|
|
if (!this.isSessionActive) return; |
|
|
|
|
|
|
|
|
this.updateControllers(); |
|
|
|
|
|
|
|
|
this.updateLaserPointers(); |
|
|
|
|
|
|
|
|
this.updateEyeTracking(); |
|
|
|
|
|
|
|
|
this.session.requestAnimationFrame(animate); |
|
|
}; |
|
|
|
|
|
this.session.requestAnimationFrame(animate); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
updateControllers() { |
|
|
this.controllers.forEach((controller, index) => { |
|
|
if (!controller.connected) return; |
|
|
|
|
|
|
|
|
this.simulateControllerMovement(controller); |
|
|
|
|
|
|
|
|
this.updateControllerGamepad(controller); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
simulateControllerMovement(controller) { |
|
|
const time = Date.now() * 0.001; |
|
|
|
|
|
|
|
|
controller.pose.position.x = Math.sin(time * 0.5) * 0.3; |
|
|
controller.pose.position.y = 1.2 + Math.sin(time * 0.8) * 0.1; |
|
|
controller.pose.position.z = Math.cos(time * 0.3) * 0.2; |
|
|
|
|
|
|
|
|
controller.pose.orientation.x = Math.sin(time * 0.3) * 0.1; |
|
|
controller.pose.orientation.y = Math.cos(time * 0.4) * 0.1; |
|
|
controller.pose.orientation.z = Math.sin(time * 0.6) * 0.1; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
updateControllerGamepad(controller) { |
|
|
const time = Date.now() * 0.001; |
|
|
|
|
|
|
|
|
controller.gamepad.axes[0] = Math.sin(time * 0.5) * 0.7; |
|
|
controller.gamepad.axes[1] = Math.cos(time * 0.7) * 0.7; |
|
|
|
|
|
|
|
|
controller.gamepad.buttons.forEach((button, index) => { |
|
|
if (index < 2) { |
|
|
button.pressed = Math.random() > 0.95; |
|
|
button.value = button.pressed ? 1 : 0; |
|
|
} |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
updateLaserPointers() { |
|
|
this.laserPointers.forEach(pointer => { |
|
|
const controller = pointer.controller; |
|
|
|
|
|
if (!controller.connected) { |
|
|
pointer.mesh.visible = false; |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
pointer.mesh.position.set( |
|
|
controller.pose.position.x, |
|
|
controller.pose.position.y, |
|
|
controller.pose.position.z |
|
|
); |
|
|
|
|
|
|
|
|
const quaternion = new THREE.Quaternion( |
|
|
controller.pose.orientation.x, |
|
|
controller.pose.orientation.y, |
|
|
controller.pose.orientation.z, |
|
|
controller.pose.orientation.w |
|
|
); |
|
|
|
|
|
pointer.mesh.setRotationFromQuaternion(quaternion); |
|
|
|
|
|
|
|
|
const intersections = this.raycast(controller); |
|
|
|
|
|
if (intersections.length > 0) { |
|
|
pointer.mesh.visible = true; |
|
|
|
|
|
|
|
|
const intersection = intersections[0]; |
|
|
const distance = intersection.distance; |
|
|
|
|
|
|
|
|
pointer.mesh.scale.z = distance / this.config.laserPointerLength; |
|
|
|
|
|
|
|
|
pointer.mesh.material.color.setHex(0x34d399); |
|
|
} else { |
|
|
pointer.mesh.visible = true; |
|
|
pointer.mesh.scale.z = 1; |
|
|
pointer.mesh.material.color.setHex(0x22d3ee); |
|
|
} |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
updateEyeTracking() { |
|
|
if (!this.eyeTrackingData) return; |
|
|
|
|
|
const time = Date.now() * 0.001; |
|
|
|
|
|
|
|
|
this.eyeTrackingData.leftEye.x = Math.sin(time * 0.3) * 0.1; |
|
|
this.eyeTrackingData.leftEye.y = Math.cos(time * 0.4) * 0.1; |
|
|
this.eyeTrackingData.leftEye.z = -1; |
|
|
|
|
|
this.eyeTrackingData.rightEye.x = Math.sin(time * 0.3) * 0.1; |
|
|
this.eyeTrackingData.rightEye.y = Math.cos(time * 0.4) * 0.1; |
|
|
this.eyeTrackingData.rightEye.z = -1; |
|
|
|
|
|
|
|
|
this.eyeTrackingData.combinedGaze.x = (this.eyeTrackingData.leftEye.x + this.eyeTrackingData.rightEye.x) / 2; |
|
|
this.eyeTrackingData.combinedGaze.y = (this.eyeTrackingData.leftEye.y + this.eyeTrackingData.rightEye.y) / 2; |
|
|
this.eyeTrackingData.combinedGaze.z = -1; |
|
|
|
|
|
|
|
|
this.handleGazeFocus(); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
handleGazeFocus() { |
|
|
|
|
|
const origin = new THREE.Vector3(0, 1.6, 0); |
|
|
const direction = new THREE.Vector3( |
|
|
this.eyeTrackingData.combinedGaze.x, |
|
|
this.eyeTrackingData.combinedGaze.y, |
|
|
this.eyeTrackingData.combinedGaze.z |
|
|
); |
|
|
|
|
|
const raycaster = new THREE.Raycaster(origin, direction); |
|
|
const intersections = raycaster.intersectObjects(this.scene.children, true); |
|
|
|
|
|
if (intersections.length > 0) { |
|
|
const target = intersections[0].object; |
|
|
|
|
|
|
|
|
if (target !== this.gazeTarget) { |
|
|
this.showGazeEffect(target); |
|
|
this.gazeTarget = target; |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
showGazeEffect(target) { |
|
|
|
|
|
const gazeGeometry = new THREE.SphereGeometry(0.05, 8, 8); |
|
|
const gazeMaterial = new THREE.MeshBasicMaterial({ |
|
|
color: 0xffd700, |
|
|
transparent: true, |
|
|
opacity: 0.6, |
|
|
blending: THREE.AdditiveBlending |
|
|
}); |
|
|
|
|
|
const gazePoint = new THREE.Mesh(gazeGeometry, gazeMaterial); |
|
|
gazePoint.position.copy(target.position); |
|
|
|
|
|
this.scene.add(gazePoint); |
|
|
|
|
|
|
|
|
setTimeout(() => { |
|
|
this.scene.remove(gazePoint); |
|
|
}, 1000); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
createMockControllers() { |
|
|
return [ |
|
|
{ |
|
|
id: 'mock_left', |
|
|
handedness: 'left', |
|
|
connected: true, |
|
|
buttons: { |
|
|
trigger: { pressed: false, value: 0 }, |
|
|
grip: { pressed: false, value: 0 }, |
|
|
a: { pressed: false, value: 0 }, |
|
|
b: { pressed: false, value: 0 }, |
|
|
thumbstick: { pressed: false, value: 0 } |
|
|
}, |
|
|
axes: [0, 0, 0, 0] |
|
|
}, |
|
|
{ |
|
|
id: 'mock_right', |
|
|
handedness: 'right', |
|
|
connected: true, |
|
|
buttons: { |
|
|
trigger: { pressed: false, value: 0 }, |
|
|
grip: { pressed: false, value: 0 }, |
|
|
a: { pressed: false, value: 0 }, |
|
|
b: { pressed: false, value: 0 }, |
|
|
thumbstick: { pressed: false, value: 0 } |
|
|
}, |
|
|
axes: [0, 0, 0, 0] |
|
|
} |
|
|
]; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
getControllersState() { |
|
|
return this.controllers.map(controller => ({ |
|
|
id: controller.id, |
|
|
handedness: controller.handedness, |
|
|
connected: controller.connected, |
|
|
position: controller.pose.position, |
|
|
orientation: controller.pose.orientation, |
|
|
buttons: controller.gamepad.buttons.map(btn => ({ |
|
|
pressed: btn.pressed, |
|
|
value: btn.value |
|
|
})), |
|
|
axes: [...controller.gamepad.axes] |
|
|
})); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
simulateInput(controllerId, input) { |
|
|
const controller = this.controllers.find(c => c.id === controllerId); |
|
|
if (!controller) return; |
|
|
|
|
|
console.log('🎮 محاكاة إدخال:', controllerId, input); |
|
|
|
|
|
|
|
|
switch (input.type) { |
|
|
case 'button': |
|
|
controller.gamepad.buttons[input.button].pressed = input.pressed; |
|
|
controller.gamepad.buttons[input.button].value = input.value || (input.pressed ? 1 : 0); |
|
|
break; |
|
|
|
|
|
case 'axis': |
|
|
controller.gamepad.axes[input.axis] = input.value; |
|
|
break; |
|
|
|
|
|
case 'position': |
|
|
controller.pose.position = input.position; |
|
|
break; |
|
|
|
|
|
case 'orientation': |
|
|
controller.pose.orientation = input.orientation; |
|
|
break; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
saveSession() { |
|
|
if (!this.config.saveSessions || !this.isSessionActive) return; |
|
|
|
|
|
try { |
|
|
const sessionData = { |
|
|
timestamp: new Date().toISOString(), |
|
|
controllers: this.getControllersState(), |
|
|
gazeTarget: this.gazeTarget?.userData || null, |
|
|
sceneState: this.captureSceneState() |
|
|
}; |
|
|
|
|
|
localStorage.setItem('vrSession', JSON.stringify(sessionData)); |
|
|
console.log('💾 تم حفظ جلسة VR'); |
|
|
|
|
|
} catch (error) { |
|
|
console.warn('تعذر حفظ الجلسة:', error); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
loadSession() { |
|
|
try { |
|
|
const sessionData = localStorage.getItem('vrSession'); |
|
|
if (!sessionData) return false; |
|
|
|
|
|
const session = JSON.parse(sessionData); |
|
|
console.log('📁 تحميل جلسة VR:', session.timestamp); |
|
|
|
|
|
|
|
|
session.controllers.forEach(savedController => { |
|
|
const controller = this.controllers.find(c => c.id === savedController.id); |
|
|
if (controller) { |
|
|
controller.pose.position = savedController.position; |
|
|
controller.pose.orientation = savedController.orientation; |
|
|
} |
|
|
}); |
|
|
|
|
|
return true; |
|
|
|
|
|
} catch (error) { |
|
|
console.warn('تعذر تحميل الجلسة:', error); |
|
|
return false; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
captureSceneState() { |
|
|
const objects = []; |
|
|
this.scene.traverse((object) => { |
|
|
if (object.userData.threat) { |
|
|
objects.push({ |
|
|
id: object.userData.threat.id, |
|
|
type: object.userData.threat.type, |
|
|
position: object.position.toArray(), |
|
|
rotation: object.rotation.toArray(), |
|
|
scale: object.scale.toArray() |
|
|
}); |
|
|
} |
|
|
}); |
|
|
return objects; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
emit(eventName, data) { |
|
|
if (this.eventListeners[eventName]) { |
|
|
this.eventListeners[eventName].forEach(callback => { |
|
|
try { |
|
|
callback(data); |
|
|
} catch (error) { |
|
|
console.error('خطأ في مستمع الحدث:', error); |
|
|
} |
|
|
}); |
|
|
} |
|
|
} |
|
|
|
|
|
on(eventName, callback) { |
|
|
if (!this.eventListeners[eventName]) { |
|
|
this.eventListeners[eventName] = []; |
|
|
} |
|
|
this.eventListeners[eventName].push(callback); |
|
|
} |
|
|
|
|
|
off(eventName, callback) { |
|
|
if (this.eventListeners[eventName]) { |
|
|
this.eventListeners[eventName] = this.eventListeners[eventName].filter(cb => cb !== callback); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
destroy() { |
|
|
console.log('🗑️ تدمير VR Controller...'); |
|
|
|
|
|
|
|
|
if (this.isSessionActive) { |
|
|
this.endSession(); |
|
|
} |
|
|
|
|
|
|
|
|
this.laserPointers.forEach(pointer => { |
|
|
this.scene.remove(pointer.mesh); |
|
|
}); |
|
|
|
|
|
this.controllers = []; |
|
|
this.laserPointers = []; |
|
|
|
|
|
console.log('✅ تم تدمير VR Controller'); |
|
|
} |
|
|
} |