diff --git a/src/components/three/XRGrabbable.tsx b/src/components/three/XRGrabbable.tsx index 2c10a61..03ace6e 100644 --- a/src/components/three/XRGrabbable.tsx +++ b/src/components/three/XRGrabbable.tsx @@ -6,6 +6,8 @@ import { useControllerGrab, ControllerGrabSnapshot } from '@/hooks/useController interface XRGrabbableProps { /** Allow uniform scaling via two-hand grab (distance between hands) */ allowScale?: boolean; + /** When true, the active model is locked — grab is ignored entirely */ + lockedActive?: boolean; /** Called the first time the user grabs (e.g. to exit placement mode) */ onGrabStart?: () => void; children: ReactNode; @@ -45,7 +47,7 @@ const _tmpQuat = new THREE.Quaternion(); * On release the group keeps its last pose (we don't write to the store — * the manual fine-tuning sliders remain available as additional offsets). */ -export function XRGrabbable({ allowScale = false, onGrabStart, children }: XRGrabbableProps) { +export function XRGrabbable({ allowScale = false, lockedActive = false, onGrabStart, children }: XRGrabbableProps) { const groupRef = useRef(null); const grab = useControllerGrab(); @@ -61,6 +63,14 @@ export function XRGrabbable({ allowScale = false, onGrabStart, children }: XRGra const L = snap.left; const R = snap.right; + // Locked: ignore all grab input — release any in-progress grabs and bail. + if (lockedActive) { + if (single.current) single.current = null; + if (dual.current) dual.current = null; + if (haloRef.current) haloRef.current.visible = false; + return; + } + const lActive = L.isGrabbing && L.hasPose; const rActive = R.isGrabbing && R.hasPose; diff --git a/src/components/three/XRHudInWorld.tsx b/src/components/three/XRHudInWorld.tsx index a202b7a..ce1921b 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,14 +121,29 @@ export function XRHudInWorld(props: XRHudInWorldProps) { setOpen((v) => !v)} /> {open && ( - + setPinned((v) => !v)} + onRecenter={() => { reanchorRequested.current = true; }} + {...props} + /> )} ); } -function FloatingPanel({ tab, setTab, ...p }: { tab: Tab; setTab: (t: Tab) => void } & XRHudInWorldProps) { +function FloatingPanel({ + tab, setTab, pinned, onTogglePin, onRecenter, ...p +}: { + tab: Tab; + setTab: (t: Tab) => void; + pinned: boolean; + onTogglePin: () => void; + onRecenter: () => void; +} & XRHudInWorldProps) { const tabs: { id: Tab; label: string; icon: string }[] = [ { id: 'scene', label: 'Cena', icon: '🧩' }, { id: 'tools', label: 'Ferram.', icon: '🛠' }, @@ -124,10 +158,14 @@ function FloatingPanel({ tab, setTab, ...p }: { tab: Tab; setTab: (t: Tab) => vo anchorX="left" anchorY="middle"> TrackSteelXR · HUD AR - - Botão A (esq) abre/fecha - + {/* Pin / Recenter controls (replace the previous static "A button" hint) */} + + + + {tabs.map((t, i) => ( diff --git a/src/pages/XRSession.tsx b/src/pages/XRSession.tsx index 56e29f9..7abd1b7 100644 --- a/src/pages/XRSession.tsx +++ b/src/pages/XRSession.tsx @@ -172,9 +172,13 @@ function XRActiveModel() { function ControllerFineTuning({ freeMove }: { freeMove: boolean }) { const { setFineTuning } = useModelStore(); const session = useXR((s) => s.session); + const isActiveLocked = useModelStore((s) => { + const a = s.models.find(m => m.id === s.activeModelId); + return !!a?.locked; + }); useFrame(() => { - if (!session) return; + if (!session || isActiveLocked) return; const inputSources = session.inputSources; if (!inputSources) return; @@ -374,63 +378,14 @@ function XRGrid() { // ImageTrackingAnchor removed — replaced by XRHitTestPlacement -// ─── XR Debug HUD (floats in front of viewer) ────────── -// Shows live grip/trigger values and isGrabbing state per hand. -// Essential for diagnosing grab issues without external monitoring. -function XRDebugHud() { - const session = useXR((s) => s.session); - const groupRef = useRef(null); - const { camera } = useThree(); - const [text, setText] = useState('XR aguardando inputs...'); - - useFrame(() => { - if (!groupRef.current) return; - // Anchor the HUD to a fixed offset in front of the camera - const offset = new THREE.Vector3(-0.25, -0.18, -0.6).applyQuaternion(camera.quaternion); - groupRef.current.position.copy(camera.position).add(offset); - groupRef.current.quaternion.copy(camera.quaternion); - - if (!session) return; - const sources = Array.from(session.inputSources); - const lines: string[] = [`inputs: ${sources.length}`]; - for (const src of sources) { - const gp = src.gamepad; - const grip = gp?.buttons[2]; - const trig = gp?.buttons[1]; - const gv = grip ? (grip.value || (grip.pressed ? 1 : 0)) : 0; - const tv = trig ? (trig.value || (trig.pressed ? 1 : 0)) : 0; - const grabbing = gv > 0.7 ? 'GRAB' : gv > 0.3 ? '...' : ' '; - lines.push(`${src.handedness.padEnd(5)} grip:${gv.toFixed(2)} trg:${tv.toFixed(2)} ${grabbing}`); - } - setText(lines.join('\n')); - }); - - return ( - - - - - - - {text} - - - ); -} - - // ─── XRSession Page ──────────────────────────────────── const XRSession = () => { const navigate = useNavigate(); const { model, anchorMode, setAnchorMode } = useModelStore(); + const isActiveLocked = useModelStore((s) => { + const a = s.models.find(m => m.id === s.activeModelId); + return !!a?.locked; + }); const [inXR, setInXR] = useState(false); const [freeMove, setFreeMove] = useState(true); const [placementMode, setPlacementMode] = useState(true); // start in placement mode @@ -538,12 +493,15 @@ const XRSession = () => { {effectiveInXR ? ( <> - {/* In XR (or SimXR): grab wrapper → ACTIVE model. Other models render as static background. */} - + {/* In XR (or SimXR): all models share the same placement origin so + switching active doesn't make others appear to "disappear". + Only the ACTIVE model receives grab transforms. */} {simXR ? ( + { if (placementMode) setPlacementMode(false); }} > @@ -558,8 +516,10 @@ const XRSession = () => { toast.success('Modelo posicionado na superfície!'); }} > + { if (placementMode) setPlacementMode(false); }} > @@ -569,7 +529,7 @@ const XRSession = () => { - + {/* In-world HUD — visible inside passthrough where DOM overlays cannot reach */}