Ai_Assistant/client/vr/vrTouchDetector.js

191 lines
5.9 KiB
JavaScript
Raw Permalink Normal View History

2026-05-24 13:31:30 +02:00
import * as THREE from 'three';
import { VR_CONFIG } from '../config.js';
import { mapBoneToRegion, sendClickInteraction } from '../interaction/touchRegionMap.js';
import { pulseXRController } from './xrHaptics.js';
/**
* VRTouchDetector - Proximity-based touch detection on VRM avatar from VR controllers.
*
* Primary: auto-triggers when a controller hand gets close enough to a VRM bone.
* Secondary: trigger button extends the detection range for intentional interaction.
* Provides haptic feedback and updates hand indicator visuals based on proximity.
*/
export class VRTouchDetector {
constructor(renderer, vrm, { controllerGrips, handIndicators }, { onTouch } = {}) {
this.renderer = renderer;
this.vrm = vrm;
this.controllerGrips = controllerGrips;
this.handIndicators = handIndicators;
this.onTouch = onTouch || null;
// Config
this.touchRadius = VR_CONFIG.touchRadius;
this.triggerTouchRadius = VR_CONFIG.triggerTouchRadius;
this.cooldown = VR_CONFIG.touchCooldown;
this.hapticIntensity = VR_CONFIG.hapticIntensity;
this.hapticDuration = VR_CONFIG.hapticDuration;
// Per-controller state
this.lastTouchTime = [0, 0];
this.activeTouches = [false, false];
// Cache VRM bone references for proximity checks
this.bones = this._cacheBones();
// Reusable vectors (avoid GC pressure in the update loop)
this._gripPos = new THREE.Vector3();
this._bonePos = new THREE.Vector3();
// Color constants for hand indicators
this._colorIdle = new THREE.Color(0x88ccff);
this._colorNear = new THREE.Color(0xffcc44);
this._colorTouch = new THREE.Color(0x44ff88);
this._enabled = false;
console.log(`VR Touch Detector initialized (${this.bones.length} bones cached)`);
}
enable() { this._enabled = true; }
disable() { this._enabled = false; }
_cacheBones() {
const bones = [];
const humanoid = this.vrm.humanoid;
if (!humanoid) return bones;
const boneNames = [
'head', 'neck', 'upperChest', 'chest', 'spine', 'hips',
'leftShoulder', 'leftUpperArm', 'leftLowerArm', 'leftHand',
'rightShoulder', 'rightUpperArm', 'rightLowerArm', 'rightHand',
'leftUpperLeg', 'leftLowerLeg', 'leftFoot',
'rightUpperLeg', 'rightLowerLeg', 'rightFoot',
];
for (const humanoidName of boneNames) {
const node = humanoid.getNormalizedBoneNode(humanoidName);
if (node) {
bones.push({ name: node.name, node });
}
}
return bones;
}
/** Call every frame from the animate loop */
update() {
if (!this._enabled) return;
const now = Date.now();
for (let i = 0; i < this.controllerGrips.length; i++) {
const grip = this.controllerGrips[i];
if (!grip) continue;
// Get controller grip world position
grip.getWorldPosition(this._gripPos);
// Find closest bone
const closest = this._findClosestBone(this._gripPos);
// Update hand indicator visuals
this._updateHandVisual(i, closest.distance);
// Check proximity touch (only if cooldown elapsed)
if (now - this.lastTouchTime[i] >= this.cooldown) {
if (closest.distance < this.touchRadius && !this.activeTouches[i]) {
this._triggerTouch(i, closest.bone, closest.distance, now);
}
}
// Track whether controller is currently in touch zone
this.activeTouches[i] = closest.distance < this.touchRadius;
}
}
/**
* Handle trigger (select) button press - extends touch range.
* Called from vrManager when trigger is pressed.
*/
handleTrigger(controllerIndex) {
if (!this._enabled) return;
const grip = this.controllerGrips[controllerIndex];
if (!grip) return;
const now = Date.now();
if (now - this.lastTouchTime[controllerIndex] < this.cooldown) return;
grip.getWorldPosition(this._gripPos);
const closest = this._findClosestBone(this._gripPos);
if (closest.distance < this.triggerTouchRadius && closest.bone) {
this._triggerTouch(controllerIndex, closest.bone, closest.distance, now);
}
}
_findClosestBone(position) {
let closestBone = null;
let closestDist = Infinity;
for (const bone of this.bones) {
bone.node.getWorldPosition(this._bonePos);
const dist = position.distanceTo(this._bonePos);
if (dist < closestDist) {
closestDist = dist;
closestBone = bone;
}
}
return { bone: closestBone, distance: closestDist };
}
_triggerTouch(controllerIndex, bone, distance, now) {
this.activeTouches[controllerIndex] = true;
this.lastTouchTime[controllerIndex] = now;
const region = mapBoneToRegion(bone.name);
console.log(`VR Touch: Hand ${controllerIndex} -> ${bone.name} (${region}, ${distance.toFixed(3)}m)`);
sendClickInteraction(bone.name, region);
this._haptic(controllerIndex);
if (this.onTouch) {
this.onTouch(region, bone.name, controllerIndex);
}
}
_haptic(controllerIndex) {
pulseXRController(this.renderer, controllerIndex, this.hapticIntensity, this.hapticDuration);
}
_updateHandVisual(controllerIndex, distance) {
const indicator = this.handIndicators[controllerIndex];
if (!indicator) return;
const { sphere, glow } = indicator.userData;
if (!sphere || !glow) return;
let color;
let glowOpacity;
if (distance < this.touchRadius) {
// Touching - green
color = this._colorTouch;
glowOpacity = 0.35;
} else if (distance < this.triggerTouchRadius) {
// Near - interpolate from idle to near color
const t = 1.0 - (distance - this.touchRadius) / (this.triggerTouchRadius - this.touchRadius);
color = this._colorIdle.clone().lerp(this._colorNear, t);
glowOpacity = 0.1 + t * 0.15;
} else {
// Idle - blue
color = this._colorIdle;
glowOpacity = 0.1;
}
sphere.material.color.copy(color);
glow.material.color.copy(color);
glow.material.opacity = glowOpacity;
}
}