diff --git a/packages/app/src/components/Header.tsx b/packages/app/src/components/Header.tsx index 9f906fe4..e82fb44d 100644 --- a/packages/app/src/components/Header.tsx +++ b/packages/app/src/components/Header.tsx @@ -1,6 +1,7 @@ import { lazy, Suspense, useEffect, useState } from "react"; import { BookOpen } from "@phosphor-icons/react/dist/ssr/BookOpen"; import { Mouse } from "@phosphor-icons/react/dist/ssr/Mouse"; +import { Goggles } from "@phosphor-icons/react/dist/ssr/Goggles"; import { Sparkle } from "@phosphor-icons/react/dist/ssr/Sparkle"; import { CaretRight } from "@phosphor-icons/react/dist/ssr/CaretRight"; import { Export } from "@phosphor-icons/react/dist/ssr/Export"; @@ -37,6 +38,7 @@ import { useAppCommands } from "@/hooks/useAppCommands"; import { COMMAND_ICONS } from "@/lib/command-icons"; import { useCapabilities } from "@/lib/capabilities"; import { useNativeMenu } from "@/hooks/useNativeMenu"; +import { xrStore, useXRPresenting, useXRSupportStore } from "@/stores/xr-store"; // Lazy so the prefs dialog (and its wasm-backed keybinding hook) doesn't // bloat the Header bundle until the user opens it. @@ -307,6 +309,36 @@ function RayTracingSubmenu() { ); } +/** XR entries for the View menu. Always shown — disabled when the browser + * doesn't report support, so users can see the capability exists. */ +function XRMenuItems() { + const vr = useXRSupportStore((s) => s.vr); + const ar = useXRSupportStore((s) => s.ar); + const presenting = useXRPresenting(); + + if (presenting) { + return ( + xrStore.getState().session?.end()} + > + Exit XR + + ); + } + + return ( + <> + xrStore.enterVR()}> + Enter VR + + xrStore.enterAR()}> + Enter AR + + + ); +} + export function Header({ onAboutOpen, onProductOpen, onSave, onOpen, onShareOpen, onVersionHistoryOpen, children }: HeaderProps) { const user = useAuthStore((s) => s.user); const isAnonymous = useAuthStore((s) => s.isAnonymous); @@ -565,6 +597,7 @@ export function Header({ onAboutOpen, onProductOpen, onSave, onOpen, onShareOpen > {t("menu.view.input_preferences")} + diff --git a/packages/app/src/components/Viewport.tsx b/packages/app/src/components/Viewport.tsx index 016ac448..f874e06b 100644 --- a/packages/app/src/components/Viewport.tsx +++ b/packages/app/src/components/Viewport.tsx @@ -7,7 +7,6 @@ import { DrawingView } from "./DrawingView"; import { TriangleInspectionPanel } from "./TriangleInspector"; import { RayTracedViewportOverlay } from "./RayTracedViewport"; import { FollowModeToggle } from "./FollowModeToggle"; -import { EnterXRButton } from "./xr/EnterXRButton"; import { xrStore, useXRPresenting } from "@/stores/xr-store"; import { useUiStore, @@ -350,7 +349,6 @@ export function Viewport() { drags in the gaps; the toggle itself re-enables them. */} {!electronicsActive && (
-
)} diff --git a/packages/app/src/components/xr/EnterXRButton.tsx b/packages/app/src/components/xr/EnterXRButton.tsx deleted file mode 100644 index 4f750f9a..00000000 --- a/packages/app/src/components/xr/EnterXRButton.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { useXR } from "@react-three/xr"; -import { xrStore, useXRSupportStore } from "@/stores/xr-store"; - -/** - * Floating Enter-XR control. Renders nothing if neither VR nor AR is - * supported, so desktop users never see it. - */ -export function EnterXRButton() { - const checked = useXRSupportStore((s) => s.checked); - const vr = useXRSupportStore((s) => s.vr); - const ar = useXRSupportStore((s) => s.ar); - // `mode` becomes a string (e.g. "immersive-vr") while a session is active. - const mode = useXR((s) => s.mode); - - if (!checked || (!vr && !ar)) return null; - const presenting = mode != null; - - if (presenting) { - return ( - - ); - } - - return ( -
- {ar && ( - - )} - {vr && ( - - )} -
- ); -} diff --git a/packages/app/src/stores/xr-store.ts b/packages/app/src/stores/xr-store.ts index 5c5c4892..6885b63e 100644 --- a/packages/app/src/stores/xr-store.ts +++ b/packages/app/src/stores/xr-store.ts @@ -12,6 +12,10 @@ export const xrStore = createXRStore({ hand: true, controller: true, foveation: 0, + // Disable the bundled iwer device emulator. It would otherwise auto-inject a + // floating "Enter XR" DOM button on localhost, which clashes with our + // in-menubar XR entry. Real headsets are unaffected. + emulate: false, }); type SupportState = {