diff --git a/src/components/three/XRHudInWorld.tsx b/src/components/three/XRHudInWorld.tsx index a202b7a..95a26a2 100644 --- a/src/components/three/XRHudInWorld.tsx +++ b/src/components/three/XRHudInWorld.tsx @@ -53,6 +53,10 @@ export function XRHudInWorld(props: XRHudInWorldProps) { const leftCtrl = useXRInputSourceState('controller', 'left'); const [open, setOpen] = useState(true); const [tab, setTab] = useState('scene'); + /** When pinned, the panel stays fixed in world space and does NOT follow the head. */ + const [pinned, setPinned] = useState(true); + /** Forces re-anchor on next frame (used by "trazer" / unpin→pin). */ + const reanchorRequested = useRef(true); const panelRef = useRef(null); const wristRef = useRef(null); @@ -70,14 +74,29 @@ export function XRHudInWorld(props: XRHudInWorldProps) { lastABtn.current = pressed; } - // Floating panel ~70cm in front of head, yaw-locked, damped + // Floating panel: + // • If pinned and already anchored once → don't move it (fixed in space). + // • If unpinned → continuously follow head with damping. + // • On (re)anchor request → snap target once and stop. if (panelRef.current && open) { const fwd = new THREE.Vector3(0, -0.05, -0.7).applyQuaternion(camera.quaternion); - targetPos.current.copy(camera.position).add(fwd); + const wantPos = new THREE.Vector3().copy(camera.position).add(fwd); const euler = new THREE.Euler().setFromQuaternion(camera.quaternion, 'YXZ'); - targetQuat.current.setFromEuler(new THREE.Euler(0, euler.y, 0, 'YXZ')); - panelRef.current.position.lerp(targetPos.current, 0.12); - panelRef.current.quaternion.slerp(targetQuat.current, 0.18); + const wantQuat = new THREE.Quaternion().setFromEuler(new THREE.Euler(0, euler.y, 0, 'YXZ')); + + if (reanchorRequested.current) { + targetPos.current.copy(wantPos); + targetQuat.current.copy(wantQuat); + panelRef.current.position.copy(wantPos); + panelRef.current.quaternion.copy(wantQuat); + reanchorRequested.current = false; + } else if (!pinned) { + targetPos.current.copy(wantPos); + targetQuat.current.copy(wantQuat); + panelRef.current.position.lerp(targetPos.current, 0.12); + panelRef.current.quaternion.slerp(targetQuat.current, 0.18); + } + // pinned + already anchored → leave panel where it is. } // Wrist toggle follows left controller @@ -102,7 +121,14 @@ export function XRHudInWorld(props: XRHudInWorldProps) { setOpen((v) => !v)} /> {open && ( - + setPinned((v) => !v)} + onRecenter={() => { reanchorRequested.current = true; }} + {...props} + /> )}