diff --git a/src/components/three/XRHudInWorld.tsx b/src/components/three/XRHudInWorld.tsx index 5c6bf98..48f5bbe 100644 --- a/src/components/three/XRHudInWorld.tsx +++ b/src/components/three/XRHudInWorld.tsx @@ -51,10 +51,13 @@ const WristToggle = forwardRef voi export function XRHudInWorld(props: XRHudInWorldProps) { const { camera } = useThree(); const leftCtrl = useXRInputSourceState('controller', 'left'); + const rightCtrl = useXRInputSourceState('controller', 'right'); 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); + /** While true, the panel is glued to the right controller (drag mode). */ + const [dragging, setDragging] = useState(false); /** Forces re-anchor on next frame (used by "trazer" / unpin→pin). */ const reanchorRequested = useRef(true); @@ -63,6 +66,10 @@ export function XRHudInWorld(props: XRHudInWorldProps) { const targetPos = useRef(new THREE.Vector3()); const targetQuat = useRef(new THREE.Quaternion()); const lastABtn = useRef(false); + /** Cached offset from controller to panel at drag start. */ + const dragOffsetPos = useRef(new THREE.Vector3()); + const dragOffsetQuat = useRef(new THREE.Quaternion()); + const dragInitialized = useRef(false); useFrame(() => { // Toggle via left A button (Quest Touch buttons[4]) @@ -79,24 +86,47 @@ export function XRHudInWorld(props: XRHudInWorldProps) { // • 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); - const wantPos = new THREE.Vector3().copy(camera.position).add(fwd); - const euler = new THREE.Euler().setFromQuaternion(camera.quaternion, 'YXZ'); - const wantQuat = new THREE.Quaternion().setFromEuler(new THREE.Euler(0, euler.y, 0, 'YXZ')); + // Drag mode → panel rigidly follows right controller using captured offset. + if (dragging) { + const ctrlObj = rightCtrl?.object; + if (ctrlObj) { + ctrlObj.updateMatrixWorld(); + const cPos = new THREE.Vector3().setFromMatrixPosition(ctrlObj.matrixWorld); + const cQuat = new THREE.Quaternion().setFromRotationMatrix(ctrlObj.matrixWorld); + if (!dragInitialized.current) { + // Capture current panel pose relative to controller + const invQ = cQuat.clone().invert(); + dragOffsetPos.current + .copy(panelRef.current.position).sub(cPos).applyQuaternion(invQ); + dragOffsetQuat.current.copy(invQ).multiply(panelRef.current.quaternion); + dragInitialized.current = true; + } + const newPos = dragOffsetPos.current.clone().applyQuaternion(cQuat).add(cPos); + const newQuat = cQuat.clone().multiply(dragOffsetQuat.current); + panelRef.current.position.copy(newPos); + panelRef.current.quaternion.copy(newQuat); + } + } else { + if (dragInitialized.current) dragInitialized.current = false; + const fwd = new THREE.Vector3(0, -0.05, -0.7).applyQuaternion(camera.quaternion); + const wantPos = new THREE.Vector3().copy(camera.position).add(fwd); + const euler = new THREE.Euler().setFromQuaternion(camera.quaternion, 'YXZ'); + 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); + 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. } - // pinned + already anchored → leave panel where it is. } // Wrist toggle follows left controller @@ -126,6 +156,8 @@ export function XRHudInWorld(props: XRHudInWorldProps) { setTab={setTab} pinned={pinned} onTogglePin={() => setPinned((v) => !v)} + dragging={dragging} + onToggleDrag={() => setDragging((v) => !v)} onRecenter={() => { reanchorRequested.current = true; }} {...props} /> @@ -136,12 +168,14 @@ export function XRHudInWorld(props: XRHudInWorldProps) { } function FloatingPanel({ - tab, setTab, pinned, onTogglePin, onRecenter, ...p + tab, setTab, pinned, onTogglePin, dragging, onToggleDrag, onRecenter, ...p }: { tab: Tab; setTab: (t: Tab) => void; pinned: boolean; onTogglePin: () => void; + dragging: boolean; + onToggleDrag: () => void; onRecenter: () => void; } & XRHudInWorldProps) { const tabs: { id: Tab; label: string; icon: string }[] = [ @@ -158,13 +192,20 @@ function FloatingPanel({ anchorX="left" anchorY="middle"> TrackSteelXR · HUD AR - {/* Pin / Recenter controls (replace the previous static "A button" hint) */} - - + {/* Trazer (recenter) — rightmost, half-width 0.029 from origin */} + + {/* Mover (drag with right controller) */} + + {/* Pin */} + -