UNPKG

@neuroequality/neuroadapt-vr

Version:

WebXR safe spaces and comfort zones for NeuroAdapt SDK

366 lines (361 loc) 11.3 kB
import { EventEmitter } from 'eventemitter3'; class SafeZoneManager extends EventEmitter { constructor(preferences) { super(); this.zones = /* @__PURE__ */ new Map(); this.currentPosition = { x: 0, y: 0, z: 0 }; this.proximityCheckInterval = null; this.lastWarnings = /* @__PURE__ */ new Set(); this.preferences = preferences; this.startProximityMonitoring(); } createSafeZone(config) { const id = this.generateZoneId(); const zone = { id, ...config }; this.zones.set(id, zone); this.emit("safe-zone-created", zone); return zone; } removeSafeZone(id) { const removed = this.zones.delete(id); if (removed) { this.emit("safe-zone-removed", { id }); this.lastWarnings.delete(id); } return removed; } updateUserPosition(position) { this.currentPosition = position; this.checkProximity(); } createComfortZone(center = this.currentPosition, radius) { const comfortRadius = radius || this.preferences.comfortRadius; return this.createSafeZone({ center, radius: comfortRadius, shape: "sphere", isActive: true, visualIndicator: true, hapticFeedback: true }); } createPersonalSpace(center = this.currentPosition) { return this.createSafeZone({ center, radius: this.preferences.personalSpace, shape: "sphere", isActive: true, visualIndicator: false, hapticFeedback: true }); } activatePanicMode() { if (!this.preferences.panicButtonEnabled) return; const panicZone = this.createSafeZone({ center: this.currentPosition, radius: 2, // 2 meter radius shape: "sphere", isActive: true, visualIndicator: true, hapticFeedback: true }); this.emit("panic-activated", { timestamp: Date.now(), position: this.currentPosition }); setTimeout(() => { this.removeSafeZone(panicZone.id); }, 3e4); } startProximityMonitoring() { this.proximityCheckInterval = window.setInterval(() => { this.checkProximity(); }, 100); } checkProximity() { for (const zone of this.zones.values()) { if (!zone.isActive) continue; const distance = this.calculateDistance(this.currentPosition, zone); const isInside = distance <= zone.radius; const isWarningZone = distance <= zone.radius * 1.2; const wasWarning = this.lastWarnings.has(zone.id); if (isInside) { if (!wasWarning) { const event = { type: "enter", zone, distance, timestamp: Date.now() }; this.emit("zone-entered", event); } this.lastWarnings.add(zone.id); } else if (isWarningZone) { if (!wasWarning) { const event = { type: "warning", zone, distance, timestamp: Date.now() }; this.emit("proximity-warning", event); } this.lastWarnings.add(zone.id); } else { if (wasWarning) { const event = { type: "exit", zone, distance, timestamp: Date.now() }; this.emit("zone-exited", event); this.lastWarnings.delete(zone.id); } } } } calculateDistance(pos1, zone) { const dx = pos1.x - zone.center.x; const dy = pos1.y - zone.center.y; const dz = pos1.z - zone.center.z; switch (zone.shape) { case "sphere": return Math.sqrt(dx * dx + dy * dy + dz * dz); case "cylinder": const radialDistance = Math.sqrt(dx * dx + dz * dz); const heightDiff = Math.abs(dy); if (zone.dimensions) { if (heightDiff > zone.dimensions.y / 2) { return Math.max(radialDistance, heightDiff - zone.dimensions.y / 2); } } return radialDistance; case "box": if (!zone.dimensions) return Math.sqrt(dx * dx + dy * dy + dz * dz); const halfWidth = zone.dimensions.x / 2; const halfHeight = zone.dimensions.y / 2; const halfDepth = zone.dimensions.z / 2; const distX = Math.max(0, Math.abs(dx) - halfWidth); const distY = Math.max(0, Math.abs(dy) - halfHeight); const distZ = Math.max(0, Math.abs(dz) - halfDepth); return Math.sqrt(distX * distX + distY * distY + distZ * distZ); default: return Math.sqrt(dx * dx + dy * dy + dz * dz); } } generateZoneId() { return `zone_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } getZones() { return Array.from(this.zones.values()); } getZone(id) { return this.zones.get(id); } updateZone(id, updates) { const zone = this.zones.get(id); if (!zone) return false; Object.assign(zone, updates); return true; } updatePreferences(preferences) { this.preferences = preferences; } getCurrentPosition() { return { ...this.currentPosition }; } dispose() { if (this.proximityCheckInterval) { clearInterval(this.proximityCheckInterval); this.proximityCheckInterval = null; } this.zones.clear(); this.lastWarnings.clear(); this.removeAllListeners(); } } class ProximityDetector extends EventEmitter { constructor(options = {}) { super(); this.isActive = false; this.updateInterval = null; this.positionHistory = []; this.velocitySmoothing = { x: 0, y: 0, z: 0 }; this.lastVelocity = { x: 0, y: 0, z: 0 }; this.currentPosition = { x: 0, y: 0, z: 0 }; this.currentControllers = []; this.options = { updateInterval: 16, // ~60fps warningThreshold: 0.5, // 50cm warning criticalThreshold: 0.2, // 20cm critical smoothingFactor: 0.8, predictionTime: 0.5, // 500ms prediction ...options }; } start() { if (this.isActive) return; this.isActive = true; this.updateInterval = window.setInterval(() => { this.update(); }, this.options.updateInterval); } stop() { if (!this.isActive) return; this.isActive = false; if (this.updateInterval) { clearInterval(this.updateInterval); this.updateInterval = null; } } updatePosition(position, controllers = []) { const timestamp = Date.now(); this.positionHistory.push({ position: { ...position }, timestamp }); if (this.positionHistory.length > 30) { this.positionHistory.shift(); } this.updateVelocity(); this.currentPosition = position; this.currentControllers = controllers; } update() { } updateVelocity() { if (this.positionHistory.length < 2) return; const latest = this.positionHistory[this.positionHistory.length - 1]; const previous = this.positionHistory[this.positionHistory.length - 2]; if (!latest || !previous) return; const deltaTime = (latest.timestamp - previous.timestamp) / 1e3; if (deltaTime <= 0) return; const instantVelocity = { x: (latest.position.x - previous.position.x) / deltaTime, y: (latest.position.y - previous.position.y) / deltaTime, z: (latest.position.z - previous.position.z) / deltaTime }; this.lastVelocity = { x: this.options.smoothingFactor * this.lastVelocity.x + (1 - this.options.smoothingFactor) * instantVelocity.x, y: this.options.smoothingFactor * this.lastVelocity.y + (1 - this.options.smoothingFactor) * instantVelocity.y, z: this.options.smoothingFactor * this.lastVelocity.z + (1 - this.options.smoothingFactor) * instantVelocity.z }; } checkProximity(zones) { const events = []; for (const zone of zones) { if (!zone.isActive) continue; const currentDistance = this.calculateDistance(this.currentPosition, zone); const currentEvent = this.evaluateProximity(zone, currentDistance); if (currentEvent) { events.push(currentEvent); } const predictedPosition = this.predictFuturePosition(this.options.predictionTime); const predictedDistance = this.calculateDistance(predictedPosition, zone); if (predictedDistance < zone.radius && currentDistance > zone.radius) { const warningEvent = { type: "warning", zone, distance: predictedDistance, timestamp: Date.now() }; events.push(warningEvent); } for (const controller of this.currentControllers) { if (!controller.connected) continue; const controllerDistance = this.calculateDistance(controller.position, zone); const controllerEvent = this.evaluateProximity(zone, controllerDistance); if (controllerEvent) { events.push({ ...controllerEvent // Add controller ID to event data if needed }); } } } return events; } evaluateProximity(zone, distance) { const timestamp = Date.now(); if (distance <= zone.radius) { return { type: "enter", zone, distance, timestamp }; } else if (distance <= zone.radius + this.options.warningThreshold) { return { type: "warning", zone, distance, timestamp }; } return null; } calculateDistance(position, zone) { const dx = position.x - zone.center.x; const dy = position.y - zone.center.y; const dz = position.z - zone.center.z; switch (zone.shape) { case "sphere": return Math.sqrt(dx * dx + dy * dy + dz * dz); case "cylinder": const radialDistance = Math.sqrt(dx * dx + dz * dz); const heightDiff = Math.abs(dy); if (zone.dimensions) { const halfHeight2 = zone.dimensions.y / 2; if (heightDiff > halfHeight2) { return Math.sqrt(radialDistance * radialDistance + (heightDiff - halfHeight2) ** 2); } } return radialDistance; case "box": if (!zone.dimensions) return Math.sqrt(dx * dx + dy * dy + dz * dz); const halfWidth = zone.dimensions.x / 2; const halfHeight = zone.dimensions.y / 2; const halfDepth = zone.dimensions.z / 2; const distX = Math.max(0, Math.abs(dx) - halfWidth); const distY = Math.max(0, Math.abs(dy) - halfHeight); const distZ = Math.max(0, Math.abs(dz) - halfDepth); return Math.sqrt(distX * distX + distY * distY + distZ * distZ); default: return Math.sqrt(dx * dx + dy * dy + dz * dz); } } predictFuturePosition(timeAhead) { return { x: this.currentPosition.x + this.lastVelocity.x * timeAhead, y: this.currentPosition.y + this.lastVelocity.y * timeAhead, z: this.currentPosition.z + this.lastVelocity.z * timeAhead }; } getVelocity() { return { ...this.lastVelocity }; } getSpeed() { const vel = this.lastVelocity; return Math.sqrt(vel.x * vel.x + vel.y * vel.y + vel.z * vel.z); } isMoving(threshold = 0.1) { return this.getSpeed() > threshold; } getPositionHistory() { return [...this.positionHistory]; } dispose() { this.stop(); this.positionHistory = []; this.removeAllListeners(); } } const VERSION = "1.1.0"; export { ProximityDetector, SafeZoneManager, VERSION }; //# sourceMappingURL=index.es.js.map