diff --git a/src/components/three/XRHudInWorld.tsx b/src/components/three/XRHudInWorld.tsx index e8c114c..fbf52f2 100644 --- a/src/components/three/XRHudInWorld.tsx +++ b/src/components/three/XRHudInWorld.tsx @@ -53,11 +53,15 @@ const WristToggle = forwardRef voi ); export function XRHudInWorld(props: XRHudInWorldProps) { - const { camera } = useThree(); + const { camera, gl } = useThree(); const leftCtrl = useXRInputSourceState('controller', 'left'); const rightCtrl = useXRInputSourceState('controller', 'right'); - const [open, setOpen] = useState(true); - const [tab, setTab] = useState('scene'); + + const open = useModelStore((s) => s.xrHudOpen); + const setOpen = useModelStore((s) => s.setXrHudOpen); + const tab = useModelStore((s) => s.xrHudTab) as Tab; + const setTab = useModelStore((s) => s.setXrHudTab) as unknown as (t: Tab) => void; + /** 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). */ @@ -82,7 +86,7 @@ export function XRHudInWorld(props: XRHudInWorldProps) { if (gp) { const aBtn = gp.buttons[4]; const pressed = !!aBtn?.pressed; - if (pressed && !lastABtn.current) setOpen((v) => !v); + if (pressed && !lastABtn.current) setOpen(!open); lastABtn.current = pressed; } @@ -150,11 +154,11 @@ export function XRHudInWorld(props: XRHudInWorldProps) { } } - // Head-locked fallback toggle — always visible in lower-left of FOV, + // Head-locked fallback toggle — always visible in lower-right of FOV, // so the user can reopen the menu even without controllers or if the // panel is positioned out of view. if (headLockRef.current) { - const offset = new THREE.Vector3(-0.18, -0.18, -0.5).applyQuaternion(camera.quaternion); + const offset = new THREE.Vector3(0.18, -0.18, -0.5).applyQuaternion(camera.quaternion); headLockRef.current.position.copy(camera.position).add(offset); headLockRef.current.quaternion.copy(camera.quaternion); } @@ -165,16 +169,21 @@ export function XRHudInWorld(props: XRHudInWorldProps) { return ( <> - setOpen((v) => !v)} /> - {/* Head-locked fallback: always-visible quick controls in lower-left of FOV */} + setOpen(!open)} /> + {/* Head-locked fallback: always-visible quick controls in lower-right of FOV */} - { setOpen((v) => !v); reanchorRequested.current = true; }} + onClick={() => { setOpen(!open); reanchorRequested.current = true; }} fontSize={0.0085} /> - setShowGrid(!showGrid)} fontSize={0.0085} /> + { + gl.xr.getSession()?.end(); + }} fontSize={0.0085} /> {open && ( diff --git a/src/stores/useModelStore.ts b/src/stores/useModelStore.ts index 81d1d86..1983c88 100644 --- a/src/stores/useModelStore.ts +++ b/src/stores/useModelStore.ts @@ -299,6 +299,12 @@ interface ModelStore { xrFeatures: XRFeatureFlags; setXRFeature: (key: keyof XRFeatureFlags, value: boolean) => void; resetXRFeatures: () => void; + + // ── WebXR HUD states persistent cache ──── + xrHudOpen: boolean; + setXrHudOpen: (open: boolean) => void; + xrHudTab: string; + setXrHudTab: (tab: string) => void; } /** Sync top-level legacy `model` / `fineTuning` from active SceneModel. */ @@ -673,4 +679,9 @@ export const useModelStore = create((set, get) => ({ saveXRFeatures(next); set({ xrFeatures: next }); }, + + xrHudOpen: true, + setXrHudOpen: (open) => set({ xrHudOpen: open }), + xrHudTab: 'scene', + setXrHudTab: (tab) => set({ xrHudTab: tab }), }));