fix: decouple hand tracking from crosshair focus

This commit is contained in:
Tom Boullay
2026-04-29 11:13:11 +02:00
parent fffabc01c2
commit 5b14a1d971
5 changed files with 73 additions and 6 deletions
+18 -1
View File
@@ -23,6 +23,11 @@ import {
import { INTERACTION_RADIUS } from "@/data/interaction/interactionConfig"; import { INTERACTION_RADIUS } from "@/data/interaction/interactionConfig";
import { useDebugFolder } from "@/hooks/debug/useDebugFolder"; import { useDebugFolder } from "@/hooks/debug/useDebugFolder";
import { useHandTrackingSnapshot } from "@/hooks/useHandTrackingSnapshot"; import { useHandTrackingSnapshot } from "@/hooks/useHandTrackingSnapshot";
import { InteractionManager } from "@/managers/InteractionManager";
import type {
HandTrackingHand,
HandTrackingLandmark,
} from "@/types/handTracking";
import type { ColliderShape, Vector3Tuple } from "@/types/three"; import type { ColliderShape, Vector3Tuple } from "@/types/three";
interface GrabbableObjectProps { interface GrabbableObjectProps {
@@ -51,6 +56,14 @@ const _cameraPos = new THREE.Vector3();
const _objectPos = new THREE.Vector3(); const _objectPos = new THREE.Vector3();
const _handRaycaster = new THREE.Raycaster(); const _handRaycaster = new THREE.Raycaster();
function getHandAnchorPoint(hand: HandTrackingHand): HandTrackingLandmark {
return hand.landmarks.reduce<HandTrackingLandmark>(
(lowestPoint, landmark) =>
landmark.y > lowestPoint.y ? landmark : lowestPoint,
{ x: hand.x, y: hand.y, z: hand.z },
);
}
export function GrabbableObject({ export function GrabbableObject({
position, position,
children, children,
@@ -107,7 +120,9 @@ export function GrabbableObject({
_currentPos.set(t.x, t.y, t.z); _currentPos.set(t.x, t.y, t.z);
if (fistHand) { if (fistHand) {
_handNdc.set((1 - fistHand.x) * 2 - 1, -fistHand.y * 2 + 1, 0.5); const handAnchor = getHandAnchorPoint(fistHand);
_handNdc.set((1 - handAnchor.x) * 2 - 1, -handAnchor.y * 2 + 1, 0.5);
_handNdc.unproject(camera); _handNdc.unproject(camera);
camera.getWorldPosition(_cameraPos); camera.getWorldPosition(_cameraPos);
_handDirection.subVectors(_handNdc, _cameraPos).normalize(); _handDirection.subVectors(_handNdc, _cameraPos).normalize();
@@ -127,10 +142,12 @@ export function GrabbableObject({
handHoldDistance.current = isHandHolding.current handHoldDistance.current = isHandHolding.current
? hits[0].distance ? hits[0].distance
: null; : null;
InteractionManager.getInstance().setHandHolding(isHandHolding.current);
} }
} else { } else {
isHandHolding.current = false; isHandHolding.current = false;
handHoldDistance.current = null; handHoldDistance.current = null;
InteractionManager.getInstance().setHandHolding(false);
} }
if (!isHolding.current && !isHandHolding.current) return; if (!isHolding.current && !isHandHolding.current) return;
+19 -2
View File
@@ -74,6 +74,7 @@ export function InteractableObject(
useEffect(() => { useEffect(() => {
const currentHandle = handle.current; const currentHandle = handle.current;
const manager = InteractionManager.getInstance();
if (currentHandle.kind === kind) { if (currentHandle.kind === kind) {
currentHandle.label = label; currentHandle.label = label;
@@ -87,6 +88,8 @@ export function InteractableObject(
return; return;
} }
manager.setNearby(currentHandle, false);
if (kind === "grab") { if (kind === "grab") {
if (!onRelease) return; if (!onRelease) return;
handle.current = { kind, label, onPress, onRelease }; handle.current = { kind, label, onPress, onRelease };
@@ -94,12 +97,23 @@ export function InteractableObject(
handle.current = { kind, label, onPress }; handle.current = { kind, label, onPress };
} }
const manager = InteractionManager.getInstance();
if (manager.getState().focused === currentHandle) { if (manager.getState().focused === currentHandle) {
manager.setFocused(handle.current); manager.setFocused(handle.current);
} }
}, [kind, label, onPress, onRelease]); }, [kind, label, onPress, onRelease]);
useEffect(() => {
const currentHandle = handle.current;
return () => {
const manager = InteractionManager.getInstance();
manager.setNearby(currentHandle, false);
if (manager.getState().focused === currentHandle) {
manager.setFocused(null);
}
};
}, []);
const setupInteractionDebugFolder = useCallback((folder: GUI) => { const setupInteractionDebugFolder = useCallback((folder: GUI) => {
folder folder
.add({ radius: INTERACTION_RADIUS }, "radius") .add({ radius: INTERACTION_RADIUS }, "radius")
@@ -128,8 +142,11 @@ export function InteractableObject(
camera.getWorldPosition(_cameraPos); camera.getWorldPosition(_cameraPos);
const dist = _cameraPos.distanceTo(_objectPos); const dist = _cameraPos.distanceTo(_objectPos);
const isNearby = dist <= INTERACTION_RADIUS;
if (dist > INTERACTION_RADIUS) { manager.setNearby(handle.current, isNearby);
if (!isNearby) {
if (manager.getState().focused === handle.current) { if (manager.getState().focused === handle.current) {
manager.setFocused(null); manager.setFocused(null);
} }
+4 -3
View File
@@ -14,10 +14,11 @@ export function HandTrackingProvider({
children: ReactNode; children: ReactNode;
}): React.JSX.Element { }): React.JSX.Element {
const sceneMode = useSceneMode(); const sceneMode = useSceneMode();
const { focused, holding } = useInteraction(); const { nearby, holding, handHolding } = useInteraction();
const isInInteractionZone = focused !== null || holding;
const enabled = const enabled =
isDebugEnabled() && sceneMode === "physics" && isInInteractionZone; isDebugEnabled() &&
sceneMode === "physics" &&
(nearby || holding || handHolding);
const snapshot = useRemoteHandTracking({ enabled }); const snapshot = useRemoteHandTracking({ enabled });
return ( return (
+30
View File
@@ -8,11 +8,15 @@ export class InteractionManager {
private static _instance: InteractionManager | null = null; private static _instance: InteractionManager | null = null;
private _focused: InteractableHandle | null = null; private _focused: InteractableHandle | null = null;
private readonly _nearbyHandles = new Set<InteractableHandle>();
private _holding = false; private _holding = false;
private _handHolding = false;
private _holdingHandle: GrabInteractableHandle | null = null; private _holdingHandle: GrabInteractableHandle | null = null;
private _snapshot: InteractionSnapshot = { private _snapshot: InteractionSnapshot = {
focused: null, focused: null,
nearby: false,
holding: false, holding: false,
handHolding: false,
}; };
private readonly _listeners = new Set<() => void>(); private readonly _listeners = new Set<() => void>();
@@ -38,6 +42,26 @@ export class InteractionManager {
this._emit(); this._emit();
} }
setNearby(handle: InteractableHandle, nearby: boolean): void {
const hadHandle = this._nearbyHandles.has(handle);
if (nearby === hadHandle) return;
if (nearby) {
this._nearbyHandles.add(handle);
} else {
this._nearbyHandles.delete(handle);
}
this._emit();
}
setHandHolding(holding: boolean): void {
if (this._handHolding === holding) return;
this._handHolding = holding;
this._emit();
}
pressInteract(): void { pressInteract(): void {
if (!this._focused) return; if (!this._focused) return;
@@ -73,11 +97,15 @@ export class InteractionManager {
destroy(): void { destroy(): void {
this._focused = null; this._focused = null;
this._nearbyHandles.clear();
this._holding = false; this._holding = false;
this._handHolding = false;
this._holdingHandle = null; this._holdingHandle = null;
this._snapshot = { this._snapshot = {
focused: null, focused: null,
nearby: false,
holding: false, holding: false,
handHolding: false,
}; };
this._listeners.clear(); this._listeners.clear();
InteractionManager._instance = null; InteractionManager._instance = null;
@@ -86,7 +114,9 @@ export class InteractionManager {
private _emit(): void { private _emit(): void {
this._snapshot = { this._snapshot = {
focused: this._focused, focused: this._focused,
nearby: this._nearbyHandles.size > 0,
holding: this._holding, holding: this._holding,
handHolding: this._handHolding,
}; };
this._listeners.forEach((cb) => cb()); this._listeners.forEach((cb) => cb());
} }
+2
View File
@@ -19,5 +19,7 @@ export type InteractableHandle =
export interface InteractionSnapshot { export interface InteractionSnapshot {
focused: InteractableHandle | null; focused: InteractableHandle | null;
nearby: boolean;
holding: boolean; holding: boolean;
handHolding: boolean;
} }